@cuboapp/api-backend 1.0.38 → 3.0.1
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 +33 -11
- package/src/core/index.ts +743 -0
- package/src/crdt/index.ts +110 -0
- package/src/dialects/index.ts +36 -0
- package/src/helpers/index.ts +113 -6
- package/src/helpers/withes.ts +16 -12
- package/src/hooks/constants.ts +21 -0
- package/src/hooks/define.ts +76 -0
- package/src/hooks/index.ts +3 -0
- package/src/hooks/types.ts +52 -0
- package/src/index.ts +6 -443
- package/src/query/compile.ts +212 -0
- package/src/query/index.ts +2 -0
- package/src/query/types.ts +92 -0
- package/src/types/basic.ts +1 -1
- package/src/types/db.ts +74 -23
- package/src/types/filters.ts +50 -0
- package/src/types/index.ts +20 -8
- package/tsconfig.tsbuildinfo +1 -0
- package/src/old.ts +0 -744
- package/src/s3/index.ts +0 -104
- package/src/s3/types/index.ts +0 -19
- package/src/s3/utils/index.ts +0 -74
- package/src/types/augmentation.ts +0 -51
package/src/index.ts
CHANGED
|
@@ -1,444 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
import { CuboApiEntitiesMap } from '@cuboapp/types'
|
|
3
|
-
import { cloneDeep } from '@cuboapp/utils'
|
|
4
|
-
|
|
5
|
-
import { ApiHelpers } from './helpers'
|
|
6
|
-
import { CuboS3UploadFunctionOptions, CuboS3UploadFunctionResult, uploadToS3 } from './s3'
|
|
7
|
-
import { CuboBackendApiAuth, CuboBackendApiOptions, CuboCrudGetManyResponse, CuboCrudMethodOptions, CuboCrudRequest } from './types'
|
|
8
|
-
|
|
9
|
-
export class CuboBackendApi<T extends CuboApiEntitiesMap<T>, A extends unknown> {
|
|
10
|
-
constructor(public options: CuboBackendApiOptions<T, A>) {}
|
|
11
|
-
|
|
12
|
-
public auth: CuboBackendApiAuth
|
|
13
|
-
public helpers = new ApiHelpers<T, A>(this)
|
|
14
|
-
|
|
15
|
-
async init() {
|
|
16
|
-
try {
|
|
17
|
-
if (this.options.api !== undefined) {
|
|
18
|
-
this.auth = await this.request<{ variables: Record<string, any> }>('/auth/me?with=variables', {
|
|
19
|
-
withAuth: true
|
|
20
|
-
})
|
|
21
|
-
|
|
22
|
-
if (!this.auth) {
|
|
23
|
-
throw { code: 403, text: 'Авторизация Cubo-Backend не пройдена' }
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
if (this.auth.variables?.db !== undefined) {
|
|
27
|
-
this.options.db = this.options.db || {}
|
|
28
|
-
|
|
29
|
-
this.options.db.options = {
|
|
30
|
-
...(this.options.db.options || {}),
|
|
31
|
-
...(this.auth.variables?.db || {})
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
} catch (e) {
|
|
36
|
-
console.error('[API-BACKEND] startup error', e)
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
async destroy() {
|
|
41
|
-
const read = await this.helpers.getConnection('read', false)
|
|
42
|
-
const write = await this.helpers.getConnection('write', false)
|
|
43
|
-
|
|
44
|
-
if (read) {
|
|
45
|
-
await read.disconnect()
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
if (write) {
|
|
49
|
-
await write.disconnect()
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
private debounces = new Map<`${string}:${number}`, { started: number; promise: Promise<any> }>()
|
|
54
|
-
|
|
55
|
-
public async transaction() {
|
|
56
|
-
const db = await this.helpers.getConnection('write')
|
|
57
|
-
|
|
58
|
-
return db.connection.transaction()
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
public async request<T>(url: string, opts?: RequestInit & { debug?: boolean; withAuth?: boolean; withoutContentType?: boolean }) {
|
|
62
|
-
const baseUrl = this.options?.api?.base_url || 'https://api.cubo.sh'
|
|
63
|
-
|
|
64
|
-
const headers: Record<string, any> = {
|
|
65
|
-
...(opts?.headers || {})
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
if (opts?.withAuth) {
|
|
69
|
-
Object.assign(headers, this.options.api?.headers || {})
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
if (!headers['Content-Type'] && !opts?.withoutContentType) {
|
|
73
|
-
headers['Accept'] = 'application/json'
|
|
74
|
-
headers['Content-Type'] = 'application/json'
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
const requestUrl = baseUrl.replace(/\/$/, '') + '/' + url.replace(/^\//, '')
|
|
78
|
-
const requestOpts = {
|
|
79
|
-
...(opts || {}),
|
|
80
|
-
headers
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
try {
|
|
84
|
-
const response = await fetch(requestUrl, requestOpts)
|
|
85
|
-
|
|
86
|
-
if ([201, 200].includes(response.status)) {
|
|
87
|
-
try {
|
|
88
|
-
const json = await response.json()
|
|
89
|
-
|
|
90
|
-
return json as Promise<T>
|
|
91
|
-
} catch (e) {
|
|
92
|
-
const text = await response.text()
|
|
93
|
-
|
|
94
|
-
try {
|
|
95
|
-
const json = text ? JSON.parse(text) : null
|
|
96
|
-
|
|
97
|
-
return json as Promise<T>
|
|
98
|
-
} catch {
|
|
99
|
-
throw { status: 500, error: 'Invalid response', text }
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
} else {
|
|
103
|
-
throw {
|
|
104
|
-
status: response.status,
|
|
105
|
-
error: response.statusText,
|
|
106
|
-
text: await response.text()
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
} catch (e) {
|
|
110
|
-
if (opts?.debug) {
|
|
111
|
-
console.log(
|
|
112
|
-
'[API BACKEND]',
|
|
113
|
-
JSON.stringify(
|
|
114
|
-
{
|
|
115
|
-
request: {
|
|
116
|
-
url: requestUrl,
|
|
117
|
-
opts: requestOpts
|
|
118
|
-
},
|
|
119
|
-
error: e
|
|
120
|
-
},
|
|
121
|
-
null,
|
|
122
|
-
2
|
|
123
|
-
)
|
|
124
|
-
)
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
throw e
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
public async uploadToS3(buffer: Buffer, opts: CuboS3UploadFunctionOptions): Promise<CuboS3UploadFunctionResult> {
|
|
132
|
-
return uploadToS3(buffer, opts)
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
private async getEntity(entityAlias: Extract<keyof T, string>) {
|
|
136
|
-
const entity = await this.helpers.getEntityByAlias(entityAlias)
|
|
137
|
-
if (!entity) {
|
|
138
|
-
throw new Error('Entity no found: "' + String(entityAlias) + '"')
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
return entity
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
async getMany<K extends Extract<keyof T, string> = Extract<keyof T, string>>(
|
|
145
|
-
entityAlias: K,
|
|
146
|
-
req?: CuboCrudRequest,
|
|
147
|
-
opts?: CuboCrudMethodOptions
|
|
148
|
-
): Promise<CuboCrudGetManyResponse<T[K]>> {
|
|
149
|
-
opts = opts || {}
|
|
150
|
-
req = req || {}
|
|
151
|
-
|
|
152
|
-
req.query = cloneDeep(req.query || {})
|
|
153
|
-
|
|
154
|
-
const entity = await this.getEntity(entityAlias)
|
|
155
|
-
const augmentation = this.options?.augmentations?.[entityAlias]
|
|
156
|
-
|
|
157
|
-
if (augmentation?.beforeGetMany) {
|
|
158
|
-
opts.queryOptions = await augmentation.beforeGetMany(req, opts)
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// get all joins
|
|
162
|
-
const queryDto = await this.helpers.withes.prepare(req, entity, opts)
|
|
163
|
-
queryDto.withDeleted = opts.queryOptions?.withDeleted
|
|
164
|
-
|
|
165
|
-
// create find query
|
|
166
|
-
const regularQuery = await this.helpers.createFindQuery(req, entity, queryDto)
|
|
167
|
-
const countQuery = await this.helpers.createFindQuery(req, entity, queryDto, true)
|
|
168
|
-
|
|
169
|
-
const db = await this.helpers.getConnection('write')
|
|
170
|
-
|
|
171
|
-
if (opts?.log) {
|
|
172
|
-
console.log(`QUERY:getMany to "${String(entityAlias)}":`, regularQuery.sql, regularQuery.replacements, { req, opts, queryDto })
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
const rows = await db.connection.query<any>(regularQuery.sql!, {
|
|
176
|
-
type: QueryTypes.SELECT,
|
|
177
|
-
replacements: regularQuery.replacements,
|
|
178
|
-
transaction: opts?.transaction
|
|
179
|
-
})
|
|
180
|
-
const [totals] = await db.connection.query<any>(countQuery.sql!, {
|
|
181
|
-
type: QueryTypes.SELECT,
|
|
182
|
-
replacements: countQuery.replacements,
|
|
183
|
-
transaction: opts?.transaction
|
|
184
|
-
})
|
|
185
|
-
|
|
186
|
-
const response = {
|
|
187
|
-
rows: await this.helpers.convert.prepareAll(req, entity, rows, regularQuery),
|
|
188
|
-
totals: {
|
|
189
|
-
count: totals.total !== undefined ? +totals.total : 0
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
if (augmentation?.afterGetMany && !opts?.excludeHooks?.includes('afterGetMany')) {
|
|
194
|
-
return augmentation.afterGetMany(response, req, opts)
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
return response
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
async getOne<K extends Extract<keyof T, string> = Extract<keyof T, string>>(
|
|
201
|
-
entityAlias: K,
|
|
202
|
-
req: CuboCrudRequest,
|
|
203
|
-
opts?: CuboCrudMethodOptions
|
|
204
|
-
): Promise<T[K] | undefined> {
|
|
205
|
-
opts = opts || {}
|
|
206
|
-
|
|
207
|
-
const entity = await this.getEntity(entityAlias)
|
|
208
|
-
const augmentation = this.options?.augmentations?.[entityAlias]
|
|
209
|
-
if (augmentation?.beforeGetOne) {
|
|
210
|
-
opts.queryOptions = await augmentation.beforeGetOne(req, opts)
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
// get all joins
|
|
214
|
-
const queryDto = await this.helpers.withes.prepare(req, entity, opts)
|
|
215
|
-
queryDto.withDeleted = opts.queryOptions?.withDeleted
|
|
216
|
-
queryDto.limit = 1
|
|
217
|
-
|
|
218
|
-
const query = await this.helpers.createFindQuery(req, entity, queryDto)
|
|
219
|
-
|
|
220
|
-
const db = await this.helpers.getConnection('read')
|
|
221
|
-
|
|
222
|
-
const [row] = await db.connection.query<any>(query.sql!, {
|
|
223
|
-
type: QueryTypes.SELECT,
|
|
224
|
-
replacements: query.replacements,
|
|
225
|
-
transaction: opts?.transaction
|
|
226
|
-
})
|
|
227
|
-
|
|
228
|
-
if (opts?.log) {
|
|
229
|
-
console.log(`QUERY:getOne to "${String(entityAlias)}":`, query.sql, query.replacements)
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
if (!row) {
|
|
233
|
-
return null as any
|
|
234
|
-
// throw { status: 404, message: 'Element "' + entityAlias + '" not found', details: req }
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
const response = await this.helpers.convert.prepareOne(req, entity, row, query)
|
|
238
|
-
|
|
239
|
-
if (augmentation?.afterGetOne && !opts?.excludeHooks?.includes('afterGetOne')) {
|
|
240
|
-
return augmentation.afterGetOne(response, req, opts)
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
return response
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
async createOne<K extends Extract<keyof T, string> = Extract<keyof T, string>>(
|
|
247
|
-
entityAlias: K,
|
|
248
|
-
req: CuboCrudRequest,
|
|
249
|
-
opts?: CuboCrudMethodOptions
|
|
250
|
-
): Promise<T[K]> {
|
|
251
|
-
opts = opts || {}
|
|
252
|
-
const entity = await this.helpers.getEntityByAlias(entityAlias)
|
|
253
|
-
const augmentation = this.options?.augmentations?.[entityAlias]
|
|
254
|
-
|
|
255
|
-
if (augmentation?.beforeCreate) {
|
|
256
|
-
opts.queryOptions = await augmentation.beforeCreate(req, opts)
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
const dto = await this.helpers.data.prepare('create', entity!.fields, req.body, opts?.performer_id || 0)
|
|
260
|
-
if (!Object.keys(dto)) {
|
|
261
|
-
throw { status: 400, text: 'No keys to update' }
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
// console.log('dto', dto)
|
|
265
|
-
// const prepared = dbPrepareInsertQueryString(dto)
|
|
266
|
-
const db = await this.helpers.getConnection('write')
|
|
267
|
-
const [created_id] = await db.create(entity.alias, dto, { transaction: opts?.transaction, log: opts?.log })
|
|
268
|
-
|
|
269
|
-
if (!created_id) {
|
|
270
|
-
throw { status: 400, text: 'Unable to create entity' }
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
let response = await this.getOne<K>(entityAlias, { query: { id: created_id } }, opts)
|
|
274
|
-
|
|
275
|
-
if (!response) {
|
|
276
|
-
throw { status: 400, text: 'Unable to find entity after create' }
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
if (augmentation?.afterCreate && !opts?.excludeHooks?.includes('afterCreate')) {
|
|
280
|
-
response = await augmentation.afterCreate(response, req, opts)
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
if (this.options.hooks?.onAfterCreate && !opts?.excludeHooks?.includes('baseHooks')) {
|
|
284
|
-
await this.options.hooks.onAfterCreate(entity, response, opts)
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
return response
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
async updateOne<K extends Extract<keyof T, string> = Extract<keyof T, string>>(
|
|
291
|
-
entityAlias: K,
|
|
292
|
-
req: CuboCrudRequest,
|
|
293
|
-
opts?: CuboCrudMethodOptions
|
|
294
|
-
): Promise<T[K] | undefined> {
|
|
295
|
-
if (opts?.debounce) {
|
|
296
|
-
if (!req.query?.id) {
|
|
297
|
-
throw 'Debounce "updateOne" allowed only with "id"'
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
const key = `${entityAlias}:${+req.query.id}` as `${string}:${number}`
|
|
301
|
-
let ex = this.debounces.get(key)
|
|
302
|
-
if (ex) {
|
|
303
|
-
if (ex.started > +new Date() - opts.debounce) {
|
|
304
|
-
Promise.reject(ex.promise)
|
|
305
|
-
} else {
|
|
306
|
-
return ex.promise
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
return new Promise((resolve, reject) => {
|
|
311
|
-
setTimeout(() => {
|
|
312
|
-
this.updateOne(entityAlias, req, { ...(opts || {}), debounce: undefined })
|
|
313
|
-
.then(resolve)
|
|
314
|
-
.catch(reject)
|
|
315
|
-
}, opts.debounce)
|
|
316
|
-
})
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
opts = opts || {}
|
|
320
|
-
const entity = await this.helpers.getEntityByAlias(entityAlias)
|
|
321
|
-
const augmentation = this.options?.augmentations?.[entityAlias]
|
|
322
|
-
|
|
323
|
-
if (augmentation?.beforeUpdate && !opts?.excludeHooks?.includes('beforeUpdate')) {
|
|
324
|
-
opts.queryOptions = await augmentation.beforeUpdate(req, opts)
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
const item: any = await this.getOne(entityAlias, req, opts)
|
|
328
|
-
|
|
329
|
-
if (!item) {
|
|
330
|
-
throw { status: 404, text: 'Entity not found' }
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
const initialEntity = cloneDeep(item)
|
|
334
|
-
|
|
335
|
-
try {
|
|
336
|
-
if (!!augmentation?.can) {
|
|
337
|
-
await augmentation.can('update', { entity, row: item, auth: opts?.extra?.auth, req })
|
|
338
|
-
}
|
|
339
|
-
} catch {
|
|
340
|
-
throw { status: 403, text: 'Updating entity forbidden' }
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
const dto = await this.helpers.data.prepare('update', entity!.fields, req.body, opts?.performer_id || 0)
|
|
344
|
-
if (!Object.keys(dto)) {
|
|
345
|
-
throw { status: 400, text: 'No keys to update' }
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
const db = await this.helpers.getConnection('write')
|
|
349
|
-
await db.update(entity.alias, 'id = :id', { id: item.id }, dto, { transaction: opts?.transaction, log: opts?.log })
|
|
350
|
-
|
|
351
|
-
let response = await this.getOne<K>(entityAlias, req, opts)
|
|
352
|
-
if (!response) {
|
|
353
|
-
throw { status: 400, text: 'Unable to find entity after update' }
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
if (augmentation?.afterUpdate && !opts?.excludeHooks?.includes('afterUpdate')) {
|
|
357
|
-
response = await augmentation.afterUpdate(response, req, opts)
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
if (this.options.hooks?.onAfterUpdate && !opts?.excludeHooks?.includes('baseHooks')) {
|
|
361
|
-
await this.options.hooks.onAfterUpdate(entity, response, initialEntity, opts)
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
return response
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
async deleteOne<K extends Extract<keyof T, string>>(
|
|
368
|
-
entityAlias: K,
|
|
369
|
-
req: CuboCrudRequest,
|
|
370
|
-
opts?: CuboCrudMethodOptions
|
|
371
|
-
): Promise<boolean> {
|
|
372
|
-
opts = opts || {}
|
|
373
|
-
const entity = await this.helpers.getEntityByAlias(entityAlias)
|
|
374
|
-
const augmentation = this.options?.augmentations?.[entityAlias]
|
|
375
|
-
|
|
376
|
-
if (augmentation?.beforeDelete) {
|
|
377
|
-
opts.queryOptions = await augmentation.beforeDelete(req, opts)
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
const item: any = await this.getOne(entityAlias, req, opts)
|
|
381
|
-
if (!item) {
|
|
382
|
-
throw { status: 404, text: 'Entity not found' }
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
const dto = await this.helpers.data.prepare('delete', entity!.fields, req.body || {}, opts?.performer_id)
|
|
386
|
-
if (!Object.keys(dto)) {
|
|
387
|
-
throw { status: 400, text: 'No keys to update' }
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
const db = await this.helpers.getConnection('write')
|
|
391
|
-
await db.update(entity.alias, 'id = :id', { id: item.id }, dto, { transaction: opts?.transaction, log: opts?.log })
|
|
392
|
-
|
|
393
|
-
if (this.options.hooks?.onAfterDelete && !opts?.excludeHooks?.includes('baseHooks')) {
|
|
394
|
-
await this.options.hooks.onAfterDelete(entity, item, opts)
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
// console.log(entityAlias, augmentation, opts)
|
|
398
|
-
|
|
399
|
-
if (augmentation?.afterDelete && !opts?.excludeHooks?.includes('afterDelete')) {
|
|
400
|
-
return augmentation.afterDelete(item, req, opts)
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
return true
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
async deleteMany<K extends Extract<keyof T, string>>(
|
|
407
|
-
entityAlias: K,
|
|
408
|
-
req: CuboCrudRequest,
|
|
409
|
-
opts?: CuboCrudMethodOptions
|
|
410
|
-
): Promise<boolean> {
|
|
411
|
-
opts = opts || {}
|
|
412
|
-
const entity = await this.helpers.getEntityByAlias(entityAlias)
|
|
413
|
-
const augmentation = this.options?.augmentations?.[entityAlias]
|
|
414
|
-
|
|
415
|
-
if (augmentation?.beforeDelete) {
|
|
416
|
-
opts.queryOptions = await augmentation.beforeDelete(req, opts)
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
const { rows: items } = await this.getMany(entityAlias, req, opts)
|
|
420
|
-
if (!items) {
|
|
421
|
-
return false
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
const dto = await this.helpers.data.prepare('delete', entity!.fields, req.body || {}, opts?.performer_id)
|
|
425
|
-
if (!Object.keys(dto)) {
|
|
426
|
-
throw { status: 400, text: 'No keys to update' }
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
const db = await this.helpers.getConnection('write')
|
|
430
|
-
|
|
431
|
-
for (const item of items) {
|
|
432
|
-
await db.update(entity.alias, 'id = :id', { id: (item as any).id }, dto, { transaction: opts?.transaction, log: opts?.log })
|
|
433
|
-
|
|
434
|
-
if (this.options.hooks?.onAfterDelete && !opts?.excludeHooks?.includes('baseHooks')) {
|
|
435
|
-
await this.options.hooks.onAfterDelete(entity, item, opts)
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
return true
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
export * from './helpers'
|
|
1
|
+
export * from './core'
|
|
444
2
|
export * from './types'
|
|
3
|
+
export * from './helpers'
|
|
4
|
+
export * from './hooks'
|
|
5
|
+
export * from './query'
|
|
6
|
+
export * from './dialects'
|
|
7
|
+
export * from './crdt'
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { CuboCompiledQuery, CuboOp, CuboQuery, CuboSort, CuboWith } from './types'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Компилирует типизированный CuboQuery<R> в плоский «строковый» запрос движка
|
|
5
|
+
* (CuboCrudRequest.query). Чистая функция: на каждый вызов создаётся новый
|
|
6
|
+
* объект — query-опции иммутабельны по построению, ничего не «накапливается».
|
|
7
|
+
*
|
|
8
|
+
* Поддержка операторов через flat-DSL:
|
|
9
|
+
* eq/ne/gt/gte/lt/lte/in/nin/like/ilike/between/isNull/not.
|
|
10
|
+
* Диапазон gte+lte на одном поле автоматически сворачивается в between.
|
|
11
|
+
* Экзотические комбинации (>1 оператора, не сводящиеся к диапазону) —
|
|
12
|
+
* через escape-hatch `raw` в CuboCrudMethodOptions.queryOptions.
|
|
13
|
+
*/
|
|
14
|
+
export function compileQuery<R>(query?: CuboQuery<R>): CuboCompiledQuery {
|
|
15
|
+
const out: CuboCompiledQuery = {}
|
|
16
|
+
|
|
17
|
+
if (!query) {
|
|
18
|
+
return out
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (query.where) {
|
|
22
|
+
compileWhere(query.where as Record<string, any>, '', out)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (query.with) {
|
|
26
|
+
const withList = compileWith(query.with as CuboWith<R>)
|
|
27
|
+
if (withList.length) {
|
|
28
|
+
out.with = withList.join(',')
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (query.sort) {
|
|
33
|
+
const sort = compileSort(query.sort)
|
|
34
|
+
if (sort) {
|
|
35
|
+
out.sort = sort
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (query.limit !== undefined) {
|
|
40
|
+
out.limit = query.limit
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (query.page !== undefined) {
|
|
44
|
+
out.page = query.page
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return out
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function compileWhere(where: Record<string, any>, prefix: string, out: CuboCompiledQuery) {
|
|
51
|
+
for (const key in where) {
|
|
52
|
+
const value = where[key]
|
|
53
|
+
const fullKey = prefix ? `${prefix}.${key}` : key
|
|
54
|
+
|
|
55
|
+
// вложенная связь -> рекурсивно с dot-notation ключом
|
|
56
|
+
if (isPlainObject(value) && !isOperatorObject(value)) {
|
|
57
|
+
compileWhere(value, fullKey, out)
|
|
58
|
+
continue
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
out[fullKey] = compileOp(fullKey, value)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** true, если объект похож на набор операторов (а не на вложенный where). */
|
|
66
|
+
const OP_KEYS = ['eq', 'ne', 'gt', 'gte', 'lt', 'lte', 'in', 'nin', 'like', 'ilike', 'between', 'isNull', 'not']
|
|
67
|
+
function isOperatorObject(value: object): boolean {
|
|
68
|
+
return Object.keys(value).some((k) => OP_KEYS.includes(k))
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function isPlainObject(value: any): value is Record<string, any> {
|
|
72
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value) && !(value instanceof Date)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Кодирует один оператор поля в строковое значение flat-DSL. */
|
|
76
|
+
function compileOp(key: string, op: CuboOp<any>): any {
|
|
77
|
+
// короткая форма: просто значение (или null) = eq
|
|
78
|
+
if (op === null || typeof op !== 'object' || op instanceof Date || Array.isArray(op)) {
|
|
79
|
+
return op
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const o = op as Exclude<CuboOp<any>, any[]> & Record<string, any>
|
|
83
|
+
const present = Object.keys(o).filter((k) => o[k] !== undefined)
|
|
84
|
+
|
|
85
|
+
// диапазон gte+lte -> between
|
|
86
|
+
if (present.length === 2 && present.includes('gte') && present.includes('lte')) {
|
|
87
|
+
return `btw:${o.gte}:${o.lte}`
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (present.length > 1) {
|
|
91
|
+
throw new Error(`query "${key}": несколько операторов на одно поле не поддерживаются (используйте between или queryOptions.raw): ${present.join(', ')}`)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const [cond] = present
|
|
95
|
+
|
|
96
|
+
switch (cond) {
|
|
97
|
+
case 'eq':
|
|
98
|
+
return o.eq
|
|
99
|
+
case 'ne':
|
|
100
|
+
return o.ne === null ? 'not:null' : `not:${o.ne}`
|
|
101
|
+
case 'gt':
|
|
102
|
+
return `gt:${o.gt}`
|
|
103
|
+
case 'gte':
|
|
104
|
+
return `gte:${o.gte}`
|
|
105
|
+
case 'lt':
|
|
106
|
+
return `lt:${o.lt}`
|
|
107
|
+
case 'lte':
|
|
108
|
+
return `lte:${o.lte}`
|
|
109
|
+
case 'in':
|
|
110
|
+
return `in:${(o.in as any[]).join(',')}`
|
|
111
|
+
case 'nin':
|
|
112
|
+
return `nin:${(o.nin as any[]).join(',')}`
|
|
113
|
+
case 'like':
|
|
114
|
+
return `like:${o.like}`
|
|
115
|
+
case 'ilike':
|
|
116
|
+
return `ilike:${o.ilike}`
|
|
117
|
+
case 'between':
|
|
118
|
+
return `btw:${o.between[0]}:${o.between[1]}`
|
|
119
|
+
case 'isNull':
|
|
120
|
+
return o.isNull ? null : 'not:null'
|
|
121
|
+
case 'not':
|
|
122
|
+
return compileNot(key, o.not)
|
|
123
|
+
default:
|
|
124
|
+
// пустой объект операторов — игнорируем
|
|
125
|
+
return undefined
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Кодирует отрицание. Движок поддерживает максимум 3 уровня: not:<cond>:<value>. */
|
|
130
|
+
function compileNot(key: string, inner: CuboOp<any>): string {
|
|
131
|
+
if (inner === null) {
|
|
132
|
+
return 'not:null'
|
|
133
|
+
}
|
|
134
|
+
if (typeof inner !== 'object' || inner instanceof Date || Array.isArray(inner)) {
|
|
135
|
+
return `not:${inner}` // not:value === ne
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const o = inner as Record<string, any>
|
|
139
|
+
const present = Object.keys(o).filter((k) => o[k] !== undefined)
|
|
140
|
+
if (present.length !== 1) {
|
|
141
|
+
throw new Error(`query "${key}": "not" поддерживает ровно один оператор`)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const [cond] = present
|
|
145
|
+
switch (cond) {
|
|
146
|
+
case 'eq':
|
|
147
|
+
return o.eq === null ? 'not:null' : `not:${o.eq}`
|
|
148
|
+
case 'gt':
|
|
149
|
+
return `not:gt:${o.gt}`
|
|
150
|
+
case 'gte':
|
|
151
|
+
return `not:gte:${o.gte}`
|
|
152
|
+
case 'lt':
|
|
153
|
+
return `not:lt:${o.lt}`
|
|
154
|
+
case 'lte':
|
|
155
|
+
return `not:lte:${o.lte}`
|
|
156
|
+
case 'in':
|
|
157
|
+
return `not:in:${(o.in as any[]).join(',')}`
|
|
158
|
+
case 'nin':
|
|
159
|
+
return `not:nin:${(o.nin as any[]).join(',')}`
|
|
160
|
+
case 'like':
|
|
161
|
+
return `not:like:${o.like}`
|
|
162
|
+
case 'ilike':
|
|
163
|
+
return `not:ilike:${o.ilike}`
|
|
164
|
+
case 'isNull':
|
|
165
|
+
return o.isNull ? 'not:null' : null!
|
|
166
|
+
default:
|
|
167
|
+
throw new Error(`query "${key}": оператор "${cond}" нельзя инвертировать через "not" (используйте queryOptions.raw)`)
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** Собирает список dot-notation путей для `with` из массива или дерева. */
|
|
172
|
+
function compileWith<R>(input: CuboWith<R>, prefix = ''): string[] {
|
|
173
|
+
const result: string[] = []
|
|
174
|
+
|
|
175
|
+
if (Array.isArray(input)) {
|
|
176
|
+
for (const key of input) {
|
|
177
|
+
result.push(prefix ? `${prefix}.${String(key)}` : String(key))
|
|
178
|
+
}
|
|
179
|
+
return result
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
for (const key in input as Record<string, any>) {
|
|
183
|
+
const node = (input as Record<string, any>)[key]
|
|
184
|
+
const path = prefix ? `${prefix}.${key}` : key
|
|
185
|
+
|
|
186
|
+
result.push(path)
|
|
187
|
+
|
|
188
|
+
if (node && node !== true && node.with) {
|
|
189
|
+
result.push(...compileWith(node.with, path))
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return result
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function compileSort<R>(sort: CuboSort<R> | CuboSort<R>[]): string {
|
|
197
|
+
const list = Array.isArray(sort) ? sort : [sort]
|
|
198
|
+
const parts: string[] = []
|
|
199
|
+
|
|
200
|
+
for (const item of list) {
|
|
201
|
+
if (typeof item === 'string') {
|
|
202
|
+
parts.push(item)
|
|
203
|
+
} else if (item && typeof item === 'object') {
|
|
204
|
+
for (const key in item as Record<string, any>) {
|
|
205
|
+
const dir = (item as Record<string, any>)[key]
|
|
206
|
+
parts.push(dir === 'desc' ? `-${key}` : key)
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return parts.join(',')
|
|
212
|
+
}
|