@cuboapp/api-backend 1.0.25 → 1.0.26

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/src/types/db.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { CuboCrdtServerDocumentOrigin } from '@cuboapp/crdt'
1
2
  import { type Transaction } from '@cuboapp/database'
2
3
 
3
4
  export type CuboCrudQueryOptions = {
@@ -11,7 +12,7 @@ export type CuboCrudQueryOptions = {
11
12
  groupBy?: string
12
13
  withDeleted?: boolean
13
14
  withCrdt?: boolean
14
-
15
+
15
16
  extra?: any
16
17
  }
17
18
 
@@ -19,6 +20,7 @@ export type CuboCrudMethodOptions<E extends object = {}> = {
19
20
  transaction?: Transaction
20
21
  performer_id?: number
21
22
  extra?: any
23
+ debounce?: number
22
24
  excludeHooks?: (
23
25
  | 'beforeGetMany'
24
26
  | 'beforeGetOne'
@@ -30,11 +32,12 @@ export type CuboCrudMethodOptions<E extends object = {}> = {
30
32
  | 'beforeUpdate'
31
33
  | 'afterCrdtCreate'
32
34
  | 'afterCrdtUpdate'
35
+ | 'baseHooks'
33
36
  )[]
34
37
 
35
38
  log?: boolean
36
39
  replace?: boolean
37
- crdt?: boolean
40
+ crdt?: CuboCrdtServerDocumentOrigin
38
41
 
39
42
  queryOptions?: CuboCrudQueryOptions
40
43
  } & E
@@ -1,8 +1,8 @@
1
- import { CuboCrdtOptions } from '@cuboapp/crdt'
2
1
  import { Database, DatabaseOptions, Options } from '@cuboapp/database'
3
2
  import { CuboApiEntitiesMap, CuboAuthTokenType, CuboEntity } from '@cuboapp/types'
4
3
 
5
4
  import { CuboCrudAugmentationsStore } from './augmentation'
5
+ import { CuboCrudMethodOptions } from './db'
6
6
 
7
7
  export * from './augmentation'
8
8
  export * from './basic'
@@ -21,7 +21,16 @@ export type CuboBackendApiOptions<T extends CuboApiEntitiesMap<T>, A> = {
21
21
  options?: Options & DatabaseOptions
22
22
  }
23
23
 
24
- crdt?: Omit<CuboCrdtOptions<A>, 'beforeLoadDocument'>
24
+ hooks?: {
25
+ onAfterCreate?: <A extends Extract<keyof T, string>>(entity: CuboEntity, row: T[A], opts: CuboCrudMethodOptions) => void | Promise<void>
26
+ onAfterUpdate?: <A extends Extract<keyof T, string>>(
27
+ entity: CuboEntity,
28
+ row: T[A],
29
+ oldRow: T[A],
30
+ opts: CuboCrudMethodOptions
31
+ ) => void | Promise<void>
32
+ onAfterDelete?: <A extends Extract<keyof T, string>>(entity: CuboEntity, row: T[A], opts: CuboCrudMethodOptions) => void | Promise<void>
33
+ }
25
34
 
26
35
  augmentations?: Partial<CuboCrudAugmentationsStore<T>>
27
36
  }
package/src/crdt/index.ts DELETED
@@ -1,413 +0,0 @@
1
- import { QueryTypes } from '@cuboapp/database'
2
- import { CuboApiEntitiesMap, CuboEntity } from '@cuboapp/types'
3
- import { Database } from '@hocuspocus/extension-database'
4
- import { Server } from '@hocuspocus/server'
5
- import { IncomingHttpHeaders } from 'http'
6
- import * as Y from 'yjs'
7
-
8
- import { CuboBackendApi } from '..'
9
- import { CuboDirectConnection } from './types'
10
-
11
- export * from './types'
12
-
13
- export * from '@hocuspocus/server'
14
-
15
- export class CuboCrdt<T extends CuboApiEntitiesMap<T>> {
16
- public server: Server
17
- private documentPrefix = 'cubo'
18
- private defaultCrdtFieldAlias = 'crdt_data'
19
-
20
- constructor(public api: CuboBackendApi<T>) {
21
- this.init()
22
- }
23
-
24
- private init() {
25
- const { onAuthenticate, onLoadDocument, onChange, onStoreDocument, afterLoadDocument, connected, ...config } = this.api.options.crdt
26
-
27
- this.server = new Server({
28
- port: config.port,
29
- extensions: [
30
- new Database({
31
- fetch: async ({ documentName, context, document, instance, requestHeaders, requestParameters, socketId, connectionConfig }) => {
32
- try {
33
- const [prefix] = documentName.split('.')
34
- if (prefix !== this.documentPrefix) {
35
- return null
36
- }
37
-
38
- const { entityAlias, id } = this.parseDocumentName(documentName)
39
-
40
- // console.log('fetch', entityAlias, id)
41
- if (id <= 0) {
42
- return null
43
- }
44
-
45
- const entity = await this.api.helpers.getEntityByAlias(entityAlias as keyof T)
46
- if (!entity) {
47
- throw new Error(`Entity not found: "${entityAlias}"`)
48
- }
49
-
50
- const isSupported = this.isCrdtSupported(entity)
51
- if (!isSupported) {
52
- throw new Error(`Entity "${entityAlias}" does not support CRDT`)
53
- }
54
-
55
- const fieldKey = this.getCrdtField(entity)?.alias
56
-
57
- if (!fieldKey) {
58
- return null
59
- }
60
-
61
- const db = await this.api.helpers.getConnection('write')
62
-
63
- const [data] = await db.connection.query(`SELECT ${fieldKey} from ${entityAlias} where id = :id`, {
64
- type: QueryTypes.SELECT,
65
- replacements: {
66
- id
67
- }
68
- })
69
-
70
- if (!data) {
71
- throw new Error(`Entity "${entityAlias}" with id=${id} not found`)
72
- }
73
-
74
- const raw = (data as any)[fieldKey]
75
-
76
- if (!raw) {
77
- return null
78
- }
79
-
80
- return raw
81
- } catch (error) {
82
- console.log('error', error)
83
- }
84
- },
85
- store: async ({ documentName, document, state, context }) => {
86
- const [prefix] = documentName.split('.')
87
- if (prefix !== this.documentPrefix) {
88
- return null
89
- }
90
-
91
- const { entityAlias, id } = this.parseDocumentName(documentName)
92
-
93
- if (id <= 0) {
94
- return
95
- }
96
-
97
- const entity = await this.api.helpers.getEntityByAlias(entityAlias as keyof T)
98
- if (!entity) {
99
- throw new Error(`Entity not found: "${entityAlias}"`)
100
- }
101
-
102
- const isSupported = this.isCrdtSupported(entity)
103
- if (!isSupported) {
104
- throw new Error(`Entity "${entityAlias}" does not support CRDT`)
105
- }
106
-
107
- const entityMap = document.getMap(entityAlias)
108
- const body: Record<string, any> = {}
109
-
110
- const fieldKey = this.getCrdtField(entity)?.alias
111
-
112
- for (const [key, value] of entityMap.entries()) {
113
- if (key === fieldKey) {
114
- continue
115
- }
116
-
117
- body[key] = value
118
- }
119
-
120
- // console.log('state', state)
121
-
122
- body[fieldKey] = state
123
-
124
- body.modified_at = new Date().toISOString()
125
-
126
- await this.api
127
- .updateOne(
128
- entityAlias as Extract<keyof T, string>,
129
- {
130
- query: { id },
131
- body
132
- },
133
- {
134
- crdt: false,
135
- extra: {
136
- auth: context.auth
137
- },
138
- excludeHooks: [],
139
- queryOptions: { withDeleted: true, withCrdt: true }
140
- }
141
- )
142
- .catch((e) => {
143
- console.log('crdt on store error', { body })
144
- })
145
- }
146
- })
147
- ],
148
- onAuthenticate: async (data) => {
149
- try {
150
- return await onAuthenticate?.(this.api, data)
151
- } catch (err) {
152
- throw new Error('Ошибка авторизации: ' + err)
153
- }
154
- },
155
- onLoadDocument: async (data) => {
156
- const { documentName } = data
157
-
158
- const [prefix] = documentName.split('.')
159
- if (prefix === this.documentPrefix) {
160
- const { entityAlias, id } = this.parseDocumentName(documentName)
161
-
162
- if (entityAlias) {
163
- const augmentation = this.api.options.augmentations?.[entityAlias]
164
-
165
- if (augmentation?.onLoadDocument) {
166
- await augmentation.onLoadDocument(data)
167
- }
168
- }
169
- }
170
-
171
- return onLoadDocument?.(this.api, data)
172
- },
173
- afterLoadDocument: async (data) => {
174
- const { documentName, document, instance } = data
175
-
176
- return afterLoadDocument?.(this.api, data)
177
- },
178
- connected: async (data) => {
179
- const { documentName, instance } = data
180
-
181
- const [prefix] = documentName?.split('.')
182
- if (prefix === this.documentPrefix) {
183
- const document = instance.documents.get(documentName)
184
-
185
- const { entityAlias, id } = this.parseDocumentName(documentName)
186
-
187
- if (entityAlias && document) {
188
- const entityMap = document.getMap(entityAlias)
189
- const id = entityMap.get('id')
190
-
191
- if (id === undefined) {
192
- document.connections.forEach(({ connection }) => {
193
- connection.close({
194
- reason: 'Document not loaded',
195
- code: 500
196
- })
197
- })
198
- instance.unloadDocument(document)
199
- }
200
- }
201
- }
202
-
203
- return connected?.(this.api, data)
204
- },
205
- onChange: async (data) => {
206
- const { documentName } = data
207
-
208
- const [prefix] = documentName.split('.')
209
- if (prefix === this.documentPrefix) {
210
- const { entityAlias, id } = this.parseDocumentName(documentName)
211
-
212
- if (entityAlias) {
213
- const augmentation = this.api.options.augmentations?.[entityAlias]
214
-
215
- if (augmentation?.onChangeDocument) {
216
- await augmentation.onChangeDocument(data)
217
- }
218
- }
219
- }
220
-
221
- return onChange?.(this.api, data)
222
- },
223
- onStoreDocument: async (data) => {
224
- const { documentName } = data
225
-
226
- const [prefix] = documentName.split('.')
227
- if (prefix === this.documentPrefix) {
228
- const { entityAlias, id } = this.parseDocumentName(documentName)
229
-
230
- if (entityAlias) {
231
- const augmentation = this.api.options.augmentations?.[entityAlias]
232
-
233
- if (augmentation?.onStoreDocument) {
234
- await augmentation.onStoreDocument(data)
235
- }
236
- }
237
- }
238
-
239
- return onStoreDocument?.(this.api, data)
240
- },
241
-
242
- ...config
243
- })
244
- }
245
-
246
- public async listen() {
247
- await this.server.listen()
248
- }
249
-
250
- private parseDocumentName(documentName: string) {
251
- const parts = documentName.split('.')
252
-
253
- const [prefix, entityAlias, idRaw] = parts
254
-
255
- if (prefix !== this.documentPrefix) {
256
- throw new Error(`Invalid prefix: "${prefix}", expected "${this.documentPrefix}"`)
257
- }
258
-
259
- if (!entityAlias) {
260
- throw new Error(`Missing entity alias in documentName: "${documentName}"`)
261
- }
262
-
263
- const id = Number(idRaw)
264
- if (Number.isNaN(id)) {
265
- throw new Error(`Invalid id in documentName: "${documentName}"`)
266
- }
267
-
268
- return { entityAlias: entityAlias as Extract<keyof T, string>, id }
269
- }
270
-
271
- public createDocumentName(entityAlias: string, id: number | string, prefix?: string) {
272
- const parts = [prefix || this.documentPrefix, entityAlias, id]
273
-
274
- return parts.join('.')
275
- }
276
-
277
- public isCrdtSupported(entity: CuboEntity) {
278
- const hasCrdtField = entity.fields.some((f) => f.alias === (this.api.options.crdt?.fieldAlias || this.defaultCrdtFieldAlias))
279
-
280
- return hasCrdtField
281
- }
282
-
283
- public getCrdtField(entity: CuboEntity) {
284
- const crdtField = entity.fields.find((f) => f.alias === (this.api.options.crdt?.fieldAlias || this.defaultCrdtFieldAlias))
285
-
286
- return crdtField
287
- }
288
-
289
- public getDocumentByEntity(entityAlias: string, id: number, requestHeaders: IncomingHttpHeaders) {
290
- const documentName = this.createDocumentName(entityAlias, id)
291
- const fakeSocketId = 'api-' + crypto.randomUUID()
292
-
293
- return this.server.hocuspocus.createDocument(documentName, { headers: requestHeaders, url: '/collaboration' }, fakeSocketId, {
294
- readOnly: false,
295
- isAuthenticated: false
296
- })
297
- }
298
-
299
- public async openDirectConnection(entityAlias: string, id: number, context?: any) {
300
- const documentName = this.createDocumentName(entityAlias, id)
301
- if (!documentName) {
302
- return
303
- }
304
-
305
- return this.server.hocuspocus.openDirectConnection(documentName, context)
306
- }
307
-
308
- public async connectionDisconnect(connection: CuboDirectConnection) {
309
- const { document, instance, context } = connection
310
-
311
- if (document) {
312
- document.removeDirectConnection()
313
-
314
- await instance.storeDocumentHooks(document, {
315
- clientsCount: document.getConnectionsCount(),
316
- context: context,
317
- document: document,
318
- documentName: document.name,
319
- instance: instance,
320
- requestHeaders: {},
321
- requestParameters: new URLSearchParams(),
322
- socketId: 'server'
323
- })
324
-
325
- // // If the direct connection was the only connection to the document
326
- // // then we should trigger the onDisconnect hook for
327
- // // this doc and unload the document
328
- // if (document.getConnectionsCount() === 0) {
329
- // await instance.hooks('onDisconnect', {
330
- // instance: instance,
331
- // clientsCount: document.getConnectionsCount(),
332
- // context: context,
333
- // document: document,
334
- // socketId: 'server',
335
- // documentName: document.name,
336
- // requestHeaders: {},
337
- // requestParameters: new URLSearchParams()
338
- // })
339
-
340
- // await instance.unloadDocument(document)
341
- // }
342
-
343
- connection.document = null
344
- }
345
- }
346
-
347
- public async updateDocument(params: { entityAlias: string; id: number; entity: CuboEntity }, dto: Record<string, any>, context?: any) {
348
- const { entityAlias, id, entity } = params
349
-
350
- const connection = await this.openDirectConnection(entityAlias, id, context)
351
- if (!connection) {
352
- return
353
- }
354
-
355
- const { document } = connection
356
- if (!document) {
357
- return
358
- }
359
-
360
- const entityMap = document.getMap(entityAlias)
361
-
362
- const fieldKey = this.getCrdtField(entity)?.alias
363
-
364
- document.transact(() => {
365
- for (const [key, value] of Object.entries(dto)) {
366
- if (key === fieldKey) {
367
- continue
368
- }
369
-
370
- let preparedValue = value
371
-
372
- if (value instanceof Date) {
373
- preparedValue = value.toISOString()
374
- }
375
-
376
- entityMap.set(key, preparedValue)
377
- }
378
- })
379
-
380
- return this.connectionDisconnect(connection)
381
- }
382
-
383
- public async createState(params: { entityAlias: string; id: number; entity: CuboEntity }, dto: Record<string, any>) {
384
- const { entityAlias, id, entity } = params
385
-
386
- const document = new Y.Doc()
387
- if (!document) {
388
- return
389
- }
390
-
391
- const entityMap = document.getMap(entityAlias)
392
-
393
- const fieldKey = this.getCrdtField(entity)?.alias
394
-
395
- document.transact(() => {
396
- for (const [key, value] of Object.entries(dto)) {
397
- if (key === fieldKey) {
398
- continue
399
- }
400
-
401
- let preparedValue = value
402
-
403
- if (value instanceof Date) {
404
- preparedValue = value.toISOString()
405
- }
406
-
407
- entityMap.set(key, preparedValue)
408
- }
409
- })
410
-
411
- return Buffer.from(Y.encodeStateAsUpdate(document))
412
- }
413
- }
package/src/crdt/types.ts DELETED
@@ -1,34 +0,0 @@
1
- import { CuboApiEntitiesMap } from '@cuboapp/types'
2
- import {
3
- DirectConnection as DirectConnectionInterface,
4
- Document,
5
- Hocuspocus,
6
- ServerConfiguration,
7
- afterLoadDocumentPayload,
8
- connectedPayload,
9
- onAuthenticatePayload,
10
- onChangePayload,
11
- onLoadDocumentPayload,
12
- onStoreDocumentPayload
13
- } from '@hocuspocus/server'
14
-
15
- import { CuboBackendApi } from '..'
16
-
17
- export type CuboCrdtConfiguration<T extends CuboApiEntitiesMap<T>> = Omit<
18
- Partial<ServerConfiguration>,
19
- 'onAuthenticate' | 'onLoadDocument' | 'onChange' | 'onStoreDocument' | 'afterLoadDocument' | 'connected'
20
- > & {
21
- onAuthenticate?: (api: CuboBackendApi<T>, data: onAuthenticatePayload) => Promise<any>
22
- onLoadDocument?: (api: CuboBackendApi<T>, data: onLoadDocumentPayload) => Promise<any>
23
- onChange?: (api: CuboBackendApi<T>, data: onChangePayload) => Promise<any>
24
- onStoreDocument?: (api: CuboBackendApi<T>, data: onStoreDocumentPayload) => Promise<any>
25
- afterLoadDocument?: (api: CuboBackendApi<T>, data: afterLoadDocumentPayload) => Promise<any>
26
- connected?: (api: CuboBackendApi<T>, data: connectedPayload) => Promise<any>
27
- fieldAlias?: string
28
- }
29
-
30
- export type CuboDirectConnection = {
31
- document: Document | null
32
- instance: Hocuspocus
33
- context: any
34
- } & DirectConnectionInterface