@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cuboapp/api-backend",
3
- "version": "1.0.25",
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.5",
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,241 +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
- if (this.options.crdt.debug) {
93
- console.log('open document', name)
94
- }
95
-
96
- try {
97
- const document = await this.crdt.getDocument(name)
98
- document.addClient(client)
99
-
100
- if (this.options.crdt.debug) {
101
- console.log('document', name, 'subscribed', document.stat)
102
- }
103
-
104
- return Array.from(document.stateAsUpdate)
105
- } catch (e) {
106
- console.error('CRDT open document', e)
107
- throw e
108
- }
109
- })
110
-
111
- this.ws.registerHandler<{ name: ID }>('crdt:close', async ({ client, message }) => {
112
- const { name } = message.data || {}
113
- if (!name) {
114
- throw 'Имя документа обязательно: "name"'
115
- }
116
-
117
- const document = await this.crdt.getDocument(name)
118
- document.removeClient(client)
119
-
120
- if (this.options.crdt.debug) {
121
- console.log('document', name, 'unsubscribed', document.stat)
122
- }
123
-
124
- await this.crdt.checkDocumentUnload(name)
125
-
126
- return true
127
- })
128
-
129
- this.ws.registerHandler<string>('crdt:subscribe', async ({ client, message }) => {
130
- let { crdt, event, filters } = message.data as any
131
- filters = filters || {}
132
-
133
- if (crdt) {
134
- event = `crdt:update:${crdt.entity}:${crdt.id}`
135
- filters = { id: crdt.id }
136
-
137
- // console.log('subscribe to crdt', event, filters)
138
- }
139
-
140
- client.subscribes = client.subscribes || new Map()
141
- const subs = client.subscribes.get(event) || []
142
- const ex = subs.find((s) => JSON.stringify(s) === JSON.stringify(filters))
143
- if (!ex) {
144
- subs.push(filters)
145
- }
146
- client.subscribes.set(event, subs)
147
-
148
- if (this.options.crdt.debug) {
149
- console.log(
150
- 'crdt:subscribe',
151
- client.id,
152
- JSON.stringify({ event, filters }),
153
- Array.from(client.subscribes).map((s) => [s[0], JSON.stringify(s[1])])
154
- )
155
- }
156
- })
157
-
158
- this.ws.registerHandler<string>('crdt:unsubscribe', async ({ client, message }) => {
159
- let { event, filters } = message.data as any
160
- filters = filters || {}
161
-
162
- client.subscribes = client.subscribes || new Map()
163
- const subs = client.subscribes.get(event) || []
164
- const exIndex = subs.findIndex((s) => JSON.stringify(s) === JSON.stringify(filters))
165
- if (exIndex >= 0) {
166
- subs.splice(exIndex)
167
- }
168
- if (!subs.length) {
169
- client.subscribes.delete(event)
170
- } else {
171
- client.subscribes.set(event, subs)
172
- }
173
-
174
- if (this.options.crdt.debug) {
175
- console.log('crdt:unsubscribe', client.id, { event, filters }, Array.from(client.subscribes))
176
- }
177
- })
178
-
179
- const subFilterCheck = (row: any, filters: any) => {
180
- let sutable = true
181
-
182
- Object.entries(filters).forEach(([key, value]) => {
183
- if (!['limit', 'with'].includes(key)) {
184
- const [formula, str] = `${value}`.split(':')
185
-
186
- // console.log({ formula, str, value: row[key] })
187
-
188
- if (!str) {
189
- if (row[key] !== value) {
190
- sutable = false
191
- }
192
- } else {
193
- switch (formula) {
194
- case 'in':
195
- if (
196
- !str
197
- .split(',')
198
- .map(Number)
199
- .includes(row[key] as number)
200
- ) {
201
- sutable = false
202
- }
203
- break
204
- default:
205
- sutable = false
206
- break
207
- }
208
- }
209
- }
210
- })
211
-
212
- return sutable
213
- }
214
-
215
- const onDocumentEvent = async (name: string, action: CuboCrudAction, data?: Uint8Array) => {
216
- const [entityAlias, id] = name.split(':') as [Extract<keyof T, string>, string]
217
-
218
- const entity = await this.helpers.getEntityByAlias(entityAlias)
219
-
220
- const row = await this.getOne(entityAlias, { query: { id: +id } }, { queryOptions: { withDeleted: true } })
221
-
222
- const a = this.options?.augmentations?.[entityAlias]
223
- const filterCheck = a?.filterRowForCrdtEvent ?? subFilterCheck
224
-
225
- try {
226
- for (const client of Array.from(this.ws?.clients)) {
227
- const hasCardSub = client.subscribes?.get(`crdt:${entityAlias}:${id}`)
228
- const hasEntitySub = client.subscribes?.get(`crdt:${entityAlias}`)
229
- const hasCrdtSub = client.subscribes?.get(`crdt:update:${entityAlias}:${id}`)
230
-
231
- const needCardPush = !hasCardSub?.length || !!hasCardSub.find((f) => !!filterCheck(row, f)) || false
232
- const needEntityPush = !hasEntitySub?.length || !!hasEntitySub.find((f) => !!filterCheck(row, f)) || false
233
- const needCrdtPush = !hasCrdtSub?.length || !!hasCrdtSub.find((f) => !!filterCheck(row, f)) || false
234
-
235
- // console.log(id, name, action, {
236
- // hasCardSub,
237
- // hasEntitySub,
238
- // hasCrdtSub,
239
- // needCardPush,
240
- // needEntityPush,
241
- // needCrdtPush,
242
- // filterCheck
243
- // })
244
-
245
- if (needCrdtPush && data?.length) {
246
- client.send(JSON.stringify({ method: 'message', event: `crdt:update:${entityAlias}:${id}`, data: Array.from(data) }))
247
- }
248
-
249
- if (needCardPush || needEntityPush) {
250
- if (!a?.can || (await a.can<A>(action, { auth: client.auth, entity, row }))) {
251
- const dto = {
252
- event: `crdt:${entityAlias}`,
253
- action,
254
- id: id ? +id : null,
255
- row
256
- }
257
-
258
- if (a?.sendCrdtEvent) {
259
- await a.sendCrdtEvent({ client, auth: client.auth, entity, dto })
260
- } else {
261
- client.send(JSON.stringify({ method: 'message', ...dto }))
262
- }
263
- } else {
264
- console.log('sutable pushes cannot', name)
265
- }
266
- } else {
267
- console.log('no sutable pushes', name)
268
- }
269
- }
270
- } catch (e) {
271
- console.log(e)
272
- }
273
- }
274
-
275
- // document events
276
- this.crdt.ee.addListener(CUBO_CRDT_EVENT.DOCUMENT_UPDATED, (ctx) => onDocumentEvent(ctx.name, 'update', ctx.data))
277
- this.crdt.ee.addListener(CUBO_CRDT_EVENT.DOCUMENT_CREATED, (ctx) => onDocumentEvent(ctx.name, 'create', ctx.data))
278
- this.crdt.ee.addListener(CUBO_CRDT_EVENT.DOCUMENT_DELETED, (ctx) => onDocumentEvent(ctx.name, 'delete', ctx.data))
279
- }
280
34
  } catch (e) {
281
35
  console.error('[API-BACKEND] startup error', e)
282
36
  }
@@ -295,6 +49,8 @@ export class CuboBackendApi<T extends CuboApiEntitiesMap<T>, A extends unknown>
295
49
  }
296
50
  }
297
51
 
52
+ private debounces = new Map<`${string}:${number}`, { started: number; promise: Promise<any> }>()
53
+
298
54
  public async request<T>(url: string, opts?: RequestInit & { debug?: boolean; withAuth?: boolean; withoutContentType?: boolean }) {
299
55
  const baseUrl = this.options?.api?.base_url || 'https://api.cubo.sh'
300
56
 
@@ -511,8 +267,8 @@ export class CuboBackendApi<T extends CuboApiEntitiesMap<T>, A extends unknown>
511
267
  response = await augmentation.afterCreate(response, req, opts)
512
268
  }
513
269
 
514
- if (this.crdt) {
515
- 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)
516
272
  }
517
273
 
518
274
  return response
@@ -523,6 +279,32 @@ export class CuboBackendApi<T extends CuboApiEntitiesMap<T>, A extends unknown>
523
279
  req: CuboCrudRequest,
524
280
  opts?: CuboCrudMethodOptions
525
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
+
526
308
  opts = opts || {}
527
309
  const entity = await this.helpers.getEntityByAlias(entityAlias)
528
310
  const augmentation = this.options?.augmentations?.[entityAlias]
@@ -537,6 +319,8 @@ export class CuboBackendApi<T extends CuboApiEntitiesMap<T>, A extends unknown>
537
319
  throw { status: 404, text: 'Entity not found' }
538
320
  }
539
321
 
322
+ const initialEntity = cloneDeep(item)
323
+
540
324
  try {
541
325
  if (!!augmentation?.can) {
542
326
  await augmentation.can('update', { entity, row: item, auth: opts?.extra?.auth, req })
@@ -562,8 +346,8 @@ export class CuboBackendApi<T extends CuboApiEntitiesMap<T>, A extends unknown>
562
346
  response = await augmentation.afterUpdate(response, req, opts)
563
347
  }
564
348
 
565
- if (this.crdt) {
566
- 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)
567
351
  }
568
352
 
569
353
  return response
@@ -595,18 +379,19 @@ export class CuboBackendApi<T extends CuboApiEntitiesMap<T>, A extends unknown>
595
379
  const db = await this.helpers.getConnection('write')
596
380
  await db.update(entity.alias, 'id = :id', { id: item.id }, dto, { transaction: opts?.transaction, log: opts?.log })
597
381
 
598
- if (augmentation?.afterDelete && !opts?.excludeHooks?.includes('afterDelete')) {
599
- 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)
600
384
  }
601
385
 
602
- if (this.crdt) {
603
- 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)
604
390
  }
605
391
 
606
392
  return true
607
393
  }
608
394
  }
609
395
 
610
- // export * from './crdt'
611
396
  export * from './helpers'
612
397
  export * from './types'