@cuboapp/api-backend 1.0.38 → 3.0.1

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,110 @@
1
+ import type {
2
+ CuboCrdtServer,
3
+ CuboCrdtServerDocumentOrigin,
4
+ CuboCrdtServerSubscribe,
5
+ CuboCrdtSocketClient
6
+ } from '@cuboapp/crdt'
7
+ import { CuboApiEntitiesMap, CuboEntity } from '@cuboapp/types'
8
+ import type { WsServer, WsServerSocket } from '@cuboapp/ws'
9
+
10
+ import type { CuboBackendApi } from '../core'
11
+
12
+ /** Тип фабрики, импортируемый только как тип (рантайм-зависимость опциональна). */
13
+ type CreateCrdtServer = typeof import('@cuboapp/crdt')['createCrdtServer']
14
+
15
+ /**
16
+ * Встроенный адаптер cubo-crdt. Инкапсулирует всю обвязку, которую раньше
17
+ * приходилось писать в каждом приложении руками (old.ts):
18
+ * - fetchRows: подписка тянет строки через api.getMany
19
+ * - storeRow: дебаунс-сохранение состояния дока обратно в БД через api.updateOne
20
+ * - checkRowIsSutable: фильтрация строк под подписку (augmentation.filterRowForCrdtEvent)
21
+ * - push create/update/delete: автоматически из CRUD-методов ядра
22
+ *
23
+ * Включается одной опцией `crdt: { ws }` у CuboBackendApi и работает для всех
24
+ * сущностей с флагом `entity.crdt === true`.
25
+ */
26
+ export class CuboCrdtAdapter<T extends CuboApiEntitiesMap<T>, A> {
27
+ public readonly server: CuboCrdtServer<T, A>
28
+
29
+ constructor(
30
+ private api: CuboBackendApi<T, A>,
31
+ ws: WsServer,
32
+ createCrdtServer: CreateCrdtServer,
33
+ private debug = false
34
+ ) {
35
+ const entities = (this.api.options.entities || []).filter((e) => e.crdt).map((e) => e.alias as Extract<keyof T, string>)
36
+
37
+ this.server = createCrdtServer<T, A>({
38
+ ws,
39
+ entities,
40
+ debug: this.debug,
41
+
42
+ // подписка -> строки из БД (хуки CRUD пропускаем, чтобы не зациклить)
43
+ fetchRows: async (client, subscribe: CuboCrdtServerSubscribe) => {
44
+ const { rows } = await this.api.getMany(subscribe.entity as Extract<keyof T, string>, { query: subscribe.filters }, {
45
+ auth: (client as CuboCrdtSocketClient<A>).auth,
46
+ excludeHooks: ['afterCrdtCreate', 'afterCrdtUpdate']
47
+ })
48
+
49
+ return rows as T[Extract<keyof T, string>][]
50
+ },
51
+
52
+ // сохранение состояния дока обратно в БД (origin прокидываем, чтобы не пушить заново)
53
+ storeRow: async (entity, entity_id, body, { origin, client }) => {
54
+ await this.api.updateOne(entity as Extract<keyof T, string>, { query: { id: entity_id }, body }, {
55
+ crdt: origin,
56
+ performer_id: (client as any)?.auth?.user_id,
57
+ auth: (client as any)?.auth,
58
+ excludeHooks: ['afterCrdtUpdate']
59
+ })
60
+ },
61
+
62
+ // фильтрация строки под конкретную подписку
63
+ checkRowIsSutable: (base, { entity, subscribe, row }) => {
64
+ const augmentation = this.api.options.augmentations?.[entity as Extract<keyof T, string>]
65
+ if (augmentation?.filterRowForCrdtEvent) {
66
+ return augmentation.filterRowForCrdtEvent(row, subscribe.filters || {})
67
+ }
68
+ return base
69
+ }
70
+ })
71
+ }
72
+
73
+ public async init() {
74
+ await this.server.init()
75
+ }
76
+
77
+ public async destroy() {
78
+ await this.server.destroy()
79
+ }
80
+
81
+ // lifecycle клиентов — приложение вызывает из ws onConnect/onDisconnect
82
+ public addClient(client: WsServerSocket<{ auth?: A }>) {
83
+ this.server.addClient(client)
84
+ }
85
+
86
+ public removeClient(client: WsServerSocket) {
87
+ this.server.removeClient(client)
88
+ }
89
+
90
+ /** Поддерживается ли CRDT для сущности. */
91
+ public isSupported(entity: CuboEntity): boolean {
92
+ return !!entity.crdt
93
+ }
94
+
95
+ // пуш изменений документов из CRUD-методов ядра
96
+ public pushCreate(entity: CuboEntity, row: any) {
97
+ if (!entity.crdt) return
98
+ this.server.ensureDocument(entity.alias as Extract<keyof T, string>, row)
99
+ }
100
+
101
+ public pushUpdate(entity: CuboEntity, row: any) {
102
+ if (!entity.crdt) return
103
+ this.server.ensureDocument(entity.alias as Extract<keyof T, string>, row)
104
+ }
105
+
106
+ public pushDelete(entity: CuboEntity, row: any, origin?: CuboCrdtServerDocumentOrigin) {
107
+ if (!entity.crdt) return
108
+ this.server.deleteDocument(entity.alias as Extract<keyof T, string>, row, origin)
109
+ }
110
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Слой диалектов БД. Раньше SQL-генерация была заточена под Postgres
3
+ * (`ilike`, `returning`). Теперь различия спрятаны за интерфейсом CuboDialect,
4
+ * и движок одинаково работает с Postgres и MySQL.
5
+ *
6
+ * Сам @cuboapp/database уже умеет insert/update/delete под оба диалекта;
7
+ * здесь покрываются различия SELECT-билдера (в первую очередь ILIKE и
8
+ * пакетные insert/update).
9
+ */
10
+ export type CuboDialectName = 'postgres' | 'mysql'
11
+
12
+ export interface CuboDialect {
13
+ name: CuboDialectName
14
+
15
+ /** Регистронезависимый LIKE. Postgres — нативный ILIKE; MySQL — LOWER() LIKE LOWER(). */
16
+ ilike(field: string, param: string, isNot: boolean): string
17
+
18
+ /** Поддерживает ли RETURNING (Postgres). У MySQL — insertId / lastval. */
19
+ supportsReturning: boolean
20
+ }
21
+
22
+ export const postgresDialect: CuboDialect = {
23
+ name: 'postgres',
24
+ supportsReturning: true,
25
+ ilike: (field, param, isNot) => `${field} ${isNot ? 'not ilike' : 'ilike'} :${param}`
26
+ }
27
+
28
+ export const mysqlDialect: CuboDialect = {
29
+ name: 'mysql',
30
+ supportsReturning: false,
31
+ ilike: (field, param, isNot) => `lower(${field}) ${isNot ? 'not like' : 'like'} lower(:${param})`
32
+ }
33
+
34
+ export function getDialect(name?: string | null): CuboDialect {
35
+ return name === 'mysql' ? mysqlDialect : postgresDialect
36
+ }
@@ -3,7 +3,7 @@ import { Database, createDatabase } from '@cuboapp/database'
3
3
  import { CuboApiEntitiesMap, CuboEntity, CuboEntityField, CuboEntityFieldExtra, CuboEntityFieldType } from '@cuboapp/types'
4
4
  import { cloneDeep } from '@cuboapp/utils'
5
5
 
6
- import { CuboBackendApi } from '../..'
6
+ import type { CuboBackendApi } from '../core'
7
7
  import {
8
8
  CUBO_CRUD_DEFAULT_FIELDS,
9
9
  CUBO_CRUD_DEFAULT_PAGE_LIMIT,
@@ -12,19 +12,25 @@ import {
12
12
  CUBO_CRUD_QUERY_KEY_REGEX,
13
13
  CUBO_CRUD_QUERY_SYMBOL_NOT
14
14
  } from '../constants'
15
- import { CuboBackendApiDbConnectionType, CuboCrudFindQuery, CuboCrudRequest } from '../types'
15
+ import { CuboDialect, getDialect } from '../dialects'
16
+ import { CuboBackendApiDbConnectionType, CuboCrudFindQuery, CuboCrudRequest, CuboDataFilter, CuboDataFiltersQuery } from '../types'
16
17
 
17
18
  import { ApiHelpersConvert } from './convert'
18
19
  import { ApiHelpersData } from './data'
19
20
  import { ApiHelpersWithes } from './withes'
20
21
 
21
- export class ApiHelpers<T extends CuboApiEntitiesMap<T>, A> {
22
+ export class ApiHelpers<T extends CuboApiEntitiesMap<T>, A = unknown> {
22
23
  constructor(public api: CuboBackendApi<T, A>) {}
23
24
 
24
25
  public withes = new ApiHelpersWithes<T>(this)
25
26
  public convert = new ApiHelpersConvert<T>(this)
26
27
  public data = new ApiHelpersData<T>()
27
28
 
29
+ /** Текущий диалект БД (postgres по умолчанию). */
30
+ public get dialect(): CuboDialect {
31
+ return getDialect(this.api.options.db?.options?.dialect as string | undefined)
32
+ }
33
+
28
34
  private cache: Record<string, any> = {}
29
35
  private fetching: Record<string, Promise<any> | null> = {}
30
36
  private connections: Partial<Record<CuboBackendApiDbConnectionType, Database>> = {}
@@ -237,7 +243,8 @@ export class ApiHelpers<T extends CuboApiEntitiesMap<T>, A> {
237
243
  // console.log(field, condition, value)
238
244
  switch (condition) {
239
245
  case CUBO_CRUD_QUERY_CONDITION.ILIKE:
240
- conditions.push(`${field} ${is_not ? 'not ilike' : 'ilike'} :${replacement}`)
246
+ // диалект-зависимо: postgres -> ILIKE, mysql -> LOWER() LIKE LOWER()
247
+ conditions.push(this.dialect.ilike(field, replacement, is_not))
241
248
  replacements[replacement] = '%' + value + '%'
242
249
  break
243
250
  case CUBO_CRUD_QUERY_CONDITION.LIKE:
@@ -309,6 +316,106 @@ export class ApiHelpers<T extends CuboApiEntitiesMap<T>, A> {
309
316
  }
310
317
  }
311
318
 
319
+ /**
320
+ * Разворачивает структурный query-параметр `filters` (JSON вида
321
+ * `{"filters":[{"field","op","value"}],"sort":[{"field","dir"}]}`) в обычные
322
+ * flat-DSL записи `req.query`. Дальше их подхватывает общий пайплайн
323
+ * (createFindQuery + withes): вложенные связи по dot-notation, приведение типов
324
+ * и сортировка работают без дополнительного кода. Мутирует и возвращает `query`.
325
+ *
326
+ * Ограничения наследуются от flat-DSL: одно условие на поле (при нескольких
327
+ * фильтрах на одно поле побеждает последний) и значения без `:` (двоеточие —
328
+ * разделитель условий). Для экзотики — `queryOptions.raw`.
329
+ */
330
+ public applyDataFilters(query: Record<string, any>): Record<string, any> {
331
+ const raw = query?.filters
332
+ if (raw === undefined || raw === null || raw === '') {
333
+ return query
334
+ }
335
+
336
+ let parsed: CuboDataFiltersQuery
337
+ try {
338
+ parsed = typeof raw === 'string' ? JSON.parse(raw) : raw
339
+ } catch {
340
+ throw { status: 400, text: 'Invalid "filters" query param: must be valid JSON' }
341
+ }
342
+
343
+ // убираем сырой ключ, чтобы пайплайн не принял "filters" за поле-условие
344
+ delete query.filters
345
+
346
+ for (const filter of parsed?.filters || []) {
347
+ if (!filter || !filter.field) {
348
+ continue
349
+ }
350
+
351
+ const value = this.dataFilterToQueryValue(filter)
352
+ if (value !== undefined) {
353
+ query[filter.field] = value
354
+ }
355
+ }
356
+
357
+ if (parsed?.sort?.length) {
358
+ const sort = parsed.sort
359
+ .filter((s) => s && s.field)
360
+ .map((s) => (s.dir === 'desc' ? '-' : '') + s.field)
361
+ .join(',')
362
+
363
+ if (sort) {
364
+ query.sort = sort
365
+ }
366
+ }
367
+
368
+ return query
369
+ }
370
+
371
+ /** Маппинг `CuboDataFilter.op` -> flat-DSL значение движка (`ilike:foo`, `not:gt:5`, `in:1,2`, ...). */
372
+ private dataFilterToQueryValue(filter: CuboDataFilter): string | undefined {
373
+ const C = CUBO_CRUD_QUERY_CONDITION
374
+ const NOT = CUBO_CRUD_QUERY_SYMBOL_NOT
375
+ const v = filter.value
376
+ const list = () => (Array.isArray(v) ? v.join(',') : `${v}`)
377
+
378
+ switch (filter.op) {
379
+ case 'eq':
380
+ return `${v}`
381
+ case 'ne':
382
+ return `${NOT}:${v}`
383
+ case 'contains':
384
+ return `${C.ILIKE}:${v}`
385
+ case 'notContains':
386
+ return `${NOT}:${C.ILIKE}:${v}`
387
+ case 'like':
388
+ return `${C.LIKE}:${v}`
389
+ case 'ilike':
390
+ return `${C.ILIKE}:${v}`
391
+ case 'gt':
392
+ return `${C.GT}:${v}`
393
+ case 'gte':
394
+ return `${C.GTE}:${v}`
395
+ case 'lt':
396
+ return `${C.LT}:${v}`
397
+ case 'lte':
398
+ return `${C.LTE}:${v}`
399
+ case 'in':
400
+ return `${C.IN}:${list()}`
401
+ case 'nin':
402
+ return `${C.NIN}:${list()}`
403
+ case 'between': {
404
+ const [from, to] = Array.isArray(v) ? v : `${v}`.split(':')
405
+ if (from === undefined || to === undefined) {
406
+ throw { status: 400, text: `Filter "${filter.field}": "between" requires [from, to]` }
407
+ }
408
+ return `${C.BTW}:${from}:${to}`
409
+ }
410
+ case 'isNull':
411
+ return 'null'
412
+ case 'isNotNull':
413
+ return `${NOT}:null`
414
+ default:
415
+ throw { status: 400, text: `Unsupported filter op "${(filter as any).op}" for "${filter.field}"` }
416
+ }
417
+ }
418
+
312
419
  public async dropConnection(type: CuboBackendApiDbConnectionType) {
313
420
  if (this.connections[type]) {
314
421
  await this.connections[type].disconnect()
@@ -376,7 +483,7 @@ export class ApiHelpers<T extends CuboApiEntitiesMap<T>, A> {
376
483
  const havings = dto.havings ?? []
377
484
  const sorts = dto.sorts ?? []
378
485
  const replacements = dto.replacements ?? {}
379
- const groupBy = dto.groupBy ?? undefined
486
+ const groupBy = dto.groupBy ?? []
380
487
 
381
488
  const { sort, limit: _limit, page, with: _withes, ...query } = cloneDeep(req.query || {})
382
489
 
@@ -452,7 +559,7 @@ export class ApiHelpers<T extends CuboApiEntitiesMap<T>, A> {
452
559
 
453
560
  let offsetString = ''
454
561
  let limitString = ''
455
- let groupByString = queryDto?.groupBy ?? ''
562
+ const groupByString = groupBy.length ? 'group by ' + groupBy.join(', ') : ''
456
563
 
457
564
  if (!isCount && perPage > 0) {
458
565
  if (filtersOffset > 0) {
@@ -2,7 +2,7 @@ import { CuboApiEntitiesMap, CuboApiEntityDefaultUsersKeys, CuboEntity, CuboEnti
2
2
  import { cloneDeep } from '@cuboapp/utils'
3
3
 
4
4
  import { CUBO_CRUD_DEFAULT_FIELDS, CUBO_CRUD_QUERY_KEY_REGEX } from '../constants'
5
- import { CuboCrudFindQuery, CuboCrudMethodOptions, CuboCrudRequest, CuboCrudWith } from '../types'
5
+ import { CuboCrudFindQuery, CuboCrudMethodOptions, CuboCrudRequest, CuboCrudWith, normalizeCuboCrudQueryOptions } from '../types'
6
6
 
7
7
  import { ApiHelpers } from '.'
8
8
 
@@ -20,7 +20,7 @@ export class ApiHelpersWithes<T extends CuboApiEntitiesMap<T>> {
20
20
  sorts?: string[]
21
21
  conditions?: string[]
22
22
  havings?: string[]
23
- groupBy?: string
23
+ groupBy?: string[]
24
24
  conditionKey?: string
25
25
  conditionValue?: any
26
26
  sortKey?: string
@@ -141,7 +141,7 @@ export class ApiHelpersWithes<T extends CuboApiEntitiesMap<T>> {
141
141
  withes,
142
142
  joins: opts.joins,
143
143
  conditions: opts.conditions || undefined,
144
- groupBy: opts.groupBy || undefined,
144
+ groupBy: opts.groupBy ?? [],
145
145
  havings: opts.havings || undefined,
146
146
  replacements: opts.replacements || undefined
147
147
  }
@@ -226,19 +226,23 @@ export class ApiHelpersWithes<T extends CuboApiEntitiesMap<T>> {
226
226
  public async prepare(req: CuboCrudRequest, entity: CuboEntity, opts?: CuboCrudMethodOptions): Promise<Partial<CuboCrudFindQuery>> {
227
227
  const { with: __withes, sort: _sort, limit: _limit, ...other } = req.query || {}
228
228
 
229
+ // нормализуем один раз: дальше работаем с гарантированными массивами (deep-copy,
230
+ // поэтому push'и ниже не затрагивают исходные queryOptions)
231
+ const queryOptions = normalizeCuboCrudQueryOptions(opts?.queryOptions)
232
+
229
233
  const _withes = (__withes || '').split(',').filter((i: string) => i !== '')
230
234
 
231
- if (opts?.queryOptions?.withes?.length) {
232
- _withes.push(...(opts?.queryOptions?.withes || []).filter((i: string) => i !== ''))
235
+ if (queryOptions.withes.length) {
236
+ _withes.push(...queryOptions.withes.filter((i: string) => i !== ''))
233
237
  }
234
238
 
235
- const joins: string[] = [...(opts?.queryOptions?.joins || [])]
236
- const selects: string[] = [...(opts?.queryOptions?.selects || [])]
237
- const conditions: string[] = [...(opts?.queryOptions?.conditions || [])]
238
- const havings: string[] = [...(opts?.queryOptions?.havings || [])]
239
- const replacements: Record<string, any> = { ...(opts?.queryOptions?.replacements || {}) }
240
- const groupBy: string = opts?.queryOptions?.groupBy || undefined
241
- const sorts: string[] = opts?.queryOptions?.sorts || []
239
+ const joins = queryOptions.joins
240
+ const selects = queryOptions.selects
241
+ const conditions = queryOptions.conditions
242
+ const havings = queryOptions.havings
243
+ const replacements = queryOptions.replacements
244
+ const groupBy = queryOptions.groupBy
245
+ const sorts = queryOptions.sorts
242
246
 
243
247
  const withes: CuboCrudWith[] = []
244
248
 
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Единый источник правды по именам хуков. Раньше список ключей `excludeHooks`
3
+ * дублировался руками в типе CuboCrudMethodOptions и легко рассинхронизировался.
4
+ */
5
+ export const CUBO_HOOKS = [
6
+ 'beforeGetMany',
7
+ 'afterGetMany',
8
+ 'beforeGetOne',
9
+ 'afterGetOne',
10
+ 'beforeCreate',
11
+ 'afterCreate',
12
+ 'beforeUpdate',
13
+ 'afterUpdate',
14
+ 'beforeDelete',
15
+ 'afterDelete',
16
+ 'afterCrdtCreate',
17
+ 'afterCrdtUpdate',
18
+ 'baseHooks'
19
+ ] as const
20
+
21
+ export type CuboHookName = (typeof CUBO_HOOKS)[number]
@@ -0,0 +1,76 @@
1
+ import { CuboApiEntitiesMap } from '@cuboapp/types'
2
+
3
+ import { CuboAugmentation, CuboAugmentationsStore } from './types'
4
+
5
+ /**
6
+ * Identity-хелпер: фиксирует тип строки сущности (и auth) ОДИН раз, после чего
7
+ * в каждом хуке `ctx` выводится контекстно — аннотировать `CuboHookCtx<...>`
8
+ * больше не нужно.
9
+ *
10
+ * @example
11
+ * export const taskAugmentation = defineAugmentation<Task, Auth>({
12
+ * async beforeGetMany({ req, queryOptions }) { // ← всё типизировано без аннотаций
13
+ * // ctx.queryOptions уже нормализован: массивы/объекты гарантированно есть,
14
+ * // можно сразу мутировать без `|| []` / `|| {}`
15
+ * if (req.query?.company_id) queryOptions.replacements.company_id = Number(req.query.company_id)
16
+ * return queryOptions
17
+ * }
18
+ * })
19
+ */
20
+ export function defineAugmentation<R, A = unknown>(augmentation: CuboAugmentation<R, A>): CuboAugmentation<R, A> {
21
+ return augmentation
22
+ }
23
+
24
+ /** Тип функции-хука аугментации по ключу `K` (без `undefined`). */
25
+ export type CuboAugmentationHook<R, A, K extends keyof CuboAugmentation<R, A>> = NonNullable<CuboAugmentation<R, A>[K]>
26
+
27
+ /**
28
+ * Identity-хелпер для ОДНОГО хука: фиксирует тип строки (R) и auth (A), а тип
29
+ * `callback` выводится из переданного `key` — например, для `'beforeGetMany'`
30
+ * это будет ровно сигнатура `beforeGetMany`. Возвращает типизированную функцию
31
+ * хука: её можно положить в нужный ключ аугментации или переиспользовать.
32
+ *
33
+ * Реализовано перегрузками (overloads): один вызов, R/A задаются явно, нужная
34
+ * сигнатура выбирается по строковому литералу `key`.
35
+ *
36
+ * @example
37
+ * const beforeGetMany = defineAugmentationHook<Task, Auth>('beforeGetMany', ({ req, queryOptions }) => {
38
+ * if (req.query?.company_id) queryOptions.conditions.push('t1.company_id = :company_id')
39
+ * return queryOptions
40
+ * })
41
+ *
42
+ * export const taskAugmentation: CuboAugmentation<Task, Auth> = { beforeGetMany }
43
+ */
44
+ export function defineAugmentationHook<R, A = unknown>(key: 'beforeGetMany', callback: CuboAugmentationHook<R, A, 'beforeGetMany'>): CuboAugmentationHook<R, A, 'beforeGetMany'>
45
+ export function defineAugmentationHook<R, A = unknown>(key: 'beforeGetOne', callback: CuboAugmentationHook<R, A, 'beforeGetOne'>): CuboAugmentationHook<R, A, 'beforeGetOne'>
46
+ export function defineAugmentationHook<R, A = unknown>(key: 'beforeCreate', callback: CuboAugmentationHook<R, A, 'beforeCreate'>): CuboAugmentationHook<R, A, 'beforeCreate'>
47
+ export function defineAugmentationHook<R, A = unknown>(key: 'beforeUpdate', callback: CuboAugmentationHook<R, A, 'beforeUpdate'>): CuboAugmentationHook<R, A, 'beforeUpdate'>
48
+ export function defineAugmentationHook<R, A = unknown>(key: 'beforeDelete', callback: CuboAugmentationHook<R, A, 'beforeDelete'>): CuboAugmentationHook<R, A, 'beforeDelete'>
49
+ export function defineAugmentationHook<R, A = unknown>(key: 'afterGetMany', callback: CuboAugmentationHook<R, A, 'afterGetMany'>): CuboAugmentationHook<R, A, 'afterGetMany'>
50
+ export function defineAugmentationHook<R, A = unknown>(key: 'afterGetOne', callback: CuboAugmentationHook<R, A, 'afterGetOne'>): CuboAugmentationHook<R, A, 'afterGetOne'>
51
+ export function defineAugmentationHook<R, A = unknown>(key: 'afterCreate', callback: CuboAugmentationHook<R, A, 'afterCreate'>): CuboAugmentationHook<R, A, 'afterCreate'>
52
+ export function defineAugmentationHook<R, A = unknown>(key: 'afterUpdate', callback: CuboAugmentationHook<R, A, 'afterUpdate'>): CuboAugmentationHook<R, A, 'afterUpdate'>
53
+ export function defineAugmentationHook<R, A = unknown>(key: 'afterDelete', callback: CuboAugmentationHook<R, A, 'afterDelete'>): CuboAugmentationHook<R, A, 'afterDelete'>
54
+ export function defineAugmentationHook<R, A = unknown>(key: 'afterCrdtCreate', callback: CuboAugmentationHook<R, A, 'afterCrdtCreate'>): CuboAugmentationHook<R, A, 'afterCrdtCreate'>
55
+ export function defineAugmentationHook<R, A = unknown>(key: 'afterCrdtUpdate', callback: CuboAugmentationHook<R, A, 'afterCrdtUpdate'>): CuboAugmentationHook<R, A, 'afterCrdtUpdate'>
56
+ export function defineAugmentationHook<R, A = unknown>(key: 'can', callback: CuboAugmentationHook<R, A, 'can'>): CuboAugmentationHook<R, A, 'can'>
57
+ export function defineAugmentationHook<R, A = unknown>(key: 'filterRowForCrdtEvent', callback: CuboAugmentationHook<R, A, 'filterRowForCrdtEvent'>): CuboAugmentationHook<R, A, 'filterRowForCrdtEvent'>
58
+ export function defineAugmentationHook<R, A = unknown>(key: keyof CuboAugmentation<R, A>, callback: any): any {
59
+ return callback
60
+ }
61
+
62
+ /**
63
+ * То же, но сразу для всей карты аугментаций. Внутри каждой сущности `ctx`
64
+ * хуков типизируется по её строке автоматически.
65
+ *
66
+ * @example
67
+ * export const augmentations = defineAugmentations<Entities, Auth>({
68
+ * task: { async beforeGetMany({ req, queryOptions }) { ... } },
69
+ * user: { async afterGetOne({ row }) { ... } }
70
+ * })
71
+ */
72
+ export function defineAugmentations<T extends CuboApiEntitiesMap<T>, A = unknown>(
73
+ store: Partial<CuboAugmentationsStore<T, A>>
74
+ ): Partial<CuboAugmentationsStore<T, A>> {
75
+ return store
76
+ }
@@ -0,0 +1,3 @@
1
+ export * from './constants'
2
+ export * from './types'
3
+ export * from './define'
@@ -0,0 +1,52 @@
1
+ import { CuboApiEntitiesMap, CuboEntity } from '@cuboapp/types'
2
+
3
+ import { CuboCrudAction, CuboCrudGetManyResponse, CuboCrudRequest } from '../types/basic'
4
+ import { CuboCrudQueryOptions, CuboCrudQueryOptionsInput } from '../types/db'
5
+
6
+ export type MaybePromise<X> = X | Promise<X>
7
+
8
+ /**
9
+ * Контекст хука. `queryOptions` — это per-call РАБОЧАЯ КОПИЯ опций: каждый вызов
10
+ * получает свой свежий объект, так что состояние НЕ «накапливается» и не
11
+ * протекает между вызовами (внутренние getOne после create/update чужие опции
12
+ * не наследуют). Хук может либо мутировать эту копию, либо вернуть свой объект
13
+ * опций — движок возьмёт возвращённое значение (если оно есть), иначе текущую
14
+ * (возможно, изменённую) копию.
15
+ */
16
+ export type CuboHookCtx<R, A = unknown> = {
17
+ entity: CuboEntity
18
+ req: CuboCrudRequest
19
+ auth?: A
20
+ performer_id?: number
21
+ queryOptions: CuboCrudQueryOptions
22
+ }
23
+
24
+ export interface CuboAugmentation<R, A = unknown> {
25
+ // before*: формируют query-опции (мутируют ctx.queryOptions и/или возвращают патч опций)
26
+ beforeGetMany?(ctx: CuboHookCtx<R, A>): MaybePromise<CuboCrudQueryOptionsInput | void>
27
+ beforeGetOne?(ctx: CuboHookCtx<R, A>): MaybePromise<CuboCrudQueryOptionsInput | void>
28
+ beforeCreate?(ctx: CuboHookCtx<R, A>): MaybePromise<CuboCrudQueryOptionsInput | void>
29
+ beforeUpdate?(ctx: CuboHookCtx<R, A> & { row: R }): MaybePromise<CuboCrudQueryOptionsInput | void>
30
+ beforeDelete?(ctx: CuboHookCtx<R, A> & { row: R }): MaybePromise<CuboCrudQueryOptionsInput | void>
31
+
32
+ // after*: трансформируют результат
33
+ afterGetMany?(ctx: CuboHookCtx<R, A> & { result: CuboCrudGetManyResponse<R> }): MaybePromise<CuboCrudGetManyResponse<R>>
34
+ afterGetOne?(ctx: CuboHookCtx<R, A> & { row: R }): MaybePromise<R | undefined>
35
+ afterCreate?(ctx: CuboHookCtx<R, A> & { row: R }): MaybePromise<R>
36
+ afterUpdate?(ctx: CuboHookCtx<R, A> & { row: R; prev: R }): MaybePromise<R>
37
+ afterDelete?(ctx: CuboHookCtx<R, A> & { row: R }): MaybePromise<boolean>
38
+
39
+ // crdt-хуки (вызываются адаптером cubo-crdt)
40
+ afterCrdtCreate?(ctx: CuboHookCtx<R, A> & { row: R }): MaybePromise<void>
41
+ afterCrdtUpdate?(ctx: CuboHookCtx<R, A> & { row: R }): MaybePromise<void>
42
+
43
+ // авторизация конкретного действия над строкой
44
+ can?(action: CuboCrudAction, ctx: CuboHookCtx<R, A> & { row?: R }): MaybePromise<boolean>
45
+
46
+ // фильтр строки под CRDT-подписку
47
+ filterRowForCrdtEvent?(row: R, filters: Record<string, any>): boolean
48
+ }
49
+
50
+ export type CuboAugmentationsStore<T extends CuboApiEntitiesMap<T>, A = unknown> = {
51
+ [K in Extract<keyof T, string>]?: CuboAugmentation<T[K], A>
52
+ }