@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.
- package/package.json +33 -11
- package/src/core/index.ts +743 -0
- package/src/crdt/index.ts +110 -0
- package/src/dialects/index.ts +36 -0
- package/src/helpers/index.ts +113 -6
- package/src/helpers/withes.ts +16 -12
- package/src/hooks/constants.ts +21 -0
- package/src/hooks/define.ts +76 -0
- package/src/hooks/index.ts +3 -0
- package/src/hooks/types.ts +52 -0
- package/src/index.ts +6 -443
- package/src/query/compile.ts +212 -0
- package/src/query/index.ts +2 -0
- package/src/query/types.ts +92 -0
- package/src/types/basic.ts +1 -1
- package/src/types/db.ts +74 -23
- package/src/types/filters.ts +50 -0
- package/src/types/index.ts +20 -8
- package/tsconfig.tsbuildinfo +1 -0
- package/src/old.ts +0 -744
- package/src/s3/index.ts +0 -104
- package/src/s3/types/index.ts +0 -19
- package/src/s3/utils/index.ts +0 -74
- package/src/types/augmentation.ts +0 -51
|
@@ -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
|
+
}
|
package/src/helpers/index.ts
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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 ??
|
|
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
|
-
|
|
562
|
+
const groupByString = groupBy.length ? 'group by ' + groupBy.join(', ') : ''
|
|
456
563
|
|
|
457
564
|
if (!isCount && perPage > 0) {
|
|
458
565
|
if (filtersOffset > 0) {
|
package/src/helpers/withes.ts
CHANGED
|
@@ -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
|
|
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 (
|
|
232
|
-
_withes.push(...
|
|
235
|
+
if (queryOptions.withes.length) {
|
|
236
|
+
_withes.push(...queryOptions.withes.filter((i: string) => i !== ''))
|
|
233
237
|
}
|
|
234
238
|
|
|
235
|
-
const joins
|
|
236
|
-
const selects
|
|
237
|
-
const conditions
|
|
238
|
-
const havings
|
|
239
|
-
const replacements
|
|
240
|
-
const groupBy
|
|
241
|
-
const 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,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
|
+
}
|