@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.
Files changed (56) hide show
  1. package/dist/constants/index.d.ts +35 -0
  2. package/dist/constants/index.d.ts.map +1 -0
  3. package/dist/core/index.d.ts +77 -0
  4. package/dist/core/index.d.ts.map +1 -0
  5. package/dist/crdt/index.d.ts +45 -0
  6. package/dist/crdt/index.d.ts.map +1 -0
  7. package/dist/dialects/index.d.ts +21 -0
  8. package/dist/dialects/index.d.ts.map +1 -0
  9. package/dist/helpers/convert.d.ts +11 -0
  10. package/dist/helpers/convert.d.ts.map +1 -0
  11. package/dist/helpers/data.d.ts +5 -0
  12. package/dist/helpers/data.d.ts.map +1 -0
  13. package/dist/helpers/index.d.ts +42 -0
  14. package/dist/helpers/index.d.ts.map +1 -0
  15. package/dist/helpers/withes.d.ts +10 -0
  16. package/dist/helpers/withes.d.ts.map +1 -0
  17. package/dist/hooks/constants.d.ts +7 -0
  18. package/dist/hooks/constants.d.ts.map +1 -0
  19. package/dist/hooks/define.d.ts +63 -0
  20. package/dist/hooks/define.d.ts.map +1 -0
  21. package/dist/hooks/index.d.ts +5 -0
  22. package/dist/hooks/index.d.ts.map +1 -0
  23. package/dist/hooks/subscribe.d.ts +83 -0
  24. package/dist/hooks/subscribe.d.ts.map +1 -0
  25. package/dist/hooks/types.d.ts +60 -0
  26. package/dist/hooks/types.d.ts.map +1 -0
  27. package/dist/index.d.ts +8 -0
  28. package/dist/index.d.ts.map +1 -0
  29. package/dist/query/compile.d.ts +14 -0
  30. package/dist/query/compile.d.ts.map +1 -0
  31. package/dist/query/index.d.ts +3 -0
  32. package/dist/query/index.d.ts.map +1 -0
  33. package/dist/query/types.d.ts +78 -0
  34. package/dist/query/types.d.ts.map +1 -0
  35. package/dist/types/basic.d.ts +60 -0
  36. package/dist/types/basic.d.ts.map +1 -0
  37. package/dist/types/db.d.ts +58 -0
  38. package/dist/types/db.d.ts.map +1 -0
  39. package/dist/types/index.d.ts +37 -0
  40. package/dist/types/index.d.ts.map +1 -0
  41. package/package.json +31 -21
  42. package/src/constants/index.ts +30 -20
  43. package/src/core/index.ts +6 -5
  44. package/src/crdt/index.ts +67 -13
  45. package/src/helpers/data.ts +14 -2
  46. package/src/helpers/index.ts +101 -46
  47. package/src/helpers/withes.ts +1 -1
  48. package/src/hooks/index.ts +1 -0
  49. package/src/hooks/subscribe.ts +160 -0
  50. package/src/hooks/types.ts +6 -3
  51. package/src/types/basic.ts +2 -2
  52. package/src/types/db.ts +1 -1
  53. package/.prettierrc +0 -8
  54. package/tsconfig.build.json +0 -4
  55. package/tsconfig.json +0 -22
  56. 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": "3.0.16",
4
- "description": "Backend Api for CuboApp",
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": "2.0.8",
12
- "@cuboapp/database": "1.0.7",
13
- "@cuboapp/types": "2.0.14",
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": "1.0.27",
18
- "@cuboapp/ws": "1.0.10",
19
- "mysql2": "3.22.5",
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": "1.0.27",
42
- "@cuboapp/ws": "1.0.10",
43
- "@types/node": "^26.0.0",
44
- "pg": "^8.22.0",
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
  }
@@ -1,10 +1,26 @@
1
1
  import { CUBO_ENTITY_FIELD_TYPE } from '@cuboapp/constants'
2
2
  import { CuboEntityField } from '@cuboapp/types'
3
3
 
4
- export const CUBO_CRUD_QUERY_KEY_REGEX = /[^a-zA-Z\.\_]/g
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
- type: CUBO_ENTITY_FIELD_TYPE.NUMBER,
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
- type: CUBO_ENTITY_FIELD_TYPE.SELECT,
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.SELECT,
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.SELECT,
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 || 0)
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 || 0)
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 || 0))
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 || 0)
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
- return (res?.[0] || []).map((r: any) => +r[this.pkKey])
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 { rows, totals } = 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
- })
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: rows as T[Extract<keyof T, string>][],
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
- this.server.ensureDocument(alias, row)
112
- this.server.notifyMutation(alias, { action: 'create', row })
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
- this.server.ensureDocument(alias, row)
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
  })
@@ -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?: number) {
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
- errors.push(`"${key}": unknown field`)
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
  }