@cuboapp/api-backend 1.0.9 → 1.0.12
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 +9 -7
- package/src/constants/index.ts +2 -1
- package/src/crdt/index.ts +418 -0
- package/src/crdt/types.ts +34 -0
- package/src/helpers/convert.ts +2 -1
- package/src/helpers/data.ts +1 -0
- package/src/helpers/index.ts +38 -71
- package/src/index.ts +100 -60
- package/src/types/augmentation.ts +7 -0
- package/src/types/db.ts +4 -0
- package/src/types/index.ts +28 -0
- package/.vscode/settings.json +0 -18
- package/package-lock.json +0 -512
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cuboapp/api-backend",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.12",
|
|
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,17 @@
|
|
|
8
8
|
"license": "MIT",
|
|
9
9
|
"type": "module",
|
|
10
10
|
"dependencies": {
|
|
11
|
-
"@cuboapp/constants": "2.0.
|
|
12
|
-
"@cuboapp/database": "1.0.1",
|
|
13
|
-
"@cuboapp/types": "2.0.
|
|
11
|
+
"@cuboapp/constants": "^2.0.5",
|
|
12
|
+
"@cuboapp/database": "^1.0.1",
|
|
13
|
+
"@cuboapp/types": "^2.0.6",
|
|
14
14
|
"@cuboapp/utils": "1.0.4",
|
|
15
|
+
"@hocuspocus/extension-database": "^3.2.3",
|
|
16
|
+
"@hocuspocus/server": "^3.2.3",
|
|
15
17
|
"pg": "^8.16.3",
|
|
16
|
-
"
|
|
18
|
+
"yjs": "^13.6.27"
|
|
17
19
|
},
|
|
18
20
|
"devDependencies": {
|
|
19
|
-
"@types/node": "^
|
|
20
|
-
"typescript": "^5.
|
|
21
|
+
"@types/node": "^24.10.1",
|
|
22
|
+
"typescript": "^5.9.3"
|
|
21
23
|
}
|
|
22
24
|
}
|
package/src/constants/index.ts
CHANGED
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
import { QueryTypes } from '@cuboapp/database'
|
|
2
|
+
import { CuboApiEntitiesMap, CuboEntity } from '@cuboapp/types'
|
|
3
|
+
import { Database } from '@hocuspocus/extension-database'
|
|
4
|
+
import {
|
|
5
|
+
beforeSyncPayload,
|
|
6
|
+
Document,
|
|
7
|
+
Hocuspocus,
|
|
8
|
+
onChangePayload,
|
|
9
|
+
onLoadDocumentPayload,
|
|
10
|
+
onStoreDocumentPayload,
|
|
11
|
+
Server
|
|
12
|
+
} from '@hocuspocus/server'
|
|
13
|
+
import { IncomingHttpHeaders } from 'http'
|
|
14
|
+
import * as Y from 'yjs'
|
|
15
|
+
|
|
16
|
+
import { CuboBackendApi } from '..'
|
|
17
|
+
import { DirectConnection } from './types'
|
|
18
|
+
|
|
19
|
+
export * from './types'
|
|
20
|
+
|
|
21
|
+
export class CuboCrdt<T extends CuboApiEntitiesMap<T>> {
|
|
22
|
+
public server: Server
|
|
23
|
+
private documentPrefix = 'cubo'
|
|
24
|
+
private defaultCrdtFieldAlias = 'crdt_data'
|
|
25
|
+
|
|
26
|
+
constructor(public api: CuboBackendApi<T>) {
|
|
27
|
+
this.init()
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
private init() {
|
|
31
|
+
const { onAuthenticate, onLoadDocument, onChange, onStoreDocument, afterLoadDocument, connected, ...config } = this.api.options.crdt
|
|
32
|
+
|
|
33
|
+
this.server = new Server({
|
|
34
|
+
port: config.port,
|
|
35
|
+
extensions: [
|
|
36
|
+
new Database({
|
|
37
|
+
fetch: async ({ documentName, context, document, instance, requestHeaders, requestParameters, socketId, connectionConfig }) => {
|
|
38
|
+
try {
|
|
39
|
+
const [prefix] = documentName.split('.')
|
|
40
|
+
if (prefix !== this.documentPrefix) {
|
|
41
|
+
return null
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const { entityAlias, id } = this.parseDocumentName(documentName)
|
|
45
|
+
|
|
46
|
+
console.log('fetch', entityAlias, id)
|
|
47
|
+
if (id <= 0) {
|
|
48
|
+
return null
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const entity = await this.api.helpers.getEntityByAlias(entityAlias as keyof T)
|
|
52
|
+
if (!entity) {
|
|
53
|
+
throw new Error(`Entity not found: "${entityAlias}"`)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const isSupported = this.isCrdtSupported(entity)
|
|
57
|
+
if (!isSupported) {
|
|
58
|
+
throw new Error(`Entity "${entityAlias}" does not support CRDT`)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const fieldKey = this.getCrdtField(entity)?.alias
|
|
62
|
+
|
|
63
|
+
if (!fieldKey) {
|
|
64
|
+
return null
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const db = await this.api.helpers.getConnection('write')
|
|
68
|
+
|
|
69
|
+
const [data] = await db.connection.query(`SELECT ${fieldKey} from ${entityAlias} where id=:id`, {
|
|
70
|
+
type: QueryTypes.SELECT,
|
|
71
|
+
replacements: {
|
|
72
|
+
id
|
|
73
|
+
}
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
if (!data) {
|
|
77
|
+
throw new Error(`Entity "${entityAlias}" with id=${id} not found`)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const raw = (data as any)[fieldKey]
|
|
81
|
+
// console.log('raw', raw)
|
|
82
|
+
|
|
83
|
+
if (!raw) {
|
|
84
|
+
return null
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return raw
|
|
88
|
+
} catch (error) {
|
|
89
|
+
console.log('error', error)
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
store: async ({ documentName, document, state, context }) => {
|
|
93
|
+
const [prefix] = documentName.split('.')
|
|
94
|
+
if (prefix !== this.documentPrefix) {
|
|
95
|
+
return null
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const { entityAlias, id } = this.parseDocumentName(documentName)
|
|
99
|
+
|
|
100
|
+
if (id <= 0) {
|
|
101
|
+
return
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const entity = await this.api.helpers.getEntityByAlias(entityAlias as keyof T)
|
|
105
|
+
if (!entity) {
|
|
106
|
+
throw new Error(`Entity not found: "${entityAlias}"`)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const isSupported = this.isCrdtSupported(entity)
|
|
110
|
+
if (!isSupported) {
|
|
111
|
+
throw new Error(`Entity "${entityAlias}" does not support CRDT`)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const entityMap = document.getMap(entityAlias)
|
|
115
|
+
const body: Record<string, any> = {}
|
|
116
|
+
|
|
117
|
+
const fieldKey = this.getCrdtField(entity)?.alias
|
|
118
|
+
|
|
119
|
+
for (const [key, value] of entityMap.entries()) {
|
|
120
|
+
if (key === fieldKey) {
|
|
121
|
+
continue
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
body[key] = value
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// console.log('state', state)
|
|
128
|
+
|
|
129
|
+
body[fieldKey] = state
|
|
130
|
+
|
|
131
|
+
body.modified_at = new Date().toISOString()
|
|
132
|
+
|
|
133
|
+
await this.api.updateOne(
|
|
134
|
+
entityAlias as Extract<keyof T, string>,
|
|
135
|
+
{
|
|
136
|
+
query: { id },
|
|
137
|
+
body
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
crdt: false,
|
|
141
|
+
extra: {
|
|
142
|
+
auth: context.auth
|
|
143
|
+
},
|
|
144
|
+
excludeHooks: [],
|
|
145
|
+
queryOptions: { withDeleted: true }
|
|
146
|
+
}
|
|
147
|
+
)
|
|
148
|
+
}
|
|
149
|
+
})
|
|
150
|
+
],
|
|
151
|
+
onAuthenticate: async (data) => {
|
|
152
|
+
try {
|
|
153
|
+
return onAuthenticate?.(this.api, data)
|
|
154
|
+
} catch (err) {
|
|
155
|
+
throw new Error('Ошибка авторизации: ' + err)
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
onLoadDocument: async (data) => {
|
|
159
|
+
const { documentName } = data
|
|
160
|
+
|
|
161
|
+
const [prefix] = documentName.split('.')
|
|
162
|
+
if (prefix === this.documentPrefix) {
|
|
163
|
+
const { entityAlias, id } = this.parseDocumentName(documentName)
|
|
164
|
+
|
|
165
|
+
if (entityAlias) {
|
|
166
|
+
const augmentation = this.api.options.augmentations?.[entityAlias]
|
|
167
|
+
|
|
168
|
+
if (augmentation?.onLoadDocument) {
|
|
169
|
+
await augmentation.onLoadDocument(data)
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return onLoadDocument?.(this.api, data)
|
|
175
|
+
},
|
|
176
|
+
afterLoadDocument: async (data) => {
|
|
177
|
+
const { documentName, document, instance } = data
|
|
178
|
+
|
|
179
|
+
return afterLoadDocument?.(this.api, data)
|
|
180
|
+
},
|
|
181
|
+
connected: async (data) => {
|
|
182
|
+
const { documentName, instance } = data
|
|
183
|
+
|
|
184
|
+
const [prefix] = documentName?.split('.')
|
|
185
|
+
if (prefix === this.documentPrefix) {
|
|
186
|
+
const document = instance.documents.get(documentName)
|
|
187
|
+
|
|
188
|
+
const { entityAlias, id } = this.parseDocumentName(documentName)
|
|
189
|
+
|
|
190
|
+
if (entityAlias && document) {
|
|
191
|
+
const entityMap = document.getMap(entityAlias)
|
|
192
|
+
const id = entityMap.get('id')
|
|
193
|
+
|
|
194
|
+
if (!id) {
|
|
195
|
+
document.connections.forEach(({ connection }) => {
|
|
196
|
+
connection.close({
|
|
197
|
+
reason: 'Document not loaded',
|
|
198
|
+
code: 500
|
|
199
|
+
})
|
|
200
|
+
})
|
|
201
|
+
instance.unloadDocument(document)
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return connected?.(this.api, data)
|
|
207
|
+
},
|
|
208
|
+
onChange: async (data) => {
|
|
209
|
+
const { documentName } = data
|
|
210
|
+
|
|
211
|
+
const [prefix] = documentName.split('.')
|
|
212
|
+
if (prefix === this.documentPrefix) {
|
|
213
|
+
const { entityAlias, id } = this.parseDocumentName(documentName)
|
|
214
|
+
|
|
215
|
+
if (entityAlias) {
|
|
216
|
+
const augmentation = this.api.options.augmentations?.[entityAlias]
|
|
217
|
+
|
|
218
|
+
if (augmentation?.onChangeDocument) {
|
|
219
|
+
await augmentation.onChangeDocument(data)
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return onChange?.(this.api, data)
|
|
225
|
+
},
|
|
226
|
+
onStoreDocument: async (data) => {
|
|
227
|
+
const { documentName } = data
|
|
228
|
+
|
|
229
|
+
const [prefix] = documentName.split('.')
|
|
230
|
+
if (prefix === this.documentPrefix) {
|
|
231
|
+
const { entityAlias, id } = this.parseDocumentName(documentName)
|
|
232
|
+
|
|
233
|
+
if (entityAlias) {
|
|
234
|
+
const augmentation = this.api.options.augmentations?.[entityAlias]
|
|
235
|
+
|
|
236
|
+
if (augmentation?.onStoreDocument) {
|
|
237
|
+
await augmentation.onStoreDocument(data)
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return onStoreDocument?.(this.api, data)
|
|
243
|
+
},
|
|
244
|
+
|
|
245
|
+
...config
|
|
246
|
+
})
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
public async listen() {
|
|
250
|
+
await this.server.listen()
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
private parseDocumentName(documentName: string) {
|
|
254
|
+
const parts = documentName.split('.')
|
|
255
|
+
|
|
256
|
+
const [prefix, entityAlias, idRaw] = parts
|
|
257
|
+
|
|
258
|
+
if (prefix !== this.documentPrefix) {
|
|
259
|
+
throw new Error(`Invalid prefix: "${prefix}", expected "${this.documentPrefix}"`)
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (!entityAlias) {
|
|
263
|
+
throw new Error(`Missing entity alias in documentName: "${documentName}"`)
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const id = Number(idRaw)
|
|
267
|
+
if (Number.isNaN(id)) {
|
|
268
|
+
throw new Error(`Invalid id in documentName: "${documentName}"`)
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return { entityAlias: entityAlias as Extract<keyof T, string>, id }
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
public createDocumentName(entityAlias: string, id: number | string, prefix?: string) {
|
|
275
|
+
const parts = [prefix || this.documentPrefix, entityAlias, id]
|
|
276
|
+
|
|
277
|
+
return parts.join('.')
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
public isCrdtSupported(entity: CuboEntity) {
|
|
281
|
+
const hasCrdtField = entity.fields.some((f) => f.alias === this.api.options.crdt?.fieldAlias || this.defaultCrdtFieldAlias)
|
|
282
|
+
|
|
283
|
+
return hasCrdtField
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
public getCrdtField(entity: CuboEntity) {
|
|
287
|
+
const crdtField = entity.fields.find((f) => f.alias === this.api.options.crdt?.fieldAlias || this.defaultCrdtFieldAlias)
|
|
288
|
+
|
|
289
|
+
return crdtField
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
public getDocumentByEntity(entityAlias: string, id: number, requestHeaders: IncomingHttpHeaders) {
|
|
293
|
+
const documentName = this.createDocumentName(entityAlias, id)
|
|
294
|
+
const fakeSocketId = 'api-' + crypto.randomUUID()
|
|
295
|
+
|
|
296
|
+
return this.server.hocuspocus.createDocument(documentName, { headers: requestHeaders, url: '/collaboration' }, fakeSocketId, {
|
|
297
|
+
readOnly: false,
|
|
298
|
+
isAuthenticated: false
|
|
299
|
+
})
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
public async openDirectConnection(entityAlias: string, id: number, context?: any) {
|
|
303
|
+
const documentName = this.createDocumentName(entityAlias, id)
|
|
304
|
+
if (!documentName) {
|
|
305
|
+
return
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return this.server.hocuspocus.openDirectConnection(documentName, context)
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
public async connectionDisconnect(connection: DirectConnection) {
|
|
312
|
+
const { document, instance, context } = connection
|
|
313
|
+
|
|
314
|
+
if (document) {
|
|
315
|
+
document.removeDirectConnection()
|
|
316
|
+
|
|
317
|
+
await instance.storeDocumentHooks(document, {
|
|
318
|
+
clientsCount: document.getConnectionsCount(),
|
|
319
|
+
context: context,
|
|
320
|
+
document: document,
|
|
321
|
+
documentName: document.name,
|
|
322
|
+
instance: instance,
|
|
323
|
+
requestHeaders: {},
|
|
324
|
+
requestParameters: new URLSearchParams(),
|
|
325
|
+
socketId: 'server'
|
|
326
|
+
})
|
|
327
|
+
|
|
328
|
+
// // If the direct connection was the only connection to the document
|
|
329
|
+
// // then we should trigger the onDisconnect hook for
|
|
330
|
+
// // this doc and unload the document
|
|
331
|
+
// if (document.getConnectionsCount() === 0) {
|
|
332
|
+
// await instance.hooks('onDisconnect', {
|
|
333
|
+
// instance: instance,
|
|
334
|
+
// clientsCount: document.getConnectionsCount(),
|
|
335
|
+
// context: context,
|
|
336
|
+
// document: document,
|
|
337
|
+
// socketId: 'server',
|
|
338
|
+
// documentName: document.name,
|
|
339
|
+
// requestHeaders: {},
|
|
340
|
+
// requestParameters: new URLSearchParams()
|
|
341
|
+
// })
|
|
342
|
+
|
|
343
|
+
// await instance.unloadDocument(document)
|
|
344
|
+
// }
|
|
345
|
+
|
|
346
|
+
connection.document = null
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
public async updateDocument(params: { entityAlias: string; id: number; entity: CuboEntity }, dto: Record<string, any>, context?: any) {
|
|
351
|
+
const { entityAlias, id, entity } = params
|
|
352
|
+
|
|
353
|
+
const connection = await this.openDirectConnection(entityAlias, id, context)
|
|
354
|
+
if (!connection) {
|
|
355
|
+
return
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const { document } = connection
|
|
359
|
+
if (!document) {
|
|
360
|
+
return
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const entityMap = document.getMap(entityAlias)
|
|
364
|
+
|
|
365
|
+
const fieldKey = this.getCrdtField(entity)?.alias
|
|
366
|
+
|
|
367
|
+
document.transact(() => {
|
|
368
|
+
for (const [key, value] of Object.entries(dto)) {
|
|
369
|
+
if (key === fieldKey) {
|
|
370
|
+
continue
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
let preparedValue = value
|
|
374
|
+
|
|
375
|
+
if (value instanceof Date) {
|
|
376
|
+
preparedValue = value.toISOString()
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
entityMap.set(key, preparedValue)
|
|
380
|
+
}
|
|
381
|
+
})
|
|
382
|
+
|
|
383
|
+
return this.connectionDisconnect(connection)
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
public async createState(params: { entityAlias: string; id: number; entity: CuboEntity }, dto: Record<string, any>) {
|
|
387
|
+
const { entityAlias, id, entity } = params
|
|
388
|
+
|
|
389
|
+
const document = new Y.Doc()
|
|
390
|
+
if (!document) {
|
|
391
|
+
return
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const entityMap = document.getMap(entityAlias)
|
|
395
|
+
|
|
396
|
+
const fieldKey = this.getCrdtField(entity)?.alias
|
|
397
|
+
|
|
398
|
+
document.transact(() => {
|
|
399
|
+
for (const [key, value] of Object.entries(dto)) {
|
|
400
|
+
if (key === fieldKey) {
|
|
401
|
+
continue
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
let preparedValue = value
|
|
405
|
+
|
|
406
|
+
if (value instanceof Date) {
|
|
407
|
+
preparedValue = value.toISOString()
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
entityMap.set(key, preparedValue)
|
|
411
|
+
}
|
|
412
|
+
})
|
|
413
|
+
|
|
414
|
+
return Buffer.from(Y.encodeStateAsUpdate(document))
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
export { beforeSyncPayload, DirectConnection, Document, Hocuspocus, onChangePayload, onLoadDocumentPayload, onStoreDocumentPayload }
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { CuboApiEntitiesMap } from '@cuboapp/types'
|
|
2
|
+
import {
|
|
3
|
+
DirectConnection as DirectConnectionInterface,
|
|
4
|
+
Document,
|
|
5
|
+
Hocuspocus,
|
|
6
|
+
ServerConfiguration,
|
|
7
|
+
afterLoadDocumentPayload,
|
|
8
|
+
connectedPayload,
|
|
9
|
+
onAuthenticatePayload,
|
|
10
|
+
onChangePayload,
|
|
11
|
+
onLoadDocumentPayload,
|
|
12
|
+
onStoreDocumentPayload
|
|
13
|
+
} from '@hocuspocus/server'
|
|
14
|
+
|
|
15
|
+
import { CuboBackendApi } from '..'
|
|
16
|
+
|
|
17
|
+
export type CuboCrdtConfiguration<T extends CuboApiEntitiesMap<T>> = Omit<
|
|
18
|
+
Partial<ServerConfiguration>,
|
|
19
|
+
'onAuthenticate' | 'onLoadDocument' | 'onChange' | 'onStoreDocument' | 'afterLoadDocument' | 'connected'
|
|
20
|
+
> & {
|
|
21
|
+
onAuthenticate?: (api: CuboBackendApi<T>, data: onAuthenticatePayload) => Promise<any>
|
|
22
|
+
onLoadDocument?: (api: CuboBackendApi<T>, data: onLoadDocumentPayload) => Promise<any>
|
|
23
|
+
onChange?: (api: CuboBackendApi<T>, data: onChangePayload) => Promise<any>
|
|
24
|
+
onStoreDocument?: (api: CuboBackendApi<T>, data: onStoreDocumentPayload) => Promise<any>
|
|
25
|
+
afterLoadDocument?: (api: CuboBackendApi<T>, data: afterLoadDocumentPayload) => Promise<any>
|
|
26
|
+
connected?: (api: CuboBackendApi<T>, data: connectedPayload) => Promise<any>
|
|
27
|
+
fieldAlias?: string
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export type DirectConnection = {
|
|
31
|
+
document: Document | null
|
|
32
|
+
instance: Hocuspocus
|
|
33
|
+
context: any
|
|
34
|
+
} & DirectConnectionInterface
|
package/src/helpers/convert.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { CuboApiEntitiesMap, CuboEntity, CuboEntityField } from '@cuboapp/types'
|
|
2
2
|
import { get, set, unset } from '@cuboapp/utils'
|
|
3
|
+
import { CUBO_ENTITY_FIELD_TYPE } from '@cuboapp/constants'
|
|
3
4
|
|
|
4
5
|
import { CUBO_CRUD_DEFAULT_FIELDS } from '../constants'
|
|
5
6
|
import { CuboCrudFindQuery, CuboCrudRequest } from '../types'
|
|
6
7
|
|
|
7
|
-
import { CUBO_ENTITY_FIELD_TYPE } from '@cuboapp/constants'
|
|
8
8
|
import { ApiHelpers } from '.'
|
|
9
9
|
|
|
10
10
|
export class ApiHelpersConvert<T extends CuboApiEntitiesMap<T>> {
|
|
@@ -98,6 +98,7 @@ export class ApiHelpersConvert<T extends CuboApiEntitiesMap<T>> {
|
|
|
98
98
|
console.warn('field type "file" is not implemented yet')
|
|
99
99
|
return undefined
|
|
100
100
|
case CUBO_ENTITY_FIELD_TYPE.JSON:
|
|
101
|
+
case CUBO_ENTITY_FIELD_TYPE.BINARY:
|
|
101
102
|
return value
|
|
102
103
|
case CUBO_ENTITY_FIELD_TYPE.NUMBER:
|
|
103
104
|
return ![undefined, null].includes(value) ? parseFloat(value) : null
|
package/src/helpers/data.ts
CHANGED
|
@@ -128,6 +128,7 @@ export class ApiHelpersData<T extends CuboApiEntitiesMap<T>> {
|
|
|
128
128
|
errors.push(`"${key}": file field type is not already implemented`)
|
|
129
129
|
break
|
|
130
130
|
case CUBO_ENTITY_FIELD_TYPE.JSON:
|
|
131
|
+
case CUBO_ENTITY_FIELD_TYPE.BINARY:
|
|
131
132
|
if (value === null) {
|
|
132
133
|
prepared[key] = value
|
|
133
134
|
} else if (Buffer.byteLength(JSON.stringify(value)) >= 65535 * 50) {
|
package/src/helpers/index.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { CUBO_ENTITY_FIELD_TYPE } from '@cuboapp/constants'
|
|
2
|
-
import { Database, createDatabase
|
|
2
|
+
import { Database, createDatabase } from '@cuboapp/database'
|
|
3
3
|
import { CuboApiEntitiesMap, CuboEntity, CuboEntityField, CuboEntityFieldExtra, CuboEntityFieldType } from '@cuboapp/types'
|
|
4
4
|
|
|
5
|
-
import { CuboBackendApi
|
|
5
|
+
import { CuboBackendApi } from '../..'
|
|
6
6
|
import {
|
|
7
7
|
CUBO_CRUD_DEFAULT_FIELDS,
|
|
8
8
|
CUBO_CRUD_DEFAULT_PAGE_LIMIT,
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
CUBO_CRUD_QUERY_KEY_REGEX,
|
|
12
12
|
CUBO_CRUD_QUERY_SYMBOL_NOT
|
|
13
13
|
} from '../constants'
|
|
14
|
+
import { CuboBackendApiDbConnectionType, CuboCrudFindQuery, CuboCrudRequest } from '../types'
|
|
14
15
|
|
|
15
16
|
import { ApiHelpersConvert } from './convert'
|
|
16
17
|
import { ApiHelpersData } from './data'
|
|
@@ -65,7 +66,10 @@ export class ApiHelpers<T extends CuboApiEntitiesMap<T>> {
|
|
|
65
66
|
// condition parts
|
|
66
67
|
switch (parts.length) {
|
|
67
68
|
case 3:
|
|
68
|
-
if (parts[0]
|
|
69
|
+
if (parts[0] === 'btw') {
|
|
70
|
+
condition = parts[0]
|
|
71
|
+
value = `${parts[1]}:${parts[2]}`
|
|
72
|
+
} else if (parts[0] !== CUBO_CRUD_QUERY_SYMBOL_NOT) {
|
|
69
73
|
throw new Error(`"${field.alias}": three-level value allow only "not" condition at first level (${value})`)
|
|
70
74
|
} else if (!Object.values(CUBO_CRUD_QUERY_CONDITION).includes(parts[1])) {
|
|
71
75
|
throw new Error(`"${field.alias}": incorrect condition "${parts[1]}" in second level (${value})`)
|
|
@@ -140,9 +144,18 @@ export class ApiHelpers<T extends CuboApiEntitiesMap<T>> {
|
|
|
140
144
|
} else {
|
|
141
145
|
if ([CUBO_CRUD_QUERY_CONDITION.IN, CUBO_CRUD_QUERY_CONDITION.NIN].includes(condition)) {
|
|
142
146
|
value = value.split(',').map(Number)
|
|
147
|
+
} else if (condition === CUBO_CRUD_QUERY_CONDITION.BTW) {
|
|
148
|
+
const [fromStr, toStr] = value.split(':')
|
|
149
|
+
const from = precision === 0 ? parseInt(fromStr) : parseFloat(fromStr)
|
|
150
|
+
const to = precision === 0 ? parseInt(toStr) : parseFloat(toStr)
|
|
151
|
+
|
|
152
|
+
if (isNaN(from) || isNaN(to)) {
|
|
153
|
+
throw new Error(`"${field.alias}": both values for "btw" must be valid numbers`)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
value = precision > 0 ? `${from.toFixed(precision)}:${to.toFixed(precision)}` : `${from}:${to}`
|
|
143
157
|
} else {
|
|
144
158
|
const potentialValue = precision === 0 ? parseInt(value) : parseFloat(value)
|
|
145
|
-
|
|
146
159
|
if (isNaN(potentialValue)) {
|
|
147
160
|
throw new Error('"' + field.alias + '" must be number')
|
|
148
161
|
} else {
|
|
@@ -211,7 +224,7 @@ export class ApiHelpers<T extends CuboApiEntitiesMap<T>> {
|
|
|
211
224
|
): { conditions: string[]; replacements: Record<string, any> } {
|
|
212
225
|
const conditions: string[] = []
|
|
213
226
|
const replacements: Record<string, any> = {}
|
|
214
|
-
|
|
227
|
+
// console.log(field, condition, value)
|
|
215
228
|
switch (condition) {
|
|
216
229
|
case CUBO_CRUD_QUERY_CONDITION.ILIKE:
|
|
217
230
|
conditions.push(`${field} ${is_not ? 'not ilike' : 'ilike'} :${replacement}`)
|
|
@@ -253,6 +266,22 @@ export class ApiHelpers<T extends CuboApiEntitiesMap<T>> {
|
|
|
253
266
|
conditions.push(`${field} ${is_not ? '>=' : '<='} :${replacement}`)
|
|
254
267
|
replacements[replacement] = value
|
|
255
268
|
break
|
|
269
|
+
case CUBO_CRUD_QUERY_CONDITION.BTW: {
|
|
270
|
+
const [from, to] = String(value).split(':')
|
|
271
|
+
const fromKey = `${replacement}_from`
|
|
272
|
+
const toKey = `${replacement}_to`
|
|
273
|
+
|
|
274
|
+
if (!from || !to) {
|
|
275
|
+
throw new Error(`Invalid "btw" value: "${value}"`)
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const expr = `${field} >= :${fromKey} AND ${field} <= :${toKey}`
|
|
279
|
+
conditions.push(is_not ? `NOT (${expr})` : `(${expr})`)
|
|
280
|
+
|
|
281
|
+
replacements[fromKey] = from
|
|
282
|
+
replacements[toKey] = to
|
|
283
|
+
break
|
|
284
|
+
}
|
|
256
285
|
case CUBO_CRUD_QUERY_CONDITION.EQ:
|
|
257
286
|
default:
|
|
258
287
|
if (value === null) {
|
|
@@ -287,75 +316,13 @@ export class ApiHelpers<T extends CuboApiEntitiesMap<T>> {
|
|
|
287
316
|
}
|
|
288
317
|
|
|
289
318
|
this.connecting[type] = new Promise(async (success) => {
|
|
290
|
-
|
|
291
|
-
let host: string = ''
|
|
292
|
-
let port: number = 0
|
|
293
|
-
let username: string = ''
|
|
294
|
-
let password: string = ''
|
|
295
|
-
let schema: string = ''
|
|
296
|
-
let dialect: string = ''
|
|
297
|
-
let dialectModule: any = undefined
|
|
298
|
-
|
|
299
|
-
if (!this.api.options.db.options) {
|
|
300
|
-
throw 'DB connection is not configured'
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
const dbConfig = this.api.options.db.options
|
|
304
|
-
|
|
305
|
-
let cfg: Options = {}
|
|
306
|
-
if (dbConfig) {
|
|
307
|
-
if (dbConfig[type]) {
|
|
308
|
-
cfg = dbConfig[type]
|
|
309
|
-
} else {
|
|
310
|
-
cfg = dbConfig as Options
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
database = cfg.database as string
|
|
315
|
-
dialectModule = cfg.dialectModule
|
|
316
|
-
host = cfg.host as string
|
|
317
|
-
port = cfg.port as number
|
|
318
|
-
username = cfg.username as string
|
|
319
|
-
password = cfg.password as string
|
|
320
|
-
schema = cfg.schema as string
|
|
321
|
-
dialect = cfg.dialect as string
|
|
322
|
-
|
|
323
|
-
const config: Options = {
|
|
324
|
-
host,
|
|
325
|
-
port,
|
|
326
|
-
username,
|
|
327
|
-
password,
|
|
328
|
-
database,
|
|
329
|
-
dialect: dialect as any,
|
|
330
|
-
timezone: '+03:00',
|
|
331
|
-
logging: false,
|
|
332
|
-
schema
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
if (dialectModule) {
|
|
336
|
-
config.dialectModule = dialectModule
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
// cert for yandex cloud
|
|
340
|
-
if (/yandexcloud/.test(config.host!)) {
|
|
341
|
-
config.dialectOptions = {
|
|
342
|
-
...(config.dialectOptions || {}),
|
|
343
|
-
...{
|
|
344
|
-
ssl: {
|
|
345
|
-
ca: await fetch('https://storage.yandexcloud.net/cloud-certs/CA.pem').then((res) => res.text()),
|
|
346
|
-
rejectUnauthorized: true,
|
|
347
|
-
target_session_attrs: 'read-write',
|
|
348
|
-
require: true
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
}
|
|
319
|
+
const { iaas, pk_key, pk_type, ...config } = this.api.options.db.options
|
|
353
320
|
|
|
354
|
-
this.connections[type] = createDatabase(config)
|
|
321
|
+
this.connections[type] = createDatabase(config, JSON.parse(JSON.stringify({ iaas, pk_key, pk_type })))
|
|
355
322
|
await this.connections[type].connect()
|
|
356
323
|
|
|
357
|
-
if (dialect === 'postgres') {
|
|
358
|
-
await this.connections[type].connection.query(`SET schema '${schema}';`)
|
|
324
|
+
if (config.dialect === 'postgres' && config.schema) {
|
|
325
|
+
await this.connections[type].connection.query(`SET schema '${config.schema}';`)
|
|
359
326
|
}
|
|
360
327
|
|
|
361
328
|
success(this.connections[type])
|