@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 +8 -10
- package/src/index.ts +42 -255
- 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,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.
|
|
513
|
-
this.
|
|
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.
|
|
564
|
-
await this.
|
|
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 (
|
|
597
|
-
|
|
382
|
+
if (this.options.hooks?.onAfterDelete && !opts?.excludeHooks?.includes('baseHooks')) {
|
|
383
|
+
await this.options.hooks.onAfterDelete(entity, item, opts)
|
|
598
384
|
}
|
|
599
385
|
|
|
600
|
-
|
|
601
|
-
|
|
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'
|