@cuboapp/api-backend 1.0.11 → 1.0.13
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 -5
- 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 +102 -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 -104
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cuboapp/api-backend",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.13",
|
|
4
4
|
"description": "Backend Api for CuboApp",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"repository": "git@github.com:cuboapp/api-backend.git",
|
|
@@ -8,10 +8,14 @@
|
|
|
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.
|
|
14
|
-
"@cuboapp/utils": "1.0.4"
|
|
11
|
+
"@cuboapp/constants": "^2.0.5",
|
|
12
|
+
"@cuboapp/database": "^1.0.1",
|
|
13
|
+
"@cuboapp/types": "^2.0.6",
|
|
14
|
+
"@cuboapp/utils": "1.0.4",
|
|
15
|
+
"@hocuspocus/extension-database": "^3.2.3",
|
|
16
|
+
"@hocuspocus/server": "^3.2.3",
|
|
17
|
+
"pg": "^8.16.3",
|
|
18
|
+
"yjs": "^13.6.27"
|
|
15
19
|
},
|
|
16
20
|
"devDependencies": {
|
|
17
21
|
"@types/node": "^24.10.1",
|
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])
|
package/src/index.ts
CHANGED
|
@@ -1,56 +1,50 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { CuboApiEntitiesMap
|
|
1
|
+
import { QueryTypes } from '@cuboapp/database'
|
|
2
|
+
import { CuboApiEntitiesMap } from '@cuboapp/types'
|
|
3
3
|
|
|
4
|
+
import { CuboCrdt } from './crdt'
|
|
4
5
|
import { ApiHelpers } from './helpers'
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
export * from './types'
|
|
8
|
-
|
|
9
|
-
export type CuboBackendApiOptions<T extends CuboApiEntitiesMap<T>> = {
|
|
10
|
-
api: {
|
|
11
|
-
base_url: string
|
|
12
|
-
token: string
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
db: {
|
|
16
|
-
connection?: Database
|
|
17
|
-
options?: Options
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
entities: CuboEntity[]
|
|
21
|
-
|
|
22
|
-
auth?: {
|
|
23
|
-
base_url?: string
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
augmentations?: Partial<CuboCrudAugmentationsStore<T>>
|
|
27
|
-
}
|
|
6
|
+
import { CuboBackendApiAuth, CuboBackendApiOptions, CuboCrudGetManyResponse, CuboCrudMethodOptions, CuboCrudRequest } from './types'
|
|
28
7
|
|
|
29
8
|
export class CuboBackendApi<T extends CuboApiEntitiesMap<T>> {
|
|
30
|
-
constructor(public options: CuboBackendApiOptions<T>) {
|
|
9
|
+
constructor(public options: CuboBackendApiOptions<T>) {
|
|
10
|
+
if (options.crdt) {
|
|
11
|
+
this.crdt = new CuboCrdt<T>(this)
|
|
12
|
+
this.crdt.listen()
|
|
13
|
+
}
|
|
14
|
+
}
|
|
31
15
|
|
|
16
|
+
public auth: CuboBackendApiAuth
|
|
17
|
+
public crdt?: CuboCrdt<T>
|
|
32
18
|
public helpers = new ApiHelpers<T>(this)
|
|
33
19
|
|
|
34
20
|
async init() {
|
|
35
|
-
|
|
21
|
+
try {
|
|
22
|
+
this.auth = await this.request<{ variables: Record<string, any> }>('/auth/me?with=variables', {
|
|
23
|
+
headers: this.options.api?.headers || {}
|
|
24
|
+
})
|
|
36
25
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
}
|
|
26
|
+
if (!this.auth) {
|
|
27
|
+
throw { code: 403, text: 'Авторизация Cubo-Backend не пройдена' }
|
|
28
|
+
}
|
|
41
29
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
}
|
|
30
|
+
if (this.auth.variables.db !== undefined) {
|
|
31
|
+
this.options.db = this.options.db || {}
|
|
45
32
|
|
|
46
|
-
|
|
47
|
-
|
|
33
|
+
this.options.db.options = {
|
|
34
|
+
...(this.options.db.options || {}),
|
|
35
|
+
...(this.auth.variables.db || {})
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
} catch (e) {
|
|
39
|
+
console.error('[API-BACKEND] startup error', e)
|
|
40
|
+
}
|
|
48
41
|
}
|
|
49
42
|
|
|
50
|
-
|
|
43
|
+
public async request<T>(url: string, opts?: RequestInit & { debug?: boolean; withoutContentType?: boolean }) {
|
|
44
|
+
const baseUrl = this.options?.api?.base_url || 'https://api.cubo.sh'
|
|
45
|
+
|
|
51
46
|
const headers: Record<string, any> = {
|
|
52
|
-
...(opts?.headers || {})
|
|
53
|
-
Authorization: this.options.api.token || ''
|
|
47
|
+
...(opts?.headers || {})
|
|
54
48
|
}
|
|
55
49
|
|
|
56
50
|
if (!headers['Content-Type'] && !opts?.withoutContentType) {
|
|
@@ -58,33 +52,57 @@ export class CuboBackendApi<T extends CuboApiEntitiesMap<T>> {
|
|
|
58
52
|
headers['Content-Type'] = 'application/json'
|
|
59
53
|
}
|
|
60
54
|
|
|
61
|
-
const
|
|
55
|
+
const requestUrl = baseUrl.replace(/\/$/, '') + '/' + url.replace(/^\//, '')
|
|
56
|
+
const requestOpts = {
|
|
62
57
|
...(opts || {}),
|
|
63
58
|
headers
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
if ([201, 200].includes(response.status)) {
|
|
67
|
-
try {
|
|
68
|
-
const json = await response.json()
|
|
59
|
+
}
|
|
69
60
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
const text = await response.text()
|
|
61
|
+
try {
|
|
62
|
+
const response = await fetch(requestUrl, requestOpts)
|
|
73
63
|
|
|
64
|
+
if ([201, 200].includes(response.status)) {
|
|
74
65
|
try {
|
|
75
|
-
const json =
|
|
66
|
+
const json = await response.json()
|
|
76
67
|
|
|
77
68
|
return json as Promise<T>
|
|
78
|
-
} catch {
|
|
79
|
-
|
|
69
|
+
} catch (e) {
|
|
70
|
+
const text = await response.text()
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
const json = text ? JSON.parse(text) : null
|
|
74
|
+
|
|
75
|
+
return json as Promise<T>
|
|
76
|
+
} catch {
|
|
77
|
+
throw { status: 500, error: 'Invalid response', text }
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
} else {
|
|
81
|
+
throw {
|
|
82
|
+
status: response.status,
|
|
83
|
+
error: response.statusText,
|
|
84
|
+
text: await response.text()
|
|
80
85
|
}
|
|
81
86
|
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
+
} catch (e) {
|
|
88
|
+
if (opts?.debug) {
|
|
89
|
+
console.log(
|
|
90
|
+
'[API BACKEND]',
|
|
91
|
+
JSON.stringify(
|
|
92
|
+
{
|
|
93
|
+
request: {
|
|
94
|
+
url: requestUrl,
|
|
95
|
+
opts: requestOpts
|
|
96
|
+
},
|
|
97
|
+
error: e
|
|
98
|
+
},
|
|
99
|
+
null,
|
|
100
|
+
2
|
|
101
|
+
)
|
|
102
|
+
)
|
|
87
103
|
}
|
|
104
|
+
|
|
105
|
+
throw e
|
|
88
106
|
}
|
|
89
107
|
}
|
|
90
108
|
|
|
@@ -222,14 +240,27 @@ export class CuboBackendApi<T extends CuboApiEntitiesMap<T>> {
|
|
|
222
240
|
throw { status: 400, text: 'Unable to create entity' }
|
|
223
241
|
}
|
|
224
242
|
|
|
225
|
-
|
|
243
|
+
let response = await this.getOne<K>(entityAlias, { query: { id: created_id } }, opts)
|
|
226
244
|
|
|
227
245
|
if (!response) {
|
|
228
246
|
throw { status: 400, text: 'Unable to find entity after create' }
|
|
229
247
|
}
|
|
230
248
|
|
|
231
249
|
if (augmentation?.afterCreate && !opts?.excludeHooks?.includes('afterCreate')) {
|
|
232
|
-
|
|
250
|
+
response = await augmentation.afterCreate(response, req, opts)
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (opts.crdt !== false && this.crdt.isCrdtSupported(entity)) {
|
|
254
|
+
const state = await this.crdt.createState({ entityAlias, id: created_id, entity }, response)
|
|
255
|
+
|
|
256
|
+
const fieldKey = entity.fields.find((f) => f.type === 'binary').alias
|
|
257
|
+
|
|
258
|
+
const db = await this.helpers.getConnection('read')
|
|
259
|
+
await db.update(entity!.alias, 'id=:id', { id: created_id }, { [fieldKey]: state }, opts)
|
|
260
|
+
|
|
261
|
+
if (augmentation?.afterCrdtCreate && !opts?.excludeHooks?.includes('afterCrdtCreate')) {
|
|
262
|
+
augmentation.afterCrdtCreate(response, req, opts)
|
|
263
|
+
}
|
|
233
264
|
}
|
|
234
265
|
|
|
235
266
|
return response
|
|
@@ -262,13 +293,22 @@ export class CuboBackendApi<T extends CuboApiEntitiesMap<T>> {
|
|
|
262
293
|
const db = await this.helpers.getConnection('write')
|
|
263
294
|
await db.update(entity.alias, 'id = :id', { id: item.id }, dto, { transaction: opts?.transaction, log: opts?.log })
|
|
264
295
|
|
|
265
|
-
|
|
296
|
+
let response = await this.getOne<K>(entityAlias, req, opts)
|
|
266
297
|
if (!response) {
|
|
267
298
|
throw { status: 400, text: 'Unable to find entity after update' }
|
|
268
299
|
}
|
|
269
300
|
|
|
270
301
|
if (augmentation?.afterUpdate && !opts?.excludeHooks?.includes('afterUpdate')) {
|
|
271
|
-
|
|
302
|
+
response = await augmentation.afterUpdate(response, req, opts)
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (opts.crdt !== false && this.crdt.isCrdtSupported(entity)) {
|
|
306
|
+
const context = { auth: opts?.extra?.auth }
|
|
307
|
+
await this.crdt.updateDocument({ entityAlias, id: item.id, entity }, response, context)
|
|
308
|
+
|
|
309
|
+
if (augmentation?.afterCrdtUpdate && !opts?.excludeHooks?.includes('afterCrdtUpdate')) {
|
|
310
|
+
augmentation.afterCrdtUpdate(response, req, opts)
|
|
311
|
+
}
|
|
272
312
|
}
|
|
273
313
|
|
|
274
314
|
return response
|
|
@@ -308,4 +348,6 @@ export class CuboBackendApi<T extends CuboApiEntitiesMap<T>> {
|
|
|
308
348
|
}
|
|
309
349
|
}
|
|
310
350
|
|
|
351
|
+
export * from './crdt'
|
|
311
352
|
export * from './helpers'
|
|
353
|
+
export * from './types'
|
|
@@ -2,6 +2,7 @@ import { CuboApiEntitiesMap } from '@cuboapp/types'
|
|
|
2
2
|
|
|
3
3
|
import { CuboCrudGetManyResponse, CuboCrudRequest } from './basic'
|
|
4
4
|
import { CuboCrudMethodOptions, CuboCrudQueryOptions } from './db'
|
|
5
|
+
import { onChangePayload, onLoadDocumentPayload, onStoreDocumentPayload } from '@hocuspocus/server'
|
|
5
6
|
|
|
6
7
|
export type CuboCrudAugmentationsStore<T extends CuboApiEntitiesMap<T>> = {
|
|
7
8
|
[K in Extract<keyof T, string>]?: CuboCrudAugmentationInstance<T[K]>
|
|
@@ -20,10 +21,16 @@ export interface CuboCrudAugmentationInstance<T = unknown> {
|
|
|
20
21
|
|
|
21
22
|
beforeCreate?: (req: CuboCrudRequest, opts?: CuboCrudMethodOptions) => Promise<CuboCrudQueryOptions>
|
|
22
23
|
afterCreate?: (result: T, req: CuboCrudRequest, opts?: CuboCrudMethodOptions) => Promise<T>
|
|
24
|
+
afterCrdtCreate?: (result: T, req: CuboCrudRequest, opts?: CuboCrudMethodOptions) => Promise<void>
|
|
23
25
|
|
|
24
26
|
beforeUpdate?: (req: CuboCrudRequest, opts?: CuboCrudMethodOptions) => Promise<CuboCrudQueryOptions>
|
|
25
27
|
afterUpdate?: (result: T, req: CuboCrudRequest, opts?: CuboCrudMethodOptions) => Promise<T>
|
|
28
|
+
afterCrdtUpdate?: (result: T, req: CuboCrudRequest, opts?: CuboCrudMethodOptions) => Promise<void>
|
|
26
29
|
|
|
27
30
|
beforeDelete?: (req: CuboCrudRequest, opts?: CuboCrudMethodOptions) => Promise<CuboCrudQueryOptions>
|
|
28
31
|
afterDelete?: (result: T, req: CuboCrudRequest, opts?: CuboCrudMethodOptions) => Promise<boolean>
|
|
32
|
+
|
|
33
|
+
onLoadDocument?: (data: onLoadDocumentPayload) => Promise<void>
|
|
34
|
+
onChangeDocument?: (data: onChangePayload) => Promise<void>
|
|
35
|
+
onStoreDocument?: (data: onStoreDocumentPayload) => Promise<void>
|
|
29
36
|
}
|
package/src/types/db.ts
CHANGED
|
@@ -27,9 +27,13 @@ export type CuboCrudMethodOptions<E extends object = {}> = {
|
|
|
27
27
|
| 'afterGetMany'
|
|
28
28
|
| 'afterGetOne'
|
|
29
29
|
| 'beforeUpdate'
|
|
30
|
+
| 'afterCrdtCreate'
|
|
31
|
+
| 'afterCrdtUpdate'
|
|
30
32
|
)[]
|
|
31
33
|
|
|
32
34
|
log?: boolean
|
|
35
|
+
replace?: boolean
|
|
36
|
+
crdt?: boolean
|
|
33
37
|
|
|
34
38
|
queryOptions?: CuboCrudQueryOptions
|
|
35
39
|
} & E
|
package/src/types/index.ts
CHANGED
|
@@ -1,3 +1,31 @@
|
|
|
1
|
+
import { Database, DatabaseOptions, Options } from '@cuboapp/database'
|
|
2
|
+
import { CuboApiEntitiesMap, CuboAuthTokenType, CuboEntity } from '@cuboapp/types'
|
|
3
|
+
|
|
4
|
+
import { CuboCrdtConfiguration } from '../crdt'
|
|
5
|
+
|
|
6
|
+
import { CuboCrudAugmentationsStore } from './augmentation'
|
|
7
|
+
|
|
1
8
|
export * from './augmentation'
|
|
2
9
|
export * from './basic'
|
|
3
10
|
export * from './db'
|
|
11
|
+
|
|
12
|
+
export type CuboBackendApiOptions<T extends CuboApiEntitiesMap<T>> = {
|
|
13
|
+
api: {
|
|
14
|
+
base_url?: string
|
|
15
|
+
headers: Partial<Record<`cubo-${CuboAuthTokenType}`, string>> & Record<string, any>
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
entities?: CuboEntity[]
|
|
19
|
+
|
|
20
|
+
db?: {
|
|
21
|
+
connection?: Database
|
|
22
|
+
options?: Options & DatabaseOptions
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
crdt?: CuboCrdtConfiguration<T>
|
|
26
|
+
augmentations?: Partial<CuboCrudAugmentationsStore<T>>
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type CuboBackendApiAuth = {
|
|
30
|
+
variables: Record<string, any>
|
|
31
|
+
}
|
package/.vscode/settings.json
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"editor.minimap.enabled": false,
|
|
3
|
-
"editor.formatOnSave": true,
|
|
4
|
-
"editor.codeActionsOnSave": {
|
|
5
|
-
"source.fixAll.eslint": "explicit",
|
|
6
|
-
"source.fixAll.stylelint": "explicit",
|
|
7
|
-
"source.organizeImports": "explicit",
|
|
8
|
-
"source.organizeExports": "explicit"
|
|
9
|
-
},
|
|
10
|
-
"emmet.includeLanguages": {
|
|
11
|
-
"javascriptreact": "html",
|
|
12
|
-
"typescriptreact": "html"
|
|
13
|
-
},
|
|
14
|
-
"files.exclude": {
|
|
15
|
-
"node_modules/": true
|
|
16
|
-
},
|
|
17
|
-
"typescript.tsdk": "node_modules/typescript/lib"
|
|
18
|
-
}
|
package/package-lock.json
DELETED
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@cuboapp/api-backend",
|
|
3
|
-
"version": "1.0.10",
|
|
4
|
-
"lockfileVersion": 3,
|
|
5
|
-
"requires": true,
|
|
6
|
-
"packages": {
|
|
7
|
-
"": {
|
|
8
|
-
"name": "@cuboapp/api-backend",
|
|
9
|
-
"version": "1.0.10",
|
|
10
|
-
"license": "MIT",
|
|
11
|
-
"dependencies": {
|
|
12
|
-
"@cuboapp/constants": "2.0.4",
|
|
13
|
-
"@cuboapp/database": "1.0.1",
|
|
14
|
-
"@cuboapp/types": "2.0.5",
|
|
15
|
-
"@cuboapp/utils": "1.0.4"
|
|
16
|
-
},
|
|
17
|
-
"devDependencies": {
|
|
18
|
-
"@types/node": "^24.10.1",
|
|
19
|
-
"typescript": "^5.9.3"
|
|
20
|
-
}
|
|
21
|
-
},
|
|
22
|
-
"../constants": {
|
|
23
|
-
"name": "@cuboapp/constants",
|
|
24
|
-
"version": "2.0.4",
|
|
25
|
-
"license": "MIT",
|
|
26
|
-
"dependencies": {
|
|
27
|
-
"@cuboapp/types": "2.0.5"
|
|
28
|
-
}
|
|
29
|
-
},
|
|
30
|
-
"../database": {
|
|
31
|
-
"name": "@cuboapp/database",
|
|
32
|
-
"version": "1.0.1",
|
|
33
|
-
"license": "MIT",
|
|
34
|
-
"dependencies": {
|
|
35
|
-
"sequelize": "^6.37.7"
|
|
36
|
-
},
|
|
37
|
-
"devDependencies": {
|
|
38
|
-
"@types/node": "^24.0.0",
|
|
39
|
-
"typescript": "^5.9.3"
|
|
40
|
-
}
|
|
41
|
-
},
|
|
42
|
-
"../types": {
|
|
43
|
-
"name": "@cuboapp/types",
|
|
44
|
-
"version": "2.0.5",
|
|
45
|
-
"license": "MIT"
|
|
46
|
-
},
|
|
47
|
-
"../utils": {
|
|
48
|
-
"name": "@cuboapp/utils",
|
|
49
|
-
"version": "1.0.4",
|
|
50
|
-
"license": "MIT",
|
|
51
|
-
"devDependencies": {
|
|
52
|
-
"@types/node": "^24.9.1",
|
|
53
|
-
"typescript": "^5.9.3"
|
|
54
|
-
}
|
|
55
|
-
},
|
|
56
|
-
"node_modules/@cuboapp/constants": {
|
|
57
|
-
"resolved": "../constants",
|
|
58
|
-
"link": true
|
|
59
|
-
},
|
|
60
|
-
"node_modules/@cuboapp/database": {
|
|
61
|
-
"resolved": "../database",
|
|
62
|
-
"link": true
|
|
63
|
-
},
|
|
64
|
-
"node_modules/@cuboapp/types": {
|
|
65
|
-
"resolved": "../types",
|
|
66
|
-
"link": true
|
|
67
|
-
},
|
|
68
|
-
"node_modules/@cuboapp/utils": {
|
|
69
|
-
"resolved": "../utils",
|
|
70
|
-
"link": true
|
|
71
|
-
},
|
|
72
|
-
"node_modules/@types/node": {
|
|
73
|
-
"version": "24.10.1",
|
|
74
|
-
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz",
|
|
75
|
-
"integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==",
|
|
76
|
-
"dev": true,
|
|
77
|
-
"license": "MIT",
|
|
78
|
-
"dependencies": {
|
|
79
|
-
"undici-types": "~7.16.0"
|
|
80
|
-
}
|
|
81
|
-
},
|
|
82
|
-
"node_modules/typescript": {
|
|
83
|
-
"version": "5.9.3",
|
|
84
|
-
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
|
85
|
-
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
|
86
|
-
"dev": true,
|
|
87
|
-
"license": "Apache-2.0",
|
|
88
|
-
"bin": {
|
|
89
|
-
"tsc": "bin/tsc",
|
|
90
|
-
"tsserver": "bin/tsserver"
|
|
91
|
-
},
|
|
92
|
-
"engines": {
|
|
93
|
-
"node": ">=14.17"
|
|
94
|
-
}
|
|
95
|
-
},
|
|
96
|
-
"node_modules/undici-types": {
|
|
97
|
-
"version": "7.16.0",
|
|
98
|
-
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
|
|
99
|
-
"integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
|
|
100
|
-
"dev": true,
|
|
101
|
-
"license": "MIT"
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
}
|