@cuboapp/api-backend 1.0.24 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cuboapp/api-backend",
3
- "version": "1.0.24",
3
+ "version": "1.0.26",
4
4
  "description": "Backend Api for CuboApp",
5
5
  "main": "src/index.ts",
6
6
  "repository": "git@github.com:cuboapp/api-backend.git",
@@ -8,15 +8,13 @@
8
8
  "license": "MIT",
9
9
  "type": "module",
10
10
  "dependencies": {
11
- "@cuboapp/constants": "^2.0.6",
12
- "@cuboapp/crdt": "1.0.2",
13
- "@cuboapp/database": "^1.0.3",
14
- "@cuboapp/types": "^2.0.11",
15
- "@cuboapp/utils": "1.0.9",
16
- "@cuboapp/ws": "1.0.4",
17
- "@hocuspocus/extension-database": "^3.4.3",
18
- "@hocuspocus/server": "^3.4.3",
19
- "pg": "^8.16.3",
11
+ "@cuboapp/constants": "2.0.7",
12
+ "@cuboapp/crdt": "1.0.6",
13
+ "@cuboapp/database": "1.0.4",
14
+ "@cuboapp/types": "2.0.11",
15
+ "@cuboapp/utils": "1.0.10",
16
+ "@cuboapp/ws": "1.0.5",
17
+ "pg": "^8.17.2",
20
18
  "yjs": "^13.6.29"
21
19
  },
22
20
  "devDependencies": {
package/src/index.ts CHANGED
@@ -1,22 +1,11 @@
1
- import { CUBO_CRDT_EVENT, CuboCrdt } from '@cuboapp/crdt'
2
1
  import { QueryTypes } from '@cuboapp/database'
3
2
  import { CuboApiEntitiesMap } from '@cuboapp/types'
4
- import { createWsServer, WsServer } from '@cuboapp/ws'
3
+ import { cloneDeep } from '@cuboapp/utils'
5
4
 
6
5
  import { ApiHelpers } from './helpers'
7
- import {
8
- CuboBackendApiAuth,
9
- CuboBackendApiOptions,
10
- CuboCrudAction,
11
- CuboCrudGetManyResponse,
12
- CuboCrudMethodOptions,
13
- CuboCrudRequest
14
- } from './types'
6
+ import { CuboBackendApiAuth, CuboBackendApiOptions, CuboCrudGetManyResponse, CuboCrudMethodOptions, CuboCrudRequest } from './types'
15
7
 
16
8
  export class CuboBackendApi<T extends CuboApiEntitiesMap<T>, A extends unknown> {
17
- public crdt?: CuboCrdt<T, A>
18
- public ws?: WsServer<{ auth: A; subscribes: Map<string, any[]> }>
19
-
20
9
  constructor(public options: CuboBackendApiOptions<T, A>) {}
21
10
 
22
11
  public auth: CuboBackendApiAuth
@@ -42,239 +31,6 @@ export class CuboBackendApi<T extends CuboApiEntitiesMap<T>, A extends unknown>
42
31
  }
43
32
  }
44
33
  }
45
-
46
- if (this.options.crdt !== undefined) {
47
- type ID = `${Extract<keyof T, string>}:${number | string}`
48
-
49
- this.crdt = new CuboCrdt<T, A>({
50
- ...this.options.crdt,
51
- beforeLoadDocument: async (name: ID) => {
52
- const [entityAlias, id] = name.split(':') as [Extract<keyof T, string>, string]
53
-
54
- // console.log('beforeLoadDocument', { entityAlias, id })
55
-
56
- return this.getOne(entityAlias, { query: { id: +id } })
57
- }
58
- })
59
-
60
- this.ws = createWsServer<{ auth: A; subscribes: Map<string, any[]> }>({
61
- host: this.options.crdt.host,
62
- port: this.options.crdt.port,
63
- debug: this.options.crdt.debug,
64
- auth:
65
- this.options.crdt.authHandler !== undefined
66
- ? {
67
- default: true,
68
- handler: async (ctx) => {
69
- ctx.request.auth = await this.options.crdt.authHandler!(ctx)
70
- }
71
- }
72
- : undefined,
73
-
74
- onDisconnect: (socket) => {
75
- this.crdt.documents.forEach((doc) => {
76
- doc.removeClient(socket)
77
-
78
- this.crdt.checkDocumentUnload(doc.opts.name as any)
79
- })
80
- }
81
- })
82
-
83
- await this.ws.start()
84
- await this.crdt.start()
85
-
86
- this.ws.registerHandler<{ name: ID }>('crdt:open', async ({ client, message }) => {
87
- const { name } = message.data || {}
88
- if (!name) {
89
- throw 'Имя документа обязательно: "name"'
90
- }
91
-
92
- console.log('open document', name)
93
-
94
- try {
95
- const document = await this.crdt.getDocument(name)
96
- document.addClient(client)
97
-
98
- if (this.options.crdt.debug) {
99
- console.log('document', name, 'subscribed', document.stat)
100
- }
101
-
102
- return Array.from(document.stateAsUpdate)
103
- } catch (e) {
104
- console.error('CRDT open document', e)
105
- throw e
106
- }
107
- })
108
-
109
- this.ws.registerHandler<{ name: ID }>('crdt:close', async ({ client, message }) => {
110
- const { name } = message.data || {}
111
- if (!name) {
112
- throw 'Имя документа обязательно: "name"'
113
- }
114
-
115
- const document = await this.crdt.getDocument(name)
116
- document.removeClient(client)
117
-
118
- if (this.options.crdt.debug) {
119
- console.log('document', name, 'unsubscribed', document.stat)
120
- }
121
-
122
- await this.crdt.checkDocumentUnload(name)
123
-
124
- return true
125
- })
126
-
127
- this.ws.registerHandler<string>('crdt:subscribe', async ({ client, message }) => {
128
- let { crdt, event, filters } = message.data as any
129
- filters = filters || {}
130
-
131
- if (crdt) {
132
- event = `crdt:update:${crdt.entity}:${crdt.id}`
133
- filters = { id: crdt.id }
134
-
135
- // console.log('subscribe to crdt', event, filters)
136
- }
137
-
138
- client.subscribes = client.subscribes || new Map()
139
- const subs = client.subscribes.get(event) || []
140
- const ex = subs.find((s) => JSON.stringify(s) === JSON.stringify(filters))
141
- if (!ex) {
142
- subs.push(filters)
143
- }
144
- client.subscribes.set(event, subs)
145
-
146
- if (this.options.crdt.debug) {
147
- console.log(
148
- 'crdt:subscribe',
149
- client.id,
150
- JSON.stringify({ event, filters }),
151
- Array.from(client.subscribes).map((s) => [s[0], JSON.stringify(s[1])])
152
- )
153
- }
154
- })
155
-
156
- this.ws.registerHandler<string>('crdt:unsubscribe', async ({ client, message }) => {
157
- let { event, filters } = message.data as any
158
- filters = filters || {}
159
-
160
- client.subscribes = client.subscribes || new Map()
161
- const subs = client.subscribes.get(event) || []
162
- const exIndex = subs.findIndex((s) => JSON.stringify(s) === JSON.stringify(filters))
163
- if (exIndex >= 0) {
164
- subs.splice(exIndex)
165
- }
166
- if (!subs.length) {
167
- client.subscribes.delete(event)
168
- } else {
169
- client.subscribes.set(event, subs)
170
- }
171
-
172
- if (this.options.crdt.debug) {
173
- console.log('crdt:unsubscribe', client.id, { event, filters }, Array.from(client.subscribes))
174
- }
175
- })
176
-
177
- const subFilterCheck = (row: any, filters: any) => {
178
- let sutable = true
179
-
180
- Object.entries(filters).forEach(([key, value]) => {
181
- if (!['limit', 'with'].includes(key)) {
182
- const [formula, str] = `${value}`.split(':')
183
-
184
- console.log({ formula, str, value: row[key] })
185
-
186
- if (!str) {
187
- if (row[key] !== value) {
188
- sutable = false
189
- }
190
- } else {
191
- switch (formula) {
192
- case 'in':
193
- if (
194
- !str
195
- .split(',')
196
- .map(Number)
197
- .includes(row[key] as number)
198
- ) {
199
- sutable = false
200
- }
201
- break
202
- default:
203
- sutable = false
204
- break
205
- }
206
- }
207
- }
208
- })
209
-
210
- return sutable
211
- }
212
-
213
- const onDocumentEvent = async (name: string, action: CuboCrudAction, data?: Uint8Array) => {
214
- const [entityAlias, id] = name.split(':') as [Extract<keyof T, string>, string]
215
-
216
- const entity = await this.helpers.getEntityByAlias(entityAlias)
217
-
218
- const row = await this.getOne(entityAlias, { query: { id: +id } }, { queryOptions: { withDeleted: true } })
219
-
220
- const a = this.options?.augmentations?.[entityAlias]
221
- const filterCheck = a?.filterRowForCrdtEvent ?? subFilterCheck
222
-
223
- try {
224
- for (const client of Array.from(this.ws?.clients)) {
225
- const hasCardSub = client.subscribes?.get(`crdt:${entityAlias}:${id}`)
226
- const hasEntitySub = client.subscribes?.get(`crdt:${entityAlias}`)
227
- const hasCrdtSub = client.subscribes?.get(`crdt:update:${entityAlias}:${id}`)
228
-
229
- const needCardPush = !hasCardSub?.length || !!hasCardSub.find((f) => !!filterCheck(row, f)) || false
230
- const needEntityPush = !hasEntitySub?.length || !!hasEntitySub.find((f) => !!filterCheck(row, f)) || false
231
- const needCrdtPush = !hasCrdtSub?.length || !!hasCrdtSub.find((f) => !!filterCheck(row, f)) || false
232
-
233
- // console.log(id, name, action, {
234
- // hasCardSub,
235
- // hasEntitySub,
236
- // hasCrdtSub,
237
- // needCardPush,
238
- // needEntityPush,
239
- // needCrdtPush,
240
- // filterCheck
241
- // })
242
-
243
- if (needCrdtPush) {
244
- client.send(JSON.stringify({ method: 'message', event: `crdt:update:${entityAlias}:${id}`, data: Array.from(data) }))
245
- }
246
-
247
- if (needCardPush || needEntityPush) {
248
- if (!a?.can || (await a.can<A>(action, { auth: client.auth, entity, row }))) {
249
- const dto = {
250
- event: `crdt:${entityAlias}`,
251
- action,
252
- id: id ? +id : null,
253
- row
254
- }
255
-
256
- if (a?.sendCrdtEvent) {
257
- await a.sendCrdtEvent({ client, auth: client.auth, entity, dto })
258
- } else {
259
- client.send(JSON.stringify({ method: 'message', ...dto }))
260
- }
261
- } else {
262
- console.log('sutable pushes cannot', name)
263
- }
264
- } else {
265
- console.log('no sutable pushes', name)
266
- }
267
- }
268
- } catch (e) {
269
- console.log(e)
270
- }
271
- }
272
-
273
- // document events
274
- this.crdt.ee.addListener(CUBO_CRDT_EVENT.DOCUMENT_UPDATED, (ctx) => onDocumentEvent(ctx.name, 'update', ctx.data))
275
- this.crdt.ee.addListener(CUBO_CRDT_EVENT.DOCUMENT_CREATED, (ctx) => onDocumentEvent(ctx.name, 'create', ctx.data))
276
- this.crdt.ee.addListener(CUBO_CRDT_EVENT.DOCUMENT_DELETED, (ctx) => onDocumentEvent(ctx.name, 'delete', ctx.data))
277
- }
278
34
  } catch (e) {
279
35
  console.error('[API-BACKEND] startup error', e)
280
36
  }
@@ -293,6 +49,8 @@ export class CuboBackendApi<T extends CuboApiEntitiesMap<T>, A extends unknown>
293
49
  }
294
50
  }
295
51
 
52
+ private debounces = new Map<`${string}:${number}`, { started: number; promise: Promise<any> }>()
53
+
296
54
  public async request<T>(url: string, opts?: RequestInit & { debug?: boolean; withAuth?: boolean; withoutContentType?: boolean }) {
297
55
  const baseUrl = this.options?.api?.base_url || 'https://api.cubo.sh'
298
56
 
@@ -509,8 +267,8 @@ export class CuboBackendApi<T extends CuboApiEntitiesMap<T>, A extends unknown>
509
267
  response = await augmentation.afterCreate(response, req, opts)
510
268
  }
511
269
 
512
- if (this.crdt) {
513
- this.crdt.ee.emit(CUBO_CRDT_EVENT.DOCUMENT_CREATED, { name: `${entityAlias}:${(response as any).id}` })
270
+ if (this.options.hooks?.onAfterCreate && !opts?.excludeHooks?.includes('baseHooks')) {
271
+ await this.options.hooks.onAfterCreate(entity, response, opts)
514
272
  }
515
273
 
516
274
  return response
@@ -521,6 +279,32 @@ export class CuboBackendApi<T extends CuboApiEntitiesMap<T>, A extends unknown>
521
279
  req: CuboCrudRequest,
522
280
  opts?: CuboCrudMethodOptions
523
281
  ): Promise<T[K] | undefined> {
282
+ if (opts?.debounce) {
283
+ if (!req.query?.id) {
284
+ throw 'Debounce "updateOne" allowed only with "id"'
285
+ }
286
+ console.log('call debounced updateOne')
287
+
288
+ const key = `${entityAlias}:${+req.query.id}` as `${string}:${number}`
289
+ let ex = this.debounces.get(key)
290
+ if (ex) {
291
+ if (ex.started > +new Date() - opts.debounce) {
292
+ console.log('clear execution')
293
+ Promise.reject(ex.promise)
294
+ } else {
295
+ return ex.promise
296
+ }
297
+ }
298
+
299
+ return new Promise((resolve, reject) => {
300
+ setTimeout(() => {
301
+ this.updateOne(entityAlias, req, { ...(opts || {}), debounce: undefined })
302
+ .then(resolve)
303
+ .catch(reject)
304
+ }, opts.debounce)
305
+ })
306
+ }
307
+
524
308
  opts = opts || {}
525
309
  const entity = await this.helpers.getEntityByAlias(entityAlias)
526
310
  const augmentation = this.options?.augmentations?.[entityAlias]
@@ -535,6 +319,8 @@ export class CuboBackendApi<T extends CuboApiEntitiesMap<T>, A extends unknown>
535
319
  throw { status: 404, text: 'Entity not found' }
536
320
  }
537
321
 
322
+ const initialEntity = cloneDeep(item)
323
+
538
324
  try {
539
325
  if (!!augmentation?.can) {
540
326
  await augmentation.can('update', { entity, row: item, auth: opts?.extra?.auth, req })
@@ -560,8 +346,8 @@ export class CuboBackendApi<T extends CuboApiEntitiesMap<T>, A extends unknown>
560
346
  response = await augmentation.afterUpdate(response, req, opts)
561
347
  }
562
348
 
563
- if (this.crdt) {
564
- await this.crdt.updateDocument(`${entityAlias}:${item.id}`, dto)
349
+ if (this.options.hooks?.onAfterUpdate && !opts?.excludeHooks?.includes('baseHooks')) {
350
+ await this.options.hooks.onAfterUpdate(entity, response, initialEntity, opts)
565
351
  }
566
352
 
567
353
  return response
@@ -593,18 +379,19 @@ export class CuboBackendApi<T extends CuboApiEntitiesMap<T>, A extends unknown>
593
379
  const db = await this.helpers.getConnection('write')
594
380
  await db.update(entity.alias, 'id = :id', { id: item.id }, dto, { transaction: opts?.transaction, log: opts?.log })
595
381
 
596
- if (augmentation?.afterDelete && !opts?.excludeHooks?.includes('afterDelete')) {
597
- return augmentation.afterDelete(item, req, opts)
382
+ if (this.options.hooks?.onAfterDelete && !opts?.excludeHooks?.includes('baseHooks')) {
383
+ await this.options.hooks.onAfterDelete(entity, item, opts)
598
384
  }
599
385
 
600
- if (this.crdt) {
601
- this.crdt.ee.emit(CUBO_CRDT_EVENT.DOCUMENT_DELETED, { name: `${entityAlias}:${item.id}` })
386
+ // console.log(entityAlias, augmentation, opts)
387
+
388
+ if (augmentation?.afterDelete && !opts?.excludeHooks?.includes('afterDelete')) {
389
+ return augmentation.afterDelete(item, req, opts)
602
390
  }
603
391
 
604
392
  return true
605
393
  }
606
394
  }
607
395
 
608
- // export * from './crdt'
609
396
  export * from './helpers'
610
397
  export * from './types'