@cuboapp/api-backend 1.0.25 → 1.0.27
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/src/old.ts
ADDED
|
@@ -0,0 +1,744 @@
|
|
|
1
|
+
import { CuboCrdtDocument, CuboCrdtServer, CuboCrdtSubescribe } from '@cuboapp/crdt'
|
|
2
|
+
import { QueryTypes } from '@cuboapp/database'
|
|
3
|
+
import { CuboApiEntitiesMap } from '@cuboapp/types'
|
|
4
|
+
import { cloneDeep, pick } from '@cuboapp/utils'
|
|
5
|
+
import { createWsServer, WsServer, WsServerSocket } from '@cuboapp/ws'
|
|
6
|
+
|
|
7
|
+
import { CuboCrdtServerSubscribe } from '@cuboapp/crdt/src/server/types'
|
|
8
|
+
import { ApiHelpers } from './helpers'
|
|
9
|
+
import { CuboBackendApiAuth, CuboBackendApiOptions, CuboCrudGetManyResponse, CuboCrudMethodOptions, CuboCrudRequest } from './types'
|
|
10
|
+
|
|
11
|
+
type CuboBackendWsServer<A> = { auth: A }
|
|
12
|
+
|
|
13
|
+
type CuboBackendApiSubscribes<A, T> = Map<
|
|
14
|
+
string,
|
|
15
|
+
Map<WsServerSocket<{ auth: A }>, Map<string, CuboCrdtSubescribe<T> & { docs: Set<CuboCrdtDocument> }>>
|
|
16
|
+
>
|
|
17
|
+
|
|
18
|
+
export class CuboBackendApi<T extends CuboApiEntitiesMap<T>, A extends unknown> {
|
|
19
|
+
public crdt?: CuboCrdtServer<T>
|
|
20
|
+
public ws?: WsServer<CuboBackendWsServer<A>>
|
|
21
|
+
|
|
22
|
+
public subscribes: CuboBackendApiSubscribes<A, T> = new Map()
|
|
23
|
+
|
|
24
|
+
constructor(public options: CuboBackendApiOptions<T, A>) {}
|
|
25
|
+
|
|
26
|
+
public auth: CuboBackendApiAuth
|
|
27
|
+
public helpers = new ApiHelpers<T, A>(this)
|
|
28
|
+
|
|
29
|
+
async init() {
|
|
30
|
+
try {
|
|
31
|
+
if (this.options.api !== undefined) {
|
|
32
|
+
this.auth = await this.request<{ variables: Record<string, any> }>('/auth/me?with=variables', {
|
|
33
|
+
withAuth: true
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
if (!this.auth) {
|
|
37
|
+
throw { code: 403, text: 'Авторизация Cubo-Backend не пройдена' }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (this.auth.variables?.db !== undefined) {
|
|
41
|
+
this.options.db = this.options.db || {}
|
|
42
|
+
|
|
43
|
+
this.options.db.options = {
|
|
44
|
+
...(this.options.db.options || {}),
|
|
45
|
+
...(this.auth.variables?.db || {})
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (this.options.crdt !== undefined) {
|
|
51
|
+
this.ws = createWsServer<CuboBackendWsServer<A>>({
|
|
52
|
+
host: this.options.crdt.host,
|
|
53
|
+
port: this.options.crdt.port,
|
|
54
|
+
debug: this.options.crdt.debug,
|
|
55
|
+
auth:
|
|
56
|
+
this.options.crdt.authHandler !== undefined
|
|
57
|
+
? {
|
|
58
|
+
default: true,
|
|
59
|
+
handler: async (ctx) => {
|
|
60
|
+
ctx.request.auth = await this.options.crdt.authHandler!(ctx)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
: undefined,
|
|
64
|
+
|
|
65
|
+
onConnect: (client) => this.crdt.addClient(client),
|
|
66
|
+
onDisconnect: (client) => this.crdt.removeClient(client)
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
this.crdt = new CuboCrdtServer<T>({
|
|
70
|
+
debug: this.options.crdt.debug,
|
|
71
|
+
ws: this.ws,
|
|
72
|
+
entities: (this.options.entities?.map((e) => e.alias) || []) as any[],
|
|
73
|
+
fetchRows: async (client: WsServerSocket, sub: CuboCrdtServerSubscribe) => {
|
|
74
|
+
return this.getMany(
|
|
75
|
+
sub.entity as Extract<keyof T, string>,
|
|
76
|
+
{ query: sub.filters },
|
|
77
|
+
{ queryOptions: { extra: { auth: client.auth } } }
|
|
78
|
+
).then((res) => res.rows)
|
|
79
|
+
}
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
const getSub = async (client: WsServerSocket<CuboBackendWsServer<A>>, dto: CuboCrdtSubescribe<T>, opts?: { create?: boolean }) => {
|
|
83
|
+
const subKey = `${dto.entity}`
|
|
84
|
+
|
|
85
|
+
let baseSub = this.subscribes.get(subKey)
|
|
86
|
+
if (!baseSub) {
|
|
87
|
+
if (opts?.create !== false) {
|
|
88
|
+
baseSub = this.subscribes.set(subKey, new Map()).get(subKey)
|
|
89
|
+
} else {
|
|
90
|
+
return { subKey }
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
let clientSub = baseSub.get(client)
|
|
95
|
+
if (!clientSub) {
|
|
96
|
+
if (opts?.create !== false) {
|
|
97
|
+
clientSub = baseSub.set(client, new Map()).get(client)
|
|
98
|
+
} else {
|
|
99
|
+
return { baseSub, subKey }
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const sub = clientSub.get(dto.id)
|
|
104
|
+
if (!sub) {
|
|
105
|
+
if (opts?.create !== false) {
|
|
106
|
+
return {
|
|
107
|
+
sub: clientSub.set(dto.id, { ...dto, docs: new Set() }).get(dto.id),
|
|
108
|
+
clientSub,
|
|
109
|
+
baseSub,
|
|
110
|
+
subKey
|
|
111
|
+
}
|
|
112
|
+
} else {
|
|
113
|
+
return {
|
|
114
|
+
clientSub,
|
|
115
|
+
baseSub,
|
|
116
|
+
subKey
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return { sub, clientSub, baseSub, subKey }
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
this.ws.registerHandler<{ subscribe_id: string; name: string; update: Uint8Array; origin?: any }>(
|
|
125
|
+
'crdt:update',
|
|
126
|
+
async ({ client, message }) => {
|
|
127
|
+
const { name, update, origin, subscribe_id } = message.data
|
|
128
|
+
// const { sub, subKey } = await getSub(client, message.data)
|
|
129
|
+
|
|
130
|
+
const doc = await this.crdt.getDocument(name, { autoCreate: false })
|
|
131
|
+
// console.log('[crdt:update] - received', message)
|
|
132
|
+
|
|
133
|
+
if (doc) {
|
|
134
|
+
doc.applyUpdate(
|
|
135
|
+
new Uint8Array(update),
|
|
136
|
+
JSON.parse(
|
|
137
|
+
JSON.stringify({
|
|
138
|
+
...(origin || {}),
|
|
139
|
+
subscribe_id
|
|
140
|
+
})
|
|
141
|
+
)
|
|
142
|
+
)
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
const crdtSubscribe = async (
|
|
148
|
+
entity: T[Extract<keyof T, string>],
|
|
149
|
+
sub: CuboCrdtSubescribe<T> & {
|
|
150
|
+
docs: Set<CuboCrdtDocument>
|
|
151
|
+
},
|
|
152
|
+
client: WsServerSocket
|
|
153
|
+
) => {
|
|
154
|
+
const entity_id = (entity as any).id
|
|
155
|
+
|
|
156
|
+
const doc = await this.crdt.getDocument(`${sub.entity}:${entity_id}`, { initialState: JSON.parse(JSON.stringify(entity)) })
|
|
157
|
+
doc.subscribe(sub.id, (data, origin) => {
|
|
158
|
+
const noStrategyResolved = !origin?.strategy || origin?.strategy === 'all'
|
|
159
|
+
const strategyExcludedResolved = (origin?.strategy === 'other' && origin.subscribe_id !== sub.id) || false
|
|
160
|
+
|
|
161
|
+
const row = doc.getMap().toJSON()
|
|
162
|
+
|
|
163
|
+
// console.log(
|
|
164
|
+
// '[crdt:update] - push to client',
|
|
165
|
+
// { subscribe_id: sub.id, origin, data: data.length },
|
|
166
|
+
// { noStrategyResolved, strategyExcludedResolved }
|
|
167
|
+
// )
|
|
168
|
+
|
|
169
|
+
if (noStrategyResolved || strategyExcludedResolved) {
|
|
170
|
+
const isSutable = subFilterCheck(row, sub.filters || {})
|
|
171
|
+
|
|
172
|
+
let action = 'update'
|
|
173
|
+
if (!isSutable) {
|
|
174
|
+
action = !sub.docs?.has(doc) ? 'create' : 'delete'
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
client.send(
|
|
178
|
+
JSON.stringify({
|
|
179
|
+
method: 'crdt:action:' + sub.id,
|
|
180
|
+
data: {
|
|
181
|
+
action,
|
|
182
|
+
entity_id,
|
|
183
|
+
entity: sub.entity,
|
|
184
|
+
data: Array.from(data)
|
|
185
|
+
}
|
|
186
|
+
})
|
|
187
|
+
)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (origin?.with_save) {
|
|
191
|
+
const body = pick(doc.getMap().toJSON(), origin.keys || [])
|
|
192
|
+
|
|
193
|
+
if (Object.keys(body)) {
|
|
194
|
+
console.log('call updateOne', sub.entity, entity_id)
|
|
195
|
+
// todo: debounce update
|
|
196
|
+
this.updateOne(
|
|
197
|
+
sub.entity,
|
|
198
|
+
{
|
|
199
|
+
query: { id: entity_id },
|
|
200
|
+
body
|
|
201
|
+
},
|
|
202
|
+
{ debounce: 300, log: false, extra: { auth: client.auth } }
|
|
203
|
+
).catch((e) => {
|
|
204
|
+
console.log(e)
|
|
205
|
+
})
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
})
|
|
209
|
+
sub.docs.add(doc)
|
|
210
|
+
|
|
211
|
+
return {
|
|
212
|
+
id: entity_id,
|
|
213
|
+
row: Array.from(doc.stateAsUpdate)
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
this.ws.registerHandler<CuboCrdtSubescribe<T>>('crdt:subscribe', async ({ client, message }) => {
|
|
218
|
+
const { sub, subKey } = await getSub(client, message.data)
|
|
219
|
+
|
|
220
|
+
if (!sub) {
|
|
221
|
+
throw 'Подписка не определена: "' + subKey + '"'
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (sub.entity_id) {
|
|
225
|
+
const entity = await this.getOne(
|
|
226
|
+
sub.entity,
|
|
227
|
+
{ query: { ...(sub.filters || {}), id: sub.entity_id } },
|
|
228
|
+
{ queryOptions: { extra: { auth: client.auth } } }
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
return crdtSubscribe(entity, sub, client)
|
|
232
|
+
} else {
|
|
233
|
+
const { rows } = await this.getMany(sub.entity, { query: sub.filters || {} }, { extra: { auth: client.auth } })
|
|
234
|
+
|
|
235
|
+
return Promise.all(rows.map((row) => crdtSubscribe(row, sub, client)))
|
|
236
|
+
}
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
this.ws.registerHandler<CuboCrdtSubescribe<T>>('crdt:unsubscribe', async ({ client, message }) => {
|
|
240
|
+
const { sub, clientSub } = await getSub(client, message.data)
|
|
241
|
+
|
|
242
|
+
if (sub) {
|
|
243
|
+
sub.docs?.forEach((doc) => doc?.unsubscribe(sub.id))
|
|
244
|
+
|
|
245
|
+
clientSub.delete(sub.id)
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return true
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
const subFilterCheck = (row: object, filters?: object) => {
|
|
252
|
+
let sutable = true
|
|
253
|
+
|
|
254
|
+
if (Object.keys(filters || {}).length) {
|
|
255
|
+
Object.entries(filters).forEach(([key, value]) => {
|
|
256
|
+
if (!['limit', 'with'].includes(key)) {
|
|
257
|
+
const [formula, str] = `${value}`.split(':')
|
|
258
|
+
|
|
259
|
+
// console.log({ formula, str, value: row[key] })
|
|
260
|
+
|
|
261
|
+
if (!str) {
|
|
262
|
+
if (row[key] !== value) {
|
|
263
|
+
sutable = false
|
|
264
|
+
}
|
|
265
|
+
} else {
|
|
266
|
+
switch (formula) {
|
|
267
|
+
case 'in':
|
|
268
|
+
if (
|
|
269
|
+
!str
|
|
270
|
+
.split(',')
|
|
271
|
+
.map(Number)
|
|
272
|
+
.includes(row[key] as number)
|
|
273
|
+
) {
|
|
274
|
+
sutable = false
|
|
275
|
+
}
|
|
276
|
+
break
|
|
277
|
+
default:
|
|
278
|
+
sutable = false
|
|
279
|
+
break
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
})
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return sutable
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
await this.ws.start()
|
|
290
|
+
|
|
291
|
+
this.ws.httpServer.registerRoute('GET', '/clients', () => {
|
|
292
|
+
return {
|
|
293
|
+
documents: this.crdt.documents.size,
|
|
294
|
+
subescribes: Object.fromEntries(
|
|
295
|
+
Array.from(this.subscribes).map(([key, clients]) => {
|
|
296
|
+
return [
|
|
297
|
+
key,
|
|
298
|
+
{
|
|
299
|
+
count: clients.size,
|
|
300
|
+
clients: Object.fromEntries(
|
|
301
|
+
Array.from(clients).map(([client, sub]) => {
|
|
302
|
+
return [
|
|
303
|
+
client.id,
|
|
304
|
+
{
|
|
305
|
+
auth: (client.auth as any)?.user?.name,
|
|
306
|
+
subscribes: Object.fromEntries(Array.from(sub))
|
|
307
|
+
}
|
|
308
|
+
]
|
|
309
|
+
})
|
|
310
|
+
)
|
|
311
|
+
}
|
|
312
|
+
]
|
|
313
|
+
})
|
|
314
|
+
),
|
|
315
|
+
clients: this.ws.clients.size
|
|
316
|
+
}
|
|
317
|
+
})
|
|
318
|
+
}
|
|
319
|
+
} catch (e) {
|
|
320
|
+
console.error('[API-BACKEND] startup error', e)
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
async destroy() {
|
|
325
|
+
const read = await this.helpers.getConnection('read', false)
|
|
326
|
+
const write = await this.helpers.getConnection('write', false)
|
|
327
|
+
|
|
328
|
+
if (read) {
|
|
329
|
+
await read.disconnect()
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (write) {
|
|
333
|
+
await write.disconnect()
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// private debouncers = new Map<string, { createOne: any; updateOne: any; deleteOne: any }>()
|
|
338
|
+
// private createDebouncers() {
|
|
339
|
+
// this.options.entities.forEach((entity) => {
|
|
340
|
+
// this.debouncers.set(entity.alias, {
|
|
341
|
+
// createOne: (wait: number, req: any, opts?: any) => {
|
|
342
|
+
// return debounce(this.createOne.bind(this, entity.alias, req, { ...(opts || {}), debounce: undefined }), wait)
|
|
343
|
+
// },
|
|
344
|
+
// updateOne: (wait: number, req: any, opts?: any) => {
|
|
345
|
+
// return debounce(this.updateOne.bind(this, entity.alias, req, { ...(opts || {}), debounce: undefined }), wait)
|
|
346
|
+
// },
|
|
347
|
+
// deleteOne: (wait: number, req: any, opts?: any) => {
|
|
348
|
+
// return debounce(this.deleteOne.bind(this, entity.alias, req, { ...(opts || {}), debounce: undefined }), wait)
|
|
349
|
+
// }
|
|
350
|
+
// })
|
|
351
|
+
// })
|
|
352
|
+
// }
|
|
353
|
+
|
|
354
|
+
private debounces = new Map<`${string}:${number}`, { started: number; promise: Promise<any> }>()
|
|
355
|
+
|
|
356
|
+
public async request<T>(url: string, opts?: RequestInit & { debug?: boolean; withAuth?: boolean; withoutContentType?: boolean }) {
|
|
357
|
+
const baseUrl = this.options?.api?.base_url || 'https://api.cubo.sh'
|
|
358
|
+
|
|
359
|
+
const headers: Record<string, any> = {
|
|
360
|
+
...(opts?.headers || {})
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
if (opts?.withAuth) {
|
|
364
|
+
Object.assign(headers, this.options.api?.headers || {})
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (!headers['Content-Type'] && !opts?.withoutContentType) {
|
|
368
|
+
headers['Accept'] = 'application/json'
|
|
369
|
+
headers['Content-Type'] = 'application/json'
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const requestUrl = baseUrl.replace(/\/$/, '') + '/' + url.replace(/^\//, '')
|
|
373
|
+
const requestOpts = {
|
|
374
|
+
...(opts || {}),
|
|
375
|
+
headers
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
try {
|
|
379
|
+
const response = await fetch(requestUrl, requestOpts)
|
|
380
|
+
|
|
381
|
+
if ([201, 200].includes(response.status)) {
|
|
382
|
+
try {
|
|
383
|
+
const json = await response.json()
|
|
384
|
+
|
|
385
|
+
return json as Promise<T>
|
|
386
|
+
} catch (e) {
|
|
387
|
+
const text = await response.text()
|
|
388
|
+
|
|
389
|
+
try {
|
|
390
|
+
const json = text ? JSON.parse(text) : null
|
|
391
|
+
|
|
392
|
+
return json as Promise<T>
|
|
393
|
+
} catch {
|
|
394
|
+
throw { status: 500, error: 'Invalid response', text }
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
} else {
|
|
398
|
+
throw {
|
|
399
|
+
status: response.status,
|
|
400
|
+
error: response.statusText,
|
|
401
|
+
text: await response.text()
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
} catch (e) {
|
|
405
|
+
if (opts?.debug) {
|
|
406
|
+
console.log(
|
|
407
|
+
'[API BACKEND]',
|
|
408
|
+
JSON.stringify(
|
|
409
|
+
{
|
|
410
|
+
request: {
|
|
411
|
+
url: requestUrl,
|
|
412
|
+
opts: requestOpts
|
|
413
|
+
},
|
|
414
|
+
error: e
|
|
415
|
+
},
|
|
416
|
+
null,
|
|
417
|
+
2
|
|
418
|
+
)
|
|
419
|
+
)
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
throw e
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
private async getEntity(entityAlias: Extract<keyof T, string>) {
|
|
427
|
+
const entity = await this.helpers.getEntityByAlias(entityAlias)
|
|
428
|
+
if (!entity) {
|
|
429
|
+
throw new Error('Entity no found: "' + String(entityAlias) + '"')
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
return entity
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
private async crdtAfterEvent<K extends Extract<keyof T, string>>(
|
|
436
|
+
event: 'update',
|
|
437
|
+
entity: K,
|
|
438
|
+
newVal?: T[K],
|
|
439
|
+
oldVal?: T[K],
|
|
440
|
+
origin?: CuboCrudMethodOptions['crdt']
|
|
441
|
+
) {
|
|
442
|
+
const entity_id = newVal ? (newVal as any).id : (oldVal as any)?.id
|
|
443
|
+
|
|
444
|
+
const doc = await this.crdt.getDocument(`${entity}:${entity_id}`, { autoCreate: false })
|
|
445
|
+
|
|
446
|
+
if (doc) {
|
|
447
|
+
switch (event) {
|
|
448
|
+
case 'update':
|
|
449
|
+
{
|
|
450
|
+
const diff = Object.fromEntries(
|
|
451
|
+
Object.entries(newVal)
|
|
452
|
+
.map(([key, value]) => {
|
|
453
|
+
if (value !== oldVal[key]) {
|
|
454
|
+
return [key, value]
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
return undefined
|
|
458
|
+
})
|
|
459
|
+
.filter((i) => !!i)
|
|
460
|
+
)
|
|
461
|
+
// console.log('call crdtAfterEvent', { entity, diff })
|
|
462
|
+
|
|
463
|
+
if (Object.keys(diff).length) {
|
|
464
|
+
doc.writeMap(diff, origin)
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
break
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
async getMany<K extends Extract<keyof T, string> = Extract<keyof T, string>>(
|
|
473
|
+
entityAlias: K,
|
|
474
|
+
req?: CuboCrudRequest,
|
|
475
|
+
opts?: CuboCrudMethodOptions
|
|
476
|
+
): Promise<CuboCrudGetManyResponse<T[K]>> {
|
|
477
|
+
opts = opts || {}
|
|
478
|
+
req = req || {}
|
|
479
|
+
const entity = await this.getEntity(entityAlias)
|
|
480
|
+
const augmentation = this.options?.augmentations?.[entityAlias]
|
|
481
|
+
|
|
482
|
+
if (augmentation?.beforeGetMany) {
|
|
483
|
+
opts.queryOptions = await augmentation.beforeGetMany(req, opts)
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// get all joins
|
|
487
|
+
const queryDto = await this.helpers.withes.prepare(req, entity, opts)
|
|
488
|
+
queryDto.withDeleted = opts.queryOptions?.withDeleted
|
|
489
|
+
// queryDto.withCrdt = opts.queryOptions?.withCrdt
|
|
490
|
+
|
|
491
|
+
// create find query
|
|
492
|
+
const regularQuery = await this.helpers.createFindQuery(req, entity, queryDto)
|
|
493
|
+
const countQuery = await this.helpers.createFindQuery(req, entity, queryDto, true)
|
|
494
|
+
|
|
495
|
+
const db = await this.helpers.getConnection('write')
|
|
496
|
+
|
|
497
|
+
if (opts?.log) {
|
|
498
|
+
console.log(`QUERY:getMany to "${String(entityAlias)}":`, regularQuery.sql, regularQuery.replacements, { req, opts, queryDto })
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
const rows = await db.connection.query<any>(regularQuery.sql!, {
|
|
502
|
+
type: QueryTypes.SELECT,
|
|
503
|
+
replacements: regularQuery.replacements,
|
|
504
|
+
transaction: opts?.transaction
|
|
505
|
+
})
|
|
506
|
+
const [totals] = await db.connection.query<any>(countQuery.sql!, {
|
|
507
|
+
type: QueryTypes.SELECT,
|
|
508
|
+
replacements: countQuery.replacements,
|
|
509
|
+
transaction: opts?.transaction
|
|
510
|
+
})
|
|
511
|
+
|
|
512
|
+
const response = {
|
|
513
|
+
rows: await this.helpers.convert.prepareAll(req, entity, rows, regularQuery),
|
|
514
|
+
totals: {
|
|
515
|
+
count: totals.total !== undefined ? +totals.total : 0
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
if (augmentation?.afterGetMany && !opts?.excludeHooks?.includes('afterGetMany')) {
|
|
520
|
+
return augmentation.afterGetMany(response, req, opts)
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
return response
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
async getOne<K extends Extract<keyof T, string> = Extract<keyof T, string>>(
|
|
527
|
+
entityAlias: K,
|
|
528
|
+
req: CuboCrudRequest,
|
|
529
|
+
opts?: CuboCrudMethodOptions
|
|
530
|
+
): Promise<T[K] | undefined> {
|
|
531
|
+
opts = opts || {}
|
|
532
|
+
|
|
533
|
+
const entity = await this.getEntity(entityAlias)
|
|
534
|
+
const augmentation = this.options?.augmentations?.[entityAlias]
|
|
535
|
+
if (augmentation?.beforeGetOne) {
|
|
536
|
+
opts.queryOptions = await augmentation.beforeGetOne(req, opts)
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// get all joins
|
|
540
|
+
const queryDto = await this.helpers.withes.prepare(req, entity, opts)
|
|
541
|
+
queryDto.withDeleted = opts.queryOptions?.withDeleted
|
|
542
|
+
queryDto.withCrdt = opts.queryOptions?.withCrdt
|
|
543
|
+
queryDto.limit = 1
|
|
544
|
+
|
|
545
|
+
const query = await this.helpers.createFindQuery(req, entity, queryDto)
|
|
546
|
+
|
|
547
|
+
const db = await this.helpers.getConnection('read')
|
|
548
|
+
|
|
549
|
+
const [row] = await db.connection.query<any>(query.sql!, {
|
|
550
|
+
type: QueryTypes.SELECT,
|
|
551
|
+
replacements: query.replacements,
|
|
552
|
+
transaction: opts?.transaction
|
|
553
|
+
})
|
|
554
|
+
|
|
555
|
+
if (opts?.log) {
|
|
556
|
+
console.log(`QUERY:getOne to "${String(entityAlias)}":`, query.sql, query.replacements)
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
if (!row) {
|
|
560
|
+
return null as any
|
|
561
|
+
// throw { status: 404, message: 'Element "' + entityAlias + '" not found', details: req }
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
const response = await this.helpers.convert.prepareOne(req, entity, row, query)
|
|
565
|
+
|
|
566
|
+
if (augmentation?.afterGetOne && !opts?.excludeHooks?.includes('afterGetOne')) {
|
|
567
|
+
return augmentation.afterGetOne(response, req, opts)
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
return response
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
async createOne<K extends Extract<keyof T, string> = Extract<keyof T, string>>(
|
|
574
|
+
entityAlias: K,
|
|
575
|
+
req: CuboCrudRequest,
|
|
576
|
+
opts?: CuboCrudMethodOptions
|
|
577
|
+
): Promise<T[K]> {
|
|
578
|
+
opts = opts || {}
|
|
579
|
+
const entity = await this.helpers.getEntityByAlias(entityAlias)
|
|
580
|
+
const augmentation = this.options?.augmentations?.[entityAlias]
|
|
581
|
+
|
|
582
|
+
if (augmentation?.beforeCreate) {
|
|
583
|
+
opts.queryOptions = await augmentation.beforeCreate(req, opts)
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
const dto = await this.helpers.data.prepare('create', entity!.fields, req.body, opts?.performer_id || 0)
|
|
587
|
+
if (!Object.keys(dto)) {
|
|
588
|
+
throw { status: 400, text: 'No keys to update' }
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// const prepared = dbPrepareInsertQueryString(dto)
|
|
592
|
+
const db = await this.helpers.getConnection('write')
|
|
593
|
+
const [created_id] = await db.create(entity.alias, dto, { transaction: opts?.transaction, log: opts?.log })
|
|
594
|
+
|
|
595
|
+
if (!created_id) {
|
|
596
|
+
throw { status: 400, text: 'Unable to create entity' }
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
let response = await this.getOne<K>(entityAlias, { query: { id: created_id } }, opts)
|
|
600
|
+
|
|
601
|
+
if (!response) {
|
|
602
|
+
throw { status: 400, text: 'Unable to find entity after create' }
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
if (augmentation?.afterCreate && !opts?.excludeHooks?.includes('afterCreate')) {
|
|
606
|
+
response = await augmentation.afterCreate(response, req, opts)
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
if (this.crdt) {
|
|
610
|
+
// this.crdt.ee.emit(CUBO_CRDT_EVENT.DOCUMENT_CREATED, { name: `${entityAlias}:${(response as any).id}` })
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
return response
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
async updateOne<K extends Extract<keyof T, string> = Extract<keyof T, string>>(
|
|
617
|
+
entityAlias: K,
|
|
618
|
+
req: CuboCrudRequest,
|
|
619
|
+
opts?: CuboCrudMethodOptions
|
|
620
|
+
): Promise<T[K] | undefined> {
|
|
621
|
+
if (opts?.debounce) {
|
|
622
|
+
if (!req.query?.id) {
|
|
623
|
+
throw 'Debounce "updateOne" allowed only with "id"'
|
|
624
|
+
}
|
|
625
|
+
console.log('call debounced updateOne')
|
|
626
|
+
|
|
627
|
+
const key = `${entityAlias}:${+req.query.id}` as `${string}:${number}`
|
|
628
|
+
let ex = this.debounces.get(key)
|
|
629
|
+
if (ex) {
|
|
630
|
+
if (ex.started > +new Date() - opts.debounce) {
|
|
631
|
+
console.log('clear execution')
|
|
632
|
+
Promise.reject(ex.promise)
|
|
633
|
+
} else {
|
|
634
|
+
return ex.promise
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
return new Promise((resolve, reject) => {
|
|
639
|
+
setTimeout(() => {
|
|
640
|
+
this.updateOne(entityAlias, req, { ...(opts || {}), debounce: undefined })
|
|
641
|
+
.then(resolve)
|
|
642
|
+
.catch(reject)
|
|
643
|
+
}, opts.debounce)
|
|
644
|
+
})
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
opts = opts || {}
|
|
648
|
+
const entity = await this.helpers.getEntityByAlias(entityAlias)
|
|
649
|
+
const augmentation = this.options?.augmentations?.[entityAlias]
|
|
650
|
+
|
|
651
|
+
if (req.query?.crdt_exclude) {
|
|
652
|
+
opts = opts || {}
|
|
653
|
+
opts.crdt = {
|
|
654
|
+
exclude: req.query.crdt_exclude
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
delete req.query.crdt_exclude
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
if (augmentation?.beforeUpdate && !opts?.excludeHooks?.includes('beforeUpdate')) {
|
|
661
|
+
opts.queryOptions = await augmentation.beforeUpdate(req, opts)
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
const item: any = await this.getOne(entityAlias, req, opts)
|
|
665
|
+
|
|
666
|
+
if (!item) {
|
|
667
|
+
throw { status: 404, text: 'Entity not found' }
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
const initialEntity = cloneDeep(item)
|
|
671
|
+
|
|
672
|
+
try {
|
|
673
|
+
if (!!augmentation?.can) {
|
|
674
|
+
await augmentation.can('update', { entity, row: item, auth: opts?.extra?.auth, req })
|
|
675
|
+
}
|
|
676
|
+
} catch {
|
|
677
|
+
throw { status: 403, text: 'Updating entity forbidden' }
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
const dto = await this.helpers.data.prepare('update', entity!.fields, req.body, opts?.performer_id || 0)
|
|
681
|
+
if (!Object.keys(dto)) {
|
|
682
|
+
throw { status: 400, text: 'No keys to update' }
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
const db = await this.helpers.getConnection('write')
|
|
686
|
+
await db.update(entity.alias, 'id = :id', { id: item.id }, dto, { transaction: opts?.transaction, log: opts?.log })
|
|
687
|
+
|
|
688
|
+
let response = await this.getOne<K>(entityAlias, req, opts)
|
|
689
|
+
if (!response) {
|
|
690
|
+
throw { status: 400, text: 'Unable to find entity after update' }
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
if (augmentation?.afterUpdate && !opts?.excludeHooks?.includes('afterUpdate')) {
|
|
694
|
+
response = await augmentation.afterUpdate(response, req, opts)
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
if (this.crdt && opts?.crdt !== undefined) {
|
|
698
|
+
// console.log('on update crdt', entity.alias, opts?.crdt)
|
|
699
|
+
await this.crdtAfterEvent('update', entityAlias, response, initialEntity, opts.crdt)
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
return response
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
async deleteOne<K extends Extract<keyof T, string>>(
|
|
706
|
+
entityAlias: K,
|
|
707
|
+
req: CuboCrudRequest,
|
|
708
|
+
opts?: CuboCrudMethodOptions
|
|
709
|
+
): Promise<boolean> {
|
|
710
|
+
opts = opts || {}
|
|
711
|
+
const entity = await this.helpers.getEntityByAlias(entityAlias)
|
|
712
|
+
const augmentation = this.options?.augmentations?.[entityAlias]
|
|
713
|
+
|
|
714
|
+
if (augmentation?.beforeDelete) {
|
|
715
|
+
opts.queryOptions = await augmentation.beforeDelete(req, opts)
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
const item: any = await this.getOne(entityAlias, req, opts)
|
|
719
|
+
if (!item) {
|
|
720
|
+
throw { status: 404, text: 'Entity not found' }
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
const dto = await this.helpers.data.prepare('delete', entity!.fields, req.body || {}, opts?.performer_id)
|
|
724
|
+
if (!Object.keys(dto)) {
|
|
725
|
+
throw { status: 400, text: 'No keys to update' }
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
const db = await this.helpers.getConnection('write')
|
|
729
|
+
await db.update(entity.alias, 'id = :id', { id: item.id }, dto, { transaction: opts?.transaction, log: opts?.log })
|
|
730
|
+
|
|
731
|
+
if (augmentation?.afterDelete && !opts?.excludeHooks?.includes('afterDelete')) {
|
|
732
|
+
return augmentation.afterDelete(item, req, opts)
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
if (this.crdt) {
|
|
736
|
+
// this.crdt.ee.emit(CUBO_CRDT_EVENT.DOCUMENT_DELETED, { name: `${entityAlias}:${item.id}` })
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
return true
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
export * from './helpers'
|
|
744
|
+
export * from './types'
|