@cuboapp/api-backend 3.0.16 → 4.0.0
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/constants/index.d.ts +35 -0
- package/dist/constants/index.d.ts.map +1 -0
- package/dist/core/index.d.ts +77 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/crdt/index.d.ts +45 -0
- package/dist/crdt/index.d.ts.map +1 -0
- package/dist/dialects/index.d.ts +21 -0
- package/dist/dialects/index.d.ts.map +1 -0
- package/dist/helpers/convert.d.ts +11 -0
- package/dist/helpers/convert.d.ts.map +1 -0
- package/dist/helpers/data.d.ts +5 -0
- package/dist/helpers/data.d.ts.map +1 -0
- package/dist/helpers/index.d.ts +42 -0
- package/dist/helpers/index.d.ts.map +1 -0
- package/dist/helpers/withes.d.ts +10 -0
- package/dist/helpers/withes.d.ts.map +1 -0
- package/dist/hooks/constants.d.ts +7 -0
- package/dist/hooks/constants.d.ts.map +1 -0
- package/dist/hooks/define.d.ts +63 -0
- package/dist/hooks/define.d.ts.map +1 -0
- package/dist/hooks/index.d.ts +5 -0
- package/dist/hooks/index.d.ts.map +1 -0
- package/dist/hooks/subscribe.d.ts +83 -0
- package/dist/hooks/subscribe.d.ts.map +1 -0
- package/dist/hooks/types.d.ts +60 -0
- package/dist/hooks/types.d.ts.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/query/compile.d.ts +14 -0
- package/dist/query/compile.d.ts.map +1 -0
- package/dist/query/index.d.ts +3 -0
- package/dist/query/index.d.ts.map +1 -0
- package/dist/query/types.d.ts +78 -0
- package/dist/query/types.d.ts.map +1 -0
- package/dist/types/basic.d.ts +60 -0
- package/dist/types/basic.d.ts.map +1 -0
- package/dist/types/db.d.ts +58 -0
- package/dist/types/db.d.ts.map +1 -0
- package/dist/types/index.d.ts +37 -0
- package/dist/types/index.d.ts.map +1 -0
- package/package.json +31 -21
- package/src/constants/index.ts +30 -20
- package/src/core/index.ts +6 -5
- package/src/crdt/index.ts +67 -13
- package/src/helpers/data.ts +14 -2
- package/src/helpers/index.ts +101 -46
- package/src/helpers/withes.ts +1 -1
- package/src/hooks/index.ts +1 -0
- package/src/hooks/subscribe.ts +160 -0
- package/src/hooks/types.ts +6 -3
- package/src/types/basic.ts +2 -2
- package/src/types/db.ts +1 -1
- package/.prettierrc +0 -8
- package/tsconfig.build.json +0 -4
- package/tsconfig.json +0 -22
- package/tsconfig.tsbuildinfo +0 -1
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Типизированный query-DSL.
|
|
3
|
+
*
|
|
4
|
+
* Поверх «строкового» DSL движка (`status_id=in:1,2`) строится полностью
|
|
5
|
+
* типизированный слой: операторы (`not/gte/lte/in/between/...`), типизированный
|
|
6
|
+
* `with` со вложенными связями и типизированные фильтры по вложенным сущностям.
|
|
7
|
+
*
|
|
8
|
+
* Сам объект `Query<R>` иммутабелен по построению — он компилируется в новый
|
|
9
|
+
* plain-object запроса на каждый вызов (см. ./compile).
|
|
10
|
+
*/
|
|
11
|
+
/** Скалярные значения колонок. */
|
|
12
|
+
export type CuboScalar = string | number | boolean | Date | null;
|
|
13
|
+
/** Снимаем массив/optional, чтобы добраться до строки связанной сущности. */
|
|
14
|
+
export type CuboRelatedRow<X> = NonNullable<X> extends (infer U)[] ? U : NonNullable<X>;
|
|
15
|
+
/** Ключи-скаляры (обычные колонки). */
|
|
16
|
+
export type CuboScalarKeys<R> = {
|
|
17
|
+
[K in keyof R]-?: NonNullable<R[K]> extends CuboScalar ? K : never;
|
|
18
|
+
}[keyof R];
|
|
19
|
+
/** Ключи-связи (объект или массив объектов — связанные сущности). */
|
|
20
|
+
export type CuboRelationKeys<R> = Exclude<keyof R, CuboScalarKeys<R>>;
|
|
21
|
+
/**
|
|
22
|
+
* Операторы для скалярного поля. Допускается как «короткая» форма (просто
|
|
23
|
+
* значение = eq), так и объект операторов. `not` — рекурсивный (`not: { in: [...] }`).
|
|
24
|
+
*/
|
|
25
|
+
export type CuboOp<V> = V | {
|
|
26
|
+
eq?: V;
|
|
27
|
+
ne?: V;
|
|
28
|
+
gt?: NonNullable<V>;
|
|
29
|
+
gte?: NonNullable<V>;
|
|
30
|
+
lt?: NonNullable<V>;
|
|
31
|
+
lte?: NonNullable<V>;
|
|
32
|
+
in?: NonNullable<V>[];
|
|
33
|
+
nin?: NonNullable<V>[];
|
|
34
|
+
like?: string;
|
|
35
|
+
ilike?: string;
|
|
36
|
+
between?: [NonNullable<V>, NonNullable<V>];
|
|
37
|
+
isNull?: boolean;
|
|
38
|
+
not?: CuboOp<V>;
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* Условие выборки: скалярные поля принимают операторы, поля-связи —
|
|
42
|
+
* вложенный `Where` по связанной сущности (типизированно и рекурсивно).
|
|
43
|
+
*/
|
|
44
|
+
export type CuboWhere<R> = {
|
|
45
|
+
[K in CuboScalarKeys<R>]?: CuboOp<R[K]>;
|
|
46
|
+
} & {
|
|
47
|
+
[K in CuboRelationKeys<R>]?: CuboWhere<CuboRelatedRow<R[K]>>;
|
|
48
|
+
};
|
|
49
|
+
/** Узел типизированного `with` — список ключей или дерево с вложенными `with`/`where`. */
|
|
50
|
+
export type CuboWithNode<R> = {
|
|
51
|
+
[K in CuboRelationKeys<R>]?: true | {
|
|
52
|
+
where?: CuboWhere<CuboRelatedRow<R[K]>>;
|
|
53
|
+
with?: CuboWith<CuboRelatedRow<R[K]>>;
|
|
54
|
+
};
|
|
55
|
+
};
|
|
56
|
+
/** Типизированный eager-load: массив ключей связей или дерево. */
|
|
57
|
+
export type CuboWith<R> = (CuboRelationKeys<R> & string)[] | CuboWithNode<R>;
|
|
58
|
+
/** Сортировка: объект `{ field: 'asc' | 'desc' }` или строка `-field`. */
|
|
59
|
+
export type CuboSort<R> = {
|
|
60
|
+
[K in CuboScalarKeys<R>]?: 'asc' | 'desc';
|
|
61
|
+
} | `${'-' | ''}${CuboScalarKeys<R> & string}`;
|
|
62
|
+
/** Полный типизированный запрос для сущности со строкой `R`. */
|
|
63
|
+
export type CuboQuery<R> = {
|
|
64
|
+
where?: CuboWhere<R>;
|
|
65
|
+
with?: CuboWith<R>;
|
|
66
|
+
sort?: CuboSort<R> | CuboSort<R>[];
|
|
67
|
+
limit?: number;
|
|
68
|
+
page?: number;
|
|
69
|
+
withDeleted?: boolean;
|
|
70
|
+
};
|
|
71
|
+
/** Плоский «строковый» запрос движка (то, что уходит в CuboCrudRequest.query). */
|
|
72
|
+
export type CuboCompiledQuery = Record<string, any> & {
|
|
73
|
+
with?: string;
|
|
74
|
+
sort?: string;
|
|
75
|
+
limit?: number;
|
|
76
|
+
page?: number;
|
|
77
|
+
};
|
|
78
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/query/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,kCAAkC;AAClC,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,GAAG,IAAI,CAAA;AAEhE,6EAA6E;AAC7E,MAAM,MAAM,cAAc,CAAC,CAAC,IAAI,WAAW,CAAC,CAAC,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,EAAE,GAAG,CAAC,GAAG,WAAW,CAAC,CAAC,CAAC,CAAA;AAEvF,uCAAuC;AACvC,MAAM,MAAM,cAAc,CAAC,CAAC,IAAI;KAC7B,CAAC,IAAI,MAAM,CAAC,CAAC,CAAC,GAAG,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,UAAU,GAAG,CAAC,GAAG,KAAK;CACnE,CAAC,MAAM,CAAC,CAAC,CAAA;AAEV,qEAAqE;AACrE,MAAM,MAAM,gBAAgB,CAAC,CAAC,IAAI,OAAO,CAAC,MAAM,CAAC,EAAE,cAAc,CAAC,CAAC,CAAC,CAAC,CAAA;AAErE;;;GAGG;AACH,MAAM,MAAM,MAAM,CAAC,CAAC,IAChB,CAAC,GACD;IACE,EAAE,CAAC,EAAE,CAAC,CAAA;IACN,EAAE,CAAC,EAAE,CAAC,CAAA;IACN,EAAE,CAAC,EAAE,WAAW,CAAC,CAAC,CAAC,CAAA;IACnB,GAAG,CAAC,EAAE,WAAW,CAAC,CAAC,CAAC,CAAA;IACpB,EAAE,CAAC,EAAE,WAAW,CAAC,CAAC,CAAC,CAAA;IACnB,GAAG,CAAC,EAAE,WAAW,CAAC,CAAC,CAAC,CAAA;IACpB,EAAE,CAAC,EAAE,WAAW,CAAC,CAAC,CAAC,EAAE,CAAA;IACrB,GAAG,CAAC,EAAE,WAAW,CAAC,CAAC,CAAC,EAAE,CAAA;IACtB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,OAAO,CAAC,EAAE,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,WAAW,CAAC,CAAC,CAAC,CAAC,CAAA;IAC1C,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,GAAG,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC,CAAA;CAChB,CAAA;AAEL;;;GAGG;AACH,MAAM,MAAM,SAAS,CAAC,CAAC,IAAI;KACxB,CAAC,IAAI,cAAc,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;CACxC,GAAG;KACD,CAAC,IAAI,gBAAgB,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,SAAS,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;CAC7D,CAAA;AAED,0FAA0F;AAC1F,MAAM,MAAM,YAAY,CAAC,CAAC,IAAI;KAC3B,CAAC,IAAI,gBAAgB,CAAC,CAAC,CAAC,CAAC,CAAC,EACvB,IAAI,GACJ;QACE,KAAK,CAAC,EAAE,SAAS,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;QACvC,IAAI,CAAC,EAAE,QAAQ,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;KACtC;CACN,CAAA;AAED,kEAAkE;AAClE,MAAM,MAAM,QAAQ,CAAC,CAAC,IAAI,CAAC,gBAAgB,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,EAAE,GAAG,YAAY,CAAC,CAAC,CAAC,CAAA;AAE5E,0EAA0E;AAC1E,MAAM,MAAM,QAAQ,CAAC,CAAC,IAClB;KAAG,CAAC,IAAI,cAAc,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,KAAK,GAAG,MAAM;CAAE,GAC7C,GAAG,GAAG,GAAG,EAAE,GAAG,cAAc,CAAC,CAAC,CAAC,GAAG,MAAM,EAAE,CAAA;AAE9C,gEAAgE;AAChE,MAAM,MAAM,SAAS,CAAC,CAAC,IAAI;IACzB,KAAK,CAAC,EAAE,SAAS,CAAC,CAAC,CAAC,CAAA;IACpB,IAAI,CAAC,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAA;IAClB,IAAI,CAAC,EAAE,QAAQ,CAAC,CAAC,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAA;IAClC,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,WAAW,CAAC,EAAE,OAAO,CAAA;CACtB,CAAA;AAED,kFAAkF;AAClF,MAAM,MAAM,iBAAiB,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG;IACpD,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,IAAI,CAAC,EAAE,MAAM,CAAA;CACd,CAAA"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { type Transaction } from '@cuboapp/database';
|
|
2
|
+
import { CuboEntity, CuboEntityField } from '@cuboapp/types';
|
|
3
|
+
import { CUBO_CRUD_ACTION } from '../constants';
|
|
4
|
+
export type CuboCrudAction = (typeof CUBO_CRUD_ACTION)[number];
|
|
5
|
+
export type CuboCrudWith = {
|
|
6
|
+
entity: CuboEntity | {
|
|
7
|
+
id: undefined;
|
|
8
|
+
alias: string;
|
|
9
|
+
fields: CuboEntityField[];
|
|
10
|
+
};
|
|
11
|
+
key: string;
|
|
12
|
+
full_key: string;
|
|
13
|
+
table_name: string;
|
|
14
|
+
};
|
|
15
|
+
export type CuboCrudQueryDefaults = {
|
|
16
|
+
query?: string;
|
|
17
|
+
sort?: string;
|
|
18
|
+
limit?: number;
|
|
19
|
+
page?: number;
|
|
20
|
+
};
|
|
21
|
+
export type CuboCrudRequest = {
|
|
22
|
+
headers?: Record<string, any>;
|
|
23
|
+
query?: Record<string, any>;
|
|
24
|
+
params?: Record<string, any>;
|
|
25
|
+
body?: any;
|
|
26
|
+
};
|
|
27
|
+
export type CuboCrudOptions = {
|
|
28
|
+
performer_id?: string;
|
|
29
|
+
transaction?: Transaction;
|
|
30
|
+
};
|
|
31
|
+
export type CuboCrudDto = {
|
|
32
|
+
entity: CuboEntity;
|
|
33
|
+
request: CuboCrudRequest;
|
|
34
|
+
options: CuboCrudOptions;
|
|
35
|
+
state: any;
|
|
36
|
+
};
|
|
37
|
+
export type CuboCrudFindQuery = {
|
|
38
|
+
selects: string[];
|
|
39
|
+
conditions: string[];
|
|
40
|
+
havings: string[];
|
|
41
|
+
replacements: Record<string, any>;
|
|
42
|
+
sorts: string[];
|
|
43
|
+
groupBy: string[];
|
|
44
|
+
offset: number;
|
|
45
|
+
limit: number;
|
|
46
|
+
joins: string[];
|
|
47
|
+
withes: CuboCrudWith[];
|
|
48
|
+
withDeleted?: boolean;
|
|
49
|
+
withCrdt?: boolean;
|
|
50
|
+
is_count: boolean;
|
|
51
|
+
sql: string;
|
|
52
|
+
};
|
|
53
|
+
export type CuboCrudGetManyResponse<T, E extends object = object> = {
|
|
54
|
+
rows: T[];
|
|
55
|
+
totals: {
|
|
56
|
+
count: number;
|
|
57
|
+
};
|
|
58
|
+
extra?: E;
|
|
59
|
+
};
|
|
60
|
+
//# sourceMappingURL=basic.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"basic.d.ts","sourceRoot":"","sources":["../../src/types/basic.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,WAAW,EAAE,MAAM,mBAAmB,CAAA;AACpD,OAAO,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAA;AAE5D,OAAO,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAA;AAE/C,MAAM,MAAM,cAAc,GAAG,CAAC,OAAO,gBAAgB,CAAC,CAAC,MAAM,CAAC,CAAA;AAE9D,MAAM,MAAM,YAAY,GAAG;IACzB,MAAM,EAAE,UAAU,GAAG;QAAE,EAAE,EAAE,SAAS,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,eAAe,EAAE,CAAA;KAAE,CAAA;IAChF,GAAG,EAAE,MAAM,CAAA;IACX,QAAQ,EAAE,MAAM,CAAA;IAChB,UAAU,EAAE,MAAM,CAAA;CACnB,CAAA;AAED,MAAM,MAAM,qBAAqB,GAAG;IAClC,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,IAAI,CAAC,EAAE,MAAM,CAAA;CACd,CAAA;AAED,MAAM,MAAM,eAAe,GAAG;IAC5B,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IAC7B,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IAC3B,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IAC5B,IAAI,CAAC,EAAE,GAAG,CAAA;CACX,CAAA;AAED,MAAM,MAAM,eAAe,GAAG;IAC5B,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,WAAW,CAAC,EAAE,WAAW,CAAA;CAC1B,CAAA;AAED,MAAM,MAAM,WAAW,GAAG;IACxB,MAAM,EAAE,UAAU,CAAA;IAClB,OAAO,EAAE,eAAe,CAAA;IACxB,OAAO,EAAE,eAAe,CAAA;IACxB,KAAK,EAAE,GAAG,CAAA;CACX,CAAA;AAED,MAAM,MAAM,iBAAiB,GAAG;IAC9B,OAAO,EAAE,MAAM,EAAE,CAAA;IACjB,UAAU,EAAE,MAAM,EAAE,CAAA;IACpB,OAAO,EAAE,MAAM,EAAE,CAAA;IACjB,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IACjC,KAAK,EAAE,MAAM,EAAE,CAAA;IACf,OAAO,EAAE,MAAM,EAAE,CAAA;IACjB,MAAM,EAAE,MAAM,CAAA;IACd,KAAK,EAAE,MAAM,CAAA;IAEb,KAAK,EAAE,MAAM,EAAE,CAAA;IACf,MAAM,EAAE,YAAY,EAAE,CAAA;IAEtB,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,QAAQ,EAAE,OAAO,CAAA;IACjB,GAAG,EAAE,MAAM,CAAA;CACZ,CAAA;AAED,MAAM,MAAM,uBAAuB,CAAC,CAAC,EAAE,CAAC,SAAS,MAAM,GAAG,MAAM,IAAI;IAClE,IAAI,EAAE,CAAC,EAAE,CAAA;IACT,MAAM,EAAE;QACN,KAAK,EAAE,MAAM,CAAA;KACd,CAAA;IACD,KAAK,CAAC,EAAE,CAAC,CAAA;CACV,CAAA"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { CuboCrdtServerDocumentOrigin } from '@cuboapp/crdt';
|
|
2
|
+
import { type Transaction } from '@cuboapp/database';
|
|
3
|
+
import { CuboHookName } from '../hooks/constants';
|
|
4
|
+
/**
|
|
5
|
+
* Опции запроса. Формируются заново на каждый CRUD-вызов и иммутабельно
|
|
6
|
+
* мёржатся с патчами `before*`-хуков (см. hooks/types). `raw` — escape-hatch
|
|
7
|
+
* для произвольных условий/реплейсментов, которые нельзя выразить типизированным
|
|
8
|
+
* query-DSL.
|
|
9
|
+
*/
|
|
10
|
+
export type CuboCrudQueryOptions = {
|
|
11
|
+
selects: string[];
|
|
12
|
+
conditions: string[];
|
|
13
|
+
havings: string[];
|
|
14
|
+
replacements: Record<string, any>;
|
|
15
|
+
joins: string[];
|
|
16
|
+
withes: string[];
|
|
17
|
+
sorts: string[];
|
|
18
|
+
groupBy: string[];
|
|
19
|
+
withDeleted?: boolean;
|
|
20
|
+
withCrdt?: boolean;
|
|
21
|
+
/** Произвольные сырые условия и реплейсменты (escape-hatch для query-DSL). */
|
|
22
|
+
raw?: {
|
|
23
|
+
conditions?: string[];
|
|
24
|
+
havings?: string[];
|
|
25
|
+
replacements?: Record<string, any>;
|
|
26
|
+
};
|
|
27
|
+
extra?: any;
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* Вход для query-опций: всё опционально. Публичный API (CuboCrudMethodOptions,
|
|
31
|
+
* before*-хуки) принимает частичный объект, который доводится до полного
|
|
32
|
+
* `CuboCrudQueryOptions` через {@link normalizeCuboCrudQueryOptions}.
|
|
33
|
+
*/
|
|
34
|
+
export type CuboCrudQueryOptionsInput = Partial<CuboCrudQueryOptions>;
|
|
35
|
+
/** Дефолты query-опций — единственный источник правды для пустых значений. */
|
|
36
|
+
export declare const createDefaultCuboCrudQueryOptions: () => CuboCrudQueryOptions;
|
|
37
|
+
/**
|
|
38
|
+
* Приводит частичный вход к полному `CuboCrudQueryOptions`: подставляет дефолты
|
|
39
|
+
* для отсутствующих ключей и делает глубокую копию (чтобы before*-хуки могли
|
|
40
|
+
* безопасно мутировать рабочую копию, не затрагивая переданный объект). Убирает
|
|
41
|
+
* необходимость в ручном `queryOptions?.selects || []` на каждом call-site.
|
|
42
|
+
*/
|
|
43
|
+
export declare const normalizeCuboCrudQueryOptions: (input?: CuboCrudQueryOptionsInput | null) => CuboCrudQueryOptions;
|
|
44
|
+
export type CuboCrudMethodOptions<E extends object = {}> = {
|
|
45
|
+
transaction?: Transaction;
|
|
46
|
+
performer_id?: string;
|
|
47
|
+
auth?: any;
|
|
48
|
+
extra?: any;
|
|
49
|
+
debounce?: number;
|
|
50
|
+
/** Имена хуков, которые нужно пропустить (источник правды — CUBO_HOOKS). */
|
|
51
|
+
excludeHooks?: CuboHookName[];
|
|
52
|
+
log?: boolean;
|
|
53
|
+
replace?: boolean;
|
|
54
|
+
crdt?: CuboCrdtServerDocumentOrigin;
|
|
55
|
+
queryOptions?: CuboCrudQueryOptionsInput;
|
|
56
|
+
} & E;
|
|
57
|
+
export type CuboBackendApiDbConnectionType = 'read' | 'write';
|
|
58
|
+
//# sourceMappingURL=db.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"db.d.ts","sourceRoot":"","sources":["../../src/types/db.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,4BAA4B,EAAE,MAAM,eAAe,CAAA;AACjE,OAAO,EAAE,KAAK,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAGpD,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAA;AAEjD;;;;;GAKG;AACH,MAAM,MAAM,oBAAoB,GAAG;IACjC,OAAO,EAAE,MAAM,EAAE,CAAA;IACjB,UAAU,EAAE,MAAM,EAAE,CAAA;IACpB,OAAO,EAAE,MAAM,EAAE,CAAA;IACjB,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IACjC,KAAK,EAAE,MAAM,EAAE,CAAA;IACf,MAAM,EAAE,MAAM,EAAE,CAAA;IAChB,KAAK,EAAE,MAAM,EAAE,CAAA;IACf,OAAO,EAAE,MAAM,EAAE,CAAA;IACjB,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAElB,8EAA8E;IAC9E,GAAG,CAAC,EAAE;QACJ,UAAU,CAAC,EAAE,MAAM,EAAE,CAAA;QACrB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAA;QAClB,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;KACnC,CAAA;IAED,KAAK,CAAC,EAAE,GAAG,CAAA;CACZ,CAAA;AAED;;;;GAIG;AACH,MAAM,MAAM,yBAAyB,GAAG,OAAO,CAAC,oBAAoB,CAAC,CAAA;AAErE,8EAA8E;AAC9E,eAAO,MAAM,iCAAiC,QAAO,oBAUnD,CAAA;AAEF;;;;;GAKG;AACH,eAAO,MAAM,6BAA6B,GAAI,QAAQ,yBAAyB,GAAG,IAAI,KAAG,oBAgBxF,CAAA;AAED,MAAM,MAAM,qBAAqB,CAAC,CAAC,SAAS,MAAM,GAAG,EAAE,IAAI;IACzD,WAAW,CAAC,EAAE,WAAW,CAAA;IACzB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,IAAI,CAAC,EAAE,GAAG,CAAA;IACV,KAAK,CAAC,EAAE,GAAG,CAAA;IACX,QAAQ,CAAC,EAAE,MAAM,CAAA;IAEjB,4EAA4E;IAC5E,YAAY,CAAC,EAAE,YAAY,EAAE,CAAA;IAE7B,GAAG,CAAC,EAAE,OAAO,CAAA;IACb,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,IAAI,CAAC,EAAE,4BAA4B,CAAA;IAEnC,YAAY,CAAC,EAAE,yBAAyB,CAAA;CACzC,GAAG,CAAC,CAAA;AAEL,MAAM,MAAM,8BAA8B,GAAG,MAAM,GAAG,OAAO,CAAA"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { Database, DatabaseOptions, Options } from '@cuboapp/database';
|
|
2
|
+
import { CuboApiEntitiesMap, CuboAuthTokenType, CuboEntity } from '@cuboapp/types';
|
|
3
|
+
import type { WsServer } from '@cuboapp/ws';
|
|
4
|
+
import { CuboAugmentationsStore } from '../hooks/types';
|
|
5
|
+
import { CuboCrudMethodOptions } from './db';
|
|
6
|
+
export * from './basic';
|
|
7
|
+
export * from './db';
|
|
8
|
+
export type CuboBackendApiOptions<T extends CuboApiEntitiesMap<T>, A> = {
|
|
9
|
+
api?: {
|
|
10
|
+
base_url?: string;
|
|
11
|
+
headers: Partial<Record<`cubo-${CuboAuthTokenType}`, string>> & Record<string, any>;
|
|
12
|
+
};
|
|
13
|
+
entities?: CuboEntity[];
|
|
14
|
+
db?: {
|
|
15
|
+
connection?: Database;
|
|
16
|
+
options?: Options & DatabaseOptions;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Встроенная поддержка cubo-crdt «из коробки». Если передан ws-сервер —
|
|
20
|
+
* сущности с флагом `entity.crdt === true` автоматически получают CRDT-доки,
|
|
21
|
+
* подписки и проброс create/update/delete без ручной обвязки.
|
|
22
|
+
*/
|
|
23
|
+
crdt?: {
|
|
24
|
+
ws: WsServer;
|
|
25
|
+
debug?: boolean;
|
|
26
|
+
};
|
|
27
|
+
hooks?: {
|
|
28
|
+
onAfterCreate?: <K extends Extract<keyof T, string>>(entity: CuboEntity, row: T[K], opts: CuboCrudMethodOptions) => void | Promise<void>;
|
|
29
|
+
onAfterUpdate?: <K extends Extract<keyof T, string>>(entity: CuboEntity, row: T[K], oldRow: T[K], opts: CuboCrudMethodOptions) => void | Promise<void>;
|
|
30
|
+
onAfterDelete?: <K extends Extract<keyof T, string>>(entity: CuboEntity, row: T[K], opts: CuboCrudMethodOptions) => void | Promise<void>;
|
|
31
|
+
};
|
|
32
|
+
augmentations?: Partial<CuboAugmentationsStore<T, A>>;
|
|
33
|
+
};
|
|
34
|
+
export type CuboBackendApiAuth = {
|
|
35
|
+
variables: Record<string, any>;
|
|
36
|
+
};
|
|
37
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,eAAe,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAA;AACtE,OAAO,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAA;AAClF,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAA;AAE3C,OAAO,EAAE,sBAAsB,EAAE,MAAM,gBAAgB,CAAA;AAEvD,OAAO,EAAE,qBAAqB,EAAE,MAAM,MAAM,CAAA;AAE5C,cAAc,SAAS,CAAA;AACvB,cAAc,MAAM,CAAA;AAEpB,MAAM,MAAM,qBAAqB,CAAC,CAAC,SAAS,kBAAkB,CAAC,CAAC,CAAC,EAAE,CAAC,IAAI;IACtE,GAAG,CAAC,EAAE;QACJ,QAAQ,CAAC,EAAE,MAAM,CAAA;QACjB,OAAO,EAAE,OAAO,CAAC,MAAM,CAAC,QAAQ,iBAAiB,EAAE,EAAE,MAAM,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;KACpF,CAAA;IAED,QAAQ,CAAC,EAAE,UAAU,EAAE,CAAA;IAEvB,EAAE,CAAC,EAAE;QACH,UAAU,CAAC,EAAE,QAAQ,CAAA;QACrB,OAAO,CAAC,EAAE,OAAO,GAAG,eAAe,CAAA;KACpC,CAAA;IAED;;;;OAIG;IACH,IAAI,CAAC,EAAE;QACL,EAAE,EAAE,QAAQ,CAAA;QACZ,KAAK,CAAC,EAAE,OAAO,CAAA;KAChB,CAAA;IAED,KAAK,CAAC,EAAE;QACN,aAAa,CAAC,EAAE,CAAC,CAAC,SAAS,OAAO,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,EAAE,MAAM,EAAE,UAAU,EAAE,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,qBAAqB,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;QACxI,aAAa,CAAC,EAAE,CAAC,CAAC,SAAS,OAAO,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,EACjD,MAAM,EAAE,UAAU,EAClB,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC,EACT,MAAM,EAAE,CAAC,CAAC,CAAC,CAAC,EACZ,IAAI,EAAE,qBAAqB,KACxB,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;QACzB,aAAa,CAAC,EAAE,CAAC,CAAC,SAAS,OAAO,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,EAAE,MAAM,EAAE,UAAU,EAAE,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,qBAAqB,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;KACzI,CAAA;IAED,aAAa,CAAC,EAAE,OAAO,CAAC,sBAAsB,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAA;CACtD,CAAA;AAED,MAAM,MAAM,kBAAkB,GAAG;IAC/B,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;CAC/B,CAAA"}
|
package/package.json
CHANGED
|
@@ -1,23 +1,37 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cuboapp/api-backend",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "
|
|
5
|
-
"main": "src/index.ts",
|
|
6
|
-
"repository": "git@github.com:cuboapp/api-backend.git",
|
|
7
|
-
"author": "CuboSoft",
|
|
3
|
+
"version": "4.0.0",
|
|
4
|
+
"description": "Metadata-driven CRUD engine (vendored — README §5; patched for uuid pk_type:'string').",
|
|
8
5
|
"license": "MIT",
|
|
9
6
|
"type": "module",
|
|
7
|
+
"main": "src/index.ts",
|
|
8
|
+
"types": "dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"default": "./src/index.ts"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"src"
|
|
18
|
+
],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "tsc -p tsconfig.json",
|
|
21
|
+
"typecheck": "echo \"vendored @cuboapp — typecheck via emitted d.ts\"",
|
|
22
|
+
"test": "vitest run",
|
|
23
|
+
"lint": "echo \"no lint\""
|
|
24
|
+
},
|
|
10
25
|
"dependencies": {
|
|
11
|
-
"@cuboapp/constants": "
|
|
12
|
-
"@cuboapp/database": "
|
|
13
|
-
"@cuboapp/types": "
|
|
14
|
-
"@cuboapp/utils": "1.0.12"
|
|
26
|
+
"@cuboapp/constants": "^3.0.0",
|
|
27
|
+
"@cuboapp/database": "^2.0.0",
|
|
28
|
+
"@cuboapp/types": "^3.0.0",
|
|
29
|
+
"@cuboapp/utils": "^1.0.12"
|
|
15
30
|
},
|
|
16
31
|
"peerDependencies": {
|
|
17
|
-
"@cuboapp/crdt": "
|
|
18
|
-
"@cuboapp/ws": "
|
|
19
|
-
"
|
|
20
|
-
"pg": "8.22.0",
|
|
32
|
+
"@cuboapp/crdt": "^2.0.0",
|
|
33
|
+
"@cuboapp/ws": "^2.0.0",
|
|
34
|
+
"pg": "^8.13.1",
|
|
21
35
|
"yjs": "^13.6.31"
|
|
22
36
|
},
|
|
23
37
|
"peerDependenciesMeta": {
|
|
@@ -27,9 +41,6 @@
|
|
|
27
41
|
"@cuboapp/ws": {
|
|
28
42
|
"optional": true
|
|
29
43
|
},
|
|
30
|
-
"mysql2": {
|
|
31
|
-
"optional": true
|
|
32
|
-
},
|
|
33
44
|
"pg": {
|
|
34
45
|
"optional": true
|
|
35
46
|
},
|
|
@@ -38,10 +49,9 @@
|
|
|
38
49
|
}
|
|
39
50
|
},
|
|
40
51
|
"devDependencies": {
|
|
41
|
-
"@cuboapp/crdt": "
|
|
42
|
-
"@cuboapp/ws": "
|
|
43
|
-
"@types/node": "^
|
|
44
|
-
"
|
|
45
|
-
"typescript": "^6.0.3"
|
|
52
|
+
"@cuboapp/crdt": "^2.0.0",
|
|
53
|
+
"@cuboapp/ws": "^2.0.0",
|
|
54
|
+
"@types/node": "^22.10.5",
|
|
55
|
+
"typescript": "^5.8.3"
|
|
46
56
|
}
|
|
47
57
|
}
|
package/src/constants/index.ts
CHANGED
|
@@ -1,10 +1,26 @@
|
|
|
1
1
|
import { CUBO_ENTITY_FIELD_TYPE } from '@cuboapp/constants'
|
|
2
2
|
import { CuboEntityField } from '@cuboapp/types'
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
// `|` is part of a key, not noise: a key may name SEVERAL fields to be OR-ed (see
|
|
5
|
+
// CUBO_CRUD_QUERY_SYMBOL_OR). Stripping it here would silently glue two field names into one.
|
|
6
|
+
export const CUBO_CRUD_QUERY_KEY_REGEX = /[^a-zA-Z\.\_\|]/g
|
|
5
7
|
|
|
6
8
|
export const CUBO_CRUD_QUERY_SYMBOL_NOT = 'not'
|
|
7
9
|
|
|
10
|
+
/**
|
|
11
|
+
* Separates the field names in an ANY-OF query key: `?a|b=<value>` means "`a` matches OR `b` matches".
|
|
12
|
+
*
|
|
13
|
+
* Every other condition in a query is AND-ed, which is right for filtering — each parameter narrows
|
|
14
|
+
* the result. But some questions are genuinely about a row's relationship to a value regardless of
|
|
15
|
+
* WHICH column records it: a document involving an organization has it as either the sender or the
|
|
16
|
+
* receiver, and asking for both as separate parameters would return the documents where it is both.
|
|
17
|
+
*
|
|
18
|
+
* One condition, several columns — not a general boolean expression language. That keeps the query
|
|
19
|
+
* string readable and keeps the SQL to a single parenthesised OR, so it still AND-s with everything
|
|
20
|
+
* else exactly as a plain key does.
|
|
21
|
+
*/
|
|
22
|
+
export const CUBO_CRUD_QUERY_SYMBOL_OR = '|'
|
|
23
|
+
|
|
8
24
|
export const CUBO_CRUD_DEFAULT_PAGE_LIMIT = 25
|
|
9
25
|
export const CUBO_CRUD_MAX_PAGE_LIMIT = 500
|
|
10
26
|
|
|
@@ -26,11 +42,11 @@ export const CUBO_CRUD_QUERY_CONDITION = {
|
|
|
26
42
|
|
|
27
43
|
export const CUBO_CRUD_DEFAULT_FIELDS: Pick<CuboEntityField, 'type' | 'alias' | 'extra'>[] = [
|
|
28
44
|
{
|
|
29
|
-
|
|
45
|
+
// PATCH (uuid pk_type:'string'): the platform PK is uuid, so the default `id`
|
|
46
|
+
// field is TEXT — otherwise filter/getOne by uuid fails the NUMBER allow-list check.
|
|
47
|
+
type: CUBO_ENTITY_FIELD_TYPE.TEXT,
|
|
30
48
|
alias: 'id',
|
|
31
|
-
extra: {
|
|
32
|
-
number_subtype: 'int'
|
|
33
|
-
}
|
|
49
|
+
extra: {}
|
|
34
50
|
},
|
|
35
51
|
{
|
|
36
52
|
type: CUBO_ENTITY_FIELD_TYPE.TIMESTAMP,
|
|
@@ -38,12 +54,12 @@ export const CUBO_CRUD_DEFAULT_FIELDS: Pick<CuboEntityField, 'type' | 'alias' |
|
|
|
38
54
|
extra: {}
|
|
39
55
|
},
|
|
40
56
|
{
|
|
41
|
-
|
|
57
|
+
// PATCH (uuid): user audit columns are uuid TEXT, not numeric SELECT — otherwise
|
|
58
|
+
// convert.prepareFieldValue parseInt()s the uuid (README PK rule). `with`-join logic
|
|
59
|
+
// still resolves the user relation from the raw column value.
|
|
60
|
+
type: CUBO_ENTITY_FIELD_TYPE.TEXT,
|
|
42
61
|
alias: 'created_by',
|
|
43
|
-
extra: {
|
|
44
|
-
select_subtype: 'user',
|
|
45
|
-
select_entity_key: 'created_by'
|
|
46
|
-
}
|
|
62
|
+
extra: { select_subtype: 'user', select_entity_key: 'created_by' }
|
|
47
63
|
},
|
|
48
64
|
{
|
|
49
65
|
type: CUBO_ENTITY_FIELD_TYPE.TIMESTAMP,
|
|
@@ -51,12 +67,9 @@ export const CUBO_CRUD_DEFAULT_FIELDS: Pick<CuboEntityField, 'type' | 'alias' |
|
|
|
51
67
|
extra: {}
|
|
52
68
|
},
|
|
53
69
|
{
|
|
54
|
-
type: CUBO_ENTITY_FIELD_TYPE.
|
|
70
|
+
type: CUBO_ENTITY_FIELD_TYPE.TEXT,
|
|
55
71
|
alias: 'modified_by',
|
|
56
|
-
extra: {
|
|
57
|
-
select_subtype: 'user',
|
|
58
|
-
select_entity_key: 'modified_by'
|
|
59
|
-
}
|
|
72
|
+
extra: { select_subtype: 'user', select_entity_key: 'modified_by' }
|
|
60
73
|
},
|
|
61
74
|
{
|
|
62
75
|
type: CUBO_ENTITY_FIELD_TYPE.TIMESTAMP,
|
|
@@ -64,12 +77,9 @@ export const CUBO_CRUD_DEFAULT_FIELDS: Pick<CuboEntityField, 'type' | 'alias' |
|
|
|
64
77
|
extra: {}
|
|
65
78
|
},
|
|
66
79
|
{
|
|
67
|
-
type: CUBO_ENTITY_FIELD_TYPE.
|
|
80
|
+
type: CUBO_ENTITY_FIELD_TYPE.TEXT,
|
|
68
81
|
alias: 'deleted_by',
|
|
69
|
-
extra: {
|
|
70
|
-
select_subtype: 'user',
|
|
71
|
-
select_entity_key: 'modified_by'
|
|
72
|
-
}
|
|
82
|
+
extra: { select_subtype: 'user', select_entity_key: 'deleted_by' }
|
|
73
83
|
}
|
|
74
84
|
]
|
|
75
85
|
|
package/src/core/index.ts
CHANGED
|
@@ -260,7 +260,7 @@ export class CuboBackendApi<T extends CuboApiEntitiesMap<T>, A extends unknown =
|
|
|
260
260
|
if (result) queryOptions = normalizeCuboCrudQueryOptions(result)
|
|
261
261
|
}
|
|
262
262
|
|
|
263
|
-
const dto = this.helpers.data.prepare('create', entity.fields, req.body, opts.performer_id
|
|
263
|
+
const dto = this.helpers.data.prepare('create', entity.fields, req.body, opts.performer_id)
|
|
264
264
|
if (!Object.keys(dto).length) {
|
|
265
265
|
throw { status: 400, text: 'No keys to create' }
|
|
266
266
|
}
|
|
@@ -357,7 +357,7 @@ export class CuboBackendApi<T extends CuboApiEntitiesMap<T>, A extends unknown =
|
|
|
357
357
|
}
|
|
358
358
|
}
|
|
359
359
|
|
|
360
|
-
const dto = this.helpers.data.prepare('update', entity.fields, req.body, opts.performer_id
|
|
360
|
+
const dto = this.helpers.data.prepare('update', entity.fields, req.body, opts.performer_id)
|
|
361
361
|
if (!Object.keys(dto).length) {
|
|
362
362
|
throw { status: 400, text: 'No keys to update' }
|
|
363
363
|
}
|
|
@@ -469,7 +469,7 @@ export class CuboBackendApi<T extends CuboApiEntitiesMap<T>, A extends unknown =
|
|
|
469
469
|
if (!rows.length) return []
|
|
470
470
|
|
|
471
471
|
const entity = await this.getEntity(entityAlias)
|
|
472
|
-
const dtos = rows.map((row) => this.helpers.data.prepare('create', entity.fields, row, opts!.performer_id
|
|
472
|
+
const dtos = rows.map((row) => this.helpers.data.prepare('create', entity.fields, row, opts!.performer_id))
|
|
473
473
|
|
|
474
474
|
const run = async (tx: any): Promise<T[K][]> => {
|
|
475
475
|
const ids = await this.insertMany(entity, dtos, { transaction: tx, log: opts!.log })
|
|
@@ -507,7 +507,7 @@ export class CuboBackendApi<T extends CuboApiEntitiesMap<T>, A extends unknown =
|
|
|
507
507
|
const ids = items.map((r: any) => r.id)
|
|
508
508
|
if (!ids.length) return []
|
|
509
509
|
|
|
510
|
-
const dto = this.helpers.data.prepare('update', entity.fields, req.body, opts.performer_id
|
|
510
|
+
const dto = this.helpers.data.prepare('update', entity.fields, req.body, opts.performer_id)
|
|
511
511
|
if (!Object.keys(dto).length) {
|
|
512
512
|
throw { status: 400, text: 'No keys to update' }
|
|
513
513
|
}
|
|
@@ -734,7 +734,8 @@ export class CuboBackendApi<T extends CuboApiEntitiesMap<T>, A extends unknown =
|
|
|
734
734
|
const res: any = await db.connection.query(sql, { type: QueryTypes.INSERT, replacements, transaction: opts.transaction })
|
|
735
735
|
|
|
736
736
|
if (dialect.supportsReturning) {
|
|
737
|
-
|
|
737
|
+
// PATCH (uuid pk_type:'string'): return the raw pk, do NOT coerce to number (+uuid = NaN).
|
|
738
|
+
return (res?.[0] || []).map((r: any) => (this.pkType === 'number' ? +r[this.pkKey] : r[this.pkKey]))
|
|
738
739
|
}
|
|
739
740
|
|
|
740
741
|
// MySQL: query() возвращает [insertId, affectedRows]; строки получают последовательные id
|
package/src/crdt/index.ts
CHANGED
|
@@ -7,11 +7,24 @@ import type {
|
|
|
7
7
|
import { CuboApiEntitiesMap, CuboEntity } from '@cuboapp/types'
|
|
8
8
|
import type { WsServer, WsServerSocket } from '@cuboapp/ws'
|
|
9
9
|
|
|
10
|
+
import { CUBO_CRUD_MAX_PAGE_LIMIT } from '../constants'
|
|
10
11
|
import type { CuboBackendApi } from '../core'
|
|
11
12
|
|
|
12
13
|
/** Тип фабрики, импортируемый только как тип (рантайм-зависимость опциональна). */
|
|
13
14
|
type CreateCrdtServer = typeof import('@cuboapp/crdt')['createCrdtServer']
|
|
14
15
|
|
|
16
|
+
/**
|
|
17
|
+
* Engine rows carry `Date` objects for timestamptz columns; Yjs can't encode a Date (it round-trips
|
|
18
|
+
* as `{}`), so coerce top-level Dates to ISO strings before they enter a CRDT document — they then
|
|
19
|
+
* survive sync identically to the REST/JSON path. (Patched alongside the uuid-pk fixes.)
|
|
20
|
+
*/
|
|
21
|
+
function normalizeCrdtRow<R>(row: R): R {
|
|
22
|
+
if (!row || typeof row !== 'object') return row
|
|
23
|
+
const out: Record<string, unknown> = { ...(row as Record<string, unknown>) }
|
|
24
|
+
for (const key of Object.keys(out)) if (out[key] instanceof Date) out[key] = (out[key] as Date).toISOString()
|
|
25
|
+
return out as R
|
|
26
|
+
}
|
|
27
|
+
|
|
15
28
|
/**
|
|
16
29
|
* Встроенный адаптер cubo-crdt. Инкапсулирует всю обвязку, которую раньше
|
|
17
30
|
* приходилось писать в каждом приложении руками (old.ts):
|
|
@@ -28,7 +41,7 @@ export class CuboCrdtAdapter<T extends CuboApiEntitiesMap<T>, A> {
|
|
|
28
41
|
|
|
29
42
|
constructor(
|
|
30
43
|
private api: CuboBackendApi<T, A>,
|
|
31
|
-
ws: WsServer,
|
|
44
|
+
private ws: WsServer,
|
|
32
45
|
createCrdtServer: CreateCrdtServer,
|
|
33
46
|
private debug = false
|
|
34
47
|
) {
|
|
@@ -39,16 +52,34 @@ export class CuboCrdtAdapter<T extends CuboApiEntitiesMap<T>, A> {
|
|
|
39
52
|
entities,
|
|
40
53
|
debug: this.debug,
|
|
41
54
|
|
|
42
|
-
// подписка -> строки из БД (хуки CRUD пропускаем, чтобы не зациклить)
|
|
55
|
+
// подписка -> строки из БД (хуки CRUD пропускаем, чтобы не зациклить).
|
|
56
|
+
// A CRDT subscription must hold the FULL live list, but getMany defaults to
|
|
57
|
+
// CUBO_CRUD_DEFAULT_PAGE_LIMIT (25) and caps at CUBO_CRUD_MAX_PAGE_LIMIT (500) — so page through
|
|
58
|
+
// until every row is loaded. Without this the document only ever holds the first 25 rows and
|
|
59
|
+
// clients see a truncated set (e.g. an account with 200+ entities_fields shows a handful).
|
|
43
60
|
fetchRows: async (client, subscribe: CuboCrdtServerSubscribe) => {
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
61
|
+
const auth = (client as CuboCrdtSocketClient<A>).auth
|
|
62
|
+
const baseFilters = subscribe.filters || {}
|
|
63
|
+
const collected: T[Extract<keyof T, string>][] = []
|
|
64
|
+
// Upstream issues ONE getMany and returns its page. Kept paging: a subscription must hold the
|
|
65
|
+
// whole live list, and one call stops at CUBO_CRUD_DEFAULT_PAGE_LIMIT.
|
|
66
|
+
let lastTotals: Awaited<ReturnType<typeof this.api.getMany>>['totals'] | undefined
|
|
67
|
+
for (let page = 1; page <= 1000; page++) {
|
|
68
|
+
const { rows, totals } = await this.api.getMany(
|
|
69
|
+
subscribe.entity as Extract<keyof T, string>,
|
|
70
|
+
{ query: { ...baseFilters, limit: CUBO_CRUD_MAX_PAGE_LIMIT, page } },
|
|
71
|
+
{ auth, excludeHooks: ['afterCrdtCreate', 'afterCrdtUpdate'] }
|
|
72
|
+
)
|
|
73
|
+
lastTotals = totals
|
|
74
|
+
collected.push(...(rows as T[Extract<keyof T, string>][]))
|
|
75
|
+
if (rows.length < CUBO_CRUD_MAX_PAGE_LIMIT || collected.length >= (totals?.count ?? 0)) break
|
|
76
|
+
}
|
|
48
77
|
|
|
78
|
+
// `{ rows, totals }` is upstream's shape — `fetchRows` accepts either, and the totals let a
|
|
79
|
+
// paginated subscription report the true count rather than what this page happened to hold.
|
|
49
80
|
return {
|
|
50
|
-
rows:
|
|
51
|
-
totals
|
|
81
|
+
rows: collected.map(normalizeCrdtRow) as T[Extract<keyof T, string>][],
|
|
82
|
+
totals: lastTotals
|
|
52
83
|
}
|
|
53
84
|
},
|
|
54
85
|
|
|
@@ -75,13 +106,27 @@ export class CuboCrdtAdapter<T extends CuboApiEntitiesMap<T>, A> {
|
|
|
75
106
|
checkRowIsSutable: (base, { entity, subscribe, row }) => {
|
|
76
107
|
const augmentation = this.api.options.augmentations?.[entity as Extract<keyof T, string>]
|
|
77
108
|
if (augmentation?.filterRowForCrdtEvent) {
|
|
78
|
-
return augmentation.filterRowForCrdtEvent(row, subscribe.filters || {})
|
|
109
|
+
return augmentation.filterRowForCrdtEvent(row, subscribe.filters || {}, this.authOf(subscribe.client_id))
|
|
79
110
|
}
|
|
80
111
|
return base
|
|
81
112
|
}
|
|
82
113
|
})
|
|
83
114
|
}
|
|
84
115
|
|
|
116
|
+
/**
|
|
117
|
+
* Авторизация клиента, владеющего подпиской.
|
|
118
|
+
*
|
|
119
|
+
* `checkRowIsSutable` получает только подписку (в ней есть client_id), а права живут на сокете —
|
|
120
|
+
* поэтому ищем сокет в реестре ws. Нужно для аугментаций, которые прячут строки по правам: гидрация
|
|
121
|
+
* подписки идёт через getMany с auth и такие строки не отдаст, а живой пуш без этого отдал бы.
|
|
122
|
+
*/
|
|
123
|
+
private authOf(clientId: string): A | undefined {
|
|
124
|
+
for (const client of this.ws.clients) {
|
|
125
|
+
if (client.id === clientId) return (client as WsServerSocket<{ auth?: A }>).auth
|
|
126
|
+
}
|
|
127
|
+
return undefined
|
|
128
|
+
}
|
|
129
|
+
|
|
85
130
|
public async init() {
|
|
86
131
|
await this.server.init()
|
|
87
132
|
}
|
|
@@ -108,17 +153,26 @@ export class CuboCrdtAdapter<T extends CuboApiEntitiesMap<T>, A> {
|
|
|
108
153
|
public pushCreate(entity: CuboEntity, row: any) {
|
|
109
154
|
if (!entity.crdt) return
|
|
110
155
|
const alias = entity.alias as Extract<keyof T, string>
|
|
111
|
-
|
|
112
|
-
|
|
156
|
+
// `normalizeCrdtRow` is ours and upstream still lacks it: a Date reaches Yjs as an object it
|
|
157
|
+
// cannot encode, and the column renders empty. Normalise once and use it for both the document
|
|
158
|
+
// and the mutation notice, so subscribers and refreshes agree on what the row says.
|
|
159
|
+
const normalized = normalizeCrdtRow(row)
|
|
160
|
+
this.server.ensureDocument(alias, normalized)
|
|
161
|
+
this.server.notifyMutation(alias, { action: 'create', row: normalized })
|
|
113
162
|
}
|
|
114
163
|
|
|
115
164
|
public pushUpdate(entity: CuboEntity, row: any, previousRow?: any, changedKeys?: string[]) {
|
|
116
165
|
if (!entity.crdt) return
|
|
117
166
|
const alias = entity.alias as Extract<keyof T, string>
|
|
118
|
-
|
|
167
|
+
// We used to reach past `ensureDocument` and write the row into the live Y.Map by hand, because
|
|
168
|
+
// it only ever wrote on FIRST creation and server-side edits therefore never reached subscribers.
|
|
169
|
+
// 1.0.27 does that itself — `ensureDocument` now calls `document.write()` when the document
|
|
170
|
+
// already exists — so the hand-rolled path is gone and only the normalisation is still ours.
|
|
171
|
+
const normalized = normalizeCrdtRow(row)
|
|
172
|
+
this.server.ensureDocument(alias, normalized)
|
|
119
173
|
this.server.notifyMutation(alias, {
|
|
120
174
|
action: 'update',
|
|
121
|
-
row,
|
|
175
|
+
row: normalized,
|
|
122
176
|
previousRow,
|
|
123
177
|
changedKeys
|
|
124
178
|
})
|
package/src/helpers/data.ts
CHANGED
|
@@ -2,8 +2,10 @@ import { CUBO_ENTITY_FIELD_TYPE } from '@cuboapp/constants'
|
|
|
2
2
|
import { CuboApiEntitiesMap, CuboEntityField } from '@cuboapp/types'
|
|
3
3
|
import { keyBy } from '@cuboapp/utils'
|
|
4
4
|
|
|
5
|
+
import { CUBO_CRUD_DEFAULT_FIELDS_ALIASES } from '../constants'
|
|
6
|
+
|
|
5
7
|
export class ApiHelpersData<T extends CuboApiEntitiesMap<T>> {
|
|
6
|
-
public prepare(type: 'create' | 'update' | 'delete', fields: CuboEntityField[], dto?: any, performer_id?:
|
|
8
|
+
public prepare(type: 'create' | 'update' | 'delete', fields: CuboEntityField[], dto?: any, performer_id?: string) {
|
|
7
9
|
const fieldsByAlias = keyBy(fields, (i) => i.alias)
|
|
8
10
|
|
|
9
11
|
const errors: string[] = []
|
|
@@ -33,7 +35,12 @@ export class ApiHelpersData<T extends CuboApiEntitiesMap<T>> {
|
|
|
33
35
|
|
|
34
36
|
for (const key in dto || {}) {
|
|
35
37
|
if (!fieldsByAlias[key]) {
|
|
36
|
-
|
|
38
|
+
// PATCH (README §7): default fields (id/created_*/deleted_* — e.g. deleted_at on
|
|
39
|
+
// restore) are handled above / stamped by the engine, so skip them silently.
|
|
40
|
+
// Any OTHER unknown field is REJECTED (the allow-list is the SQL-injection boundary).
|
|
41
|
+
if (!CUBO_CRUD_DEFAULT_FIELDS_ALIASES.includes(key)) {
|
|
42
|
+
errors.push(`"${key}": unknown field`)
|
|
43
|
+
}
|
|
37
44
|
} else {
|
|
38
45
|
const value = dto[key]
|
|
39
46
|
const tval = typeof value
|
|
@@ -137,6 +144,11 @@ export class ApiHelpersData<T extends CuboApiEntitiesMap<T>> {
|
|
|
137
144
|
}
|
|
138
145
|
}
|
|
139
146
|
|
|
147
|
+
// PATCH (README §7): reject on any validation error instead of silently dropping.
|
|
148
|
+
if (errors.length) {
|
|
149
|
+
throw { status: 400, text: `Validation failed: ${errors.join('; ')}` }
|
|
150
|
+
}
|
|
151
|
+
|
|
140
152
|
return prepared
|
|
141
153
|
}
|
|
142
154
|
}
|