@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 +8 -10
- package/src/index.ts +42 -257
- package/src/old.ts +744 -0
- package/src/types/db.ts +5 -2
- package/src/types/index.ts +11 -2
- package/src/crdt/index.ts +0 -413
- package/src/crdt/types.ts +0 -34
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cuboapp/api-backend",
|
|
3
|
-
"version": "1.0.
|
|
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": "
|
|
12
|
-
"@cuboapp/crdt": "1.0.
|
|
13
|
-
"@cuboapp/database": "
|
|
14
|
-
"@cuboapp/types": "
|
|
15
|
-
"@cuboapp/utils": "1.0.
|
|
16
|
-
"@cuboapp/ws": "1.0.
|
|
17
|
-
"
|
|
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 {
|
|
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.
|
|
515
|
-
this.
|
|
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.
|
|
566
|
-
await this.
|
|
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 (
|
|
599
|
-
|
|
382
|
+
if (this.options.hooks?.onAfterDelete && !opts?.excludeHooks?.includes('baseHooks')) {
|
|
383
|
+
await this.options.hooks.onAfterDelete(entity, item, opts)
|
|
600
384
|
}
|
|
601
385
|
|
|
602
|
-
|
|
603
|
-
|
|
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'
|