@cuboapp/api-backend 1.0.7 → 1.0.9

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.
@@ -1,11 +1,10 @@
1
1
  import { CuboApiEntitiesMap, CuboEntity, CuboEntityField } from '@cuboapp/types'
2
- import get from 'lodash-es/get'
3
- import set from 'lodash-es/set'
4
- import unset from 'lodash-es/unset'
2
+ import { get, set, unset } from '@cuboapp/utils'
5
3
 
6
- import { CUBO_CRUD_DEFAULT_FIELDS, CUBO_ENTITY_FIELD_TYPE } from '../constants'
4
+ import { CUBO_CRUD_DEFAULT_FIELDS } from '../constants'
7
5
  import { CuboCrudFindQuery, CuboCrudRequest } from '../types'
8
6
 
7
+ import { CUBO_ENTITY_FIELD_TYPE } from '@cuboapp/constants'
9
8
  import { ApiHelpers } from '.'
10
9
 
11
10
  export class ApiHelpersConvert<T extends CuboApiEntitiesMap<T>> {
@@ -42,58 +41,34 @@ export class ApiHelpersConvert<T extends CuboApiEntitiesMap<T>> {
42
41
 
43
42
  const rowValue = get(row, initialValueKey, undefined)
44
43
 
45
- // todo: fix fields from core
46
- if (!withEl.entity && withEl.entity.alias === 'users') {
47
- userFieldsPrepared.push(withEl.full_key)
48
-
49
- switch (rowValue) {
50
- case null:
51
- case undefined:
52
- set(row, withEl.full_key, null)
53
- break
54
- case 0:
55
- set(row, withEl.full_key, { id: 0, name: this.helpers.api.account.name })
56
- break
57
- default:
58
- const user = await this.helpers.users.getById(rowValue)
59
-
60
- set(
61
- row,
62
- withEl.full_key,
63
- user ? { id: user.id, name: user.name, email: user.email } : { id: rowValue, name: `User #${rowValue}` }
64
- )
65
- break
66
- }
67
- } else {
68
- const rowValueEmpty = [0, '0', '', null, 'null'].includes(rowValue)
44
+ const rowValueEmpty = [0, '0', '', null, 'null'].includes(rowValue)
69
45
 
70
- if (rowValueEmpty) {
71
- set(row, withEl.full_key, null)
72
- }
46
+ if (rowValueEmpty) {
47
+ set(row, withEl.full_key, null)
48
+ }
73
49
 
74
- if (toPrepare[withEl.key]) {
75
- delete toPrepare[withEl.key]
76
- }
50
+ if (toPrepare[withEl.key]) {
51
+ delete toPrepare[withEl.key]
52
+ }
77
53
 
78
- // console.log('rowValue', withEl.full_key, { rowValueEmpty, rowValue })
54
+ // console.log('rowValue', withEl.full_key, { rowValueEmpty, rowValue })
79
55
 
80
- for (const field of [...withEl.entity.fields, ...CUBO_CRUD_DEFAULT_FIELDS]) {
81
- const fieldValueKey = withEl.table_name.toLowerCase() + '_' + field.alias
56
+ for (const field of [...withEl.entity.fields, ...CUBO_CRUD_DEFAULT_FIELDS]) {
57
+ const fieldValueKey = withEl.table_name.toLowerCase() + '_' + field.alias
82
58
 
83
- // console.log('add field', field.alias, rowValueEmpty)
59
+ // console.log('add field', field.alias, rowValueEmpty)
84
60
 
85
- if (!rowValueEmpty && !usersDefaultFields.includes(field.alias)) {
86
- const fieldKeyPath = withEl.full_key + '.' + field.alias
87
- const rowRelationValue = get(row, fieldValueKey, undefined)
61
+ if (!rowValueEmpty && !usersDefaultFields.includes(field.alias)) {
62
+ const fieldKeyPath = withEl.full_key + '.' + field.alias
63
+ const rowRelationValue = get(row, fieldValueKey, undefined)
88
64
 
89
- set(row, fieldKeyPath, this.prepareFieldValue(field, rowRelationValue))
65
+ set(row, fieldKeyPath, this.prepareFieldValue(field, rowRelationValue))
90
66
 
91
- toPrepare[fieldKeyPath] = field
92
- }
67
+ toPrepare[fieldKeyPath] = field
68
+ }
93
69
 
94
- if (field.alias !== 'id') {
95
- toRemove.push(fieldValueKey)
96
- }
70
+ if (field.alias !== 'id') {
71
+ toRemove.push(fieldValueKey)
97
72
  }
98
73
  }
99
74
  }
@@ -113,8 +88,8 @@ export class ApiHelpersConvert<T extends CuboApiEntitiesMap<T>> {
113
88
  return row
114
89
  }
115
90
 
116
- private prepareFieldValue(field: Pick<CuboEntityField, 'type_id' | 'alias' | 'extra'>, value: any) {
117
- switch (field.type_id) {
91
+ private prepareFieldValue(field: Pick<CuboEntityField, 'type' | 'alias' | 'extra'>, value: any) {
92
+ switch (field.type) {
118
93
  case CUBO_ENTITY_FIELD_TYPE.BOOLEAN:
119
94
  return value !== undefined ? (['true', true].includes(value) ? true : false) : null
120
95
  case CUBO_ENTITY_FIELD_TYPE.DATE:
@@ -1,7 +1,6 @@
1
+ import { CUBO_ENTITY_FIELD_TYPE } from '@cuboapp/constants'
1
2
  import { CuboApiEntitiesMap, CuboEntityField } from '@cuboapp/types'
2
- import keyBy from 'lodash-es/keyBy'
3
-
4
- import { CUBO_ENTITY_FIELD_TYPE } from '../constants'
3
+ import { keyBy } from '@cuboapp/utils'
5
4
 
6
5
  import { ApiHelpers } from '.'
7
6
 
@@ -43,7 +42,7 @@ export class ApiHelpersData<T extends CuboApiEntitiesMap<T>> {
43
42
  const value = dto[key]
44
43
  const tval = typeof value
45
44
 
46
- switch (fieldsByAlias[key].type_id) {
45
+ switch (fieldsByAlias[key].type) {
47
46
  case CUBO_ENTITY_FIELD_TYPE.TEXT:
48
47
  {
49
48
  if (value === null) {
@@ -1,31 +1,33 @@
1
- import { CuboConfigDatabase } from '@cuboapp/config'
2
- import { CuboApiEntitiesMap, CuboEntityField, CuboEntityFieldExtra } from '@cuboapp/types'
3
- import { Sequelize, type Options } from 'sequelize'
4
-
5
- import { CuboBackendApi } from '../..'
6
- import { CUBO_CRUD_QUERY_CONDITION, CUBO_CRUD_QUERY_SYMBOL_NOT, CUBO_ENTITY_FIELD_TYPE } from '../constants'
1
+ import { CUBO_ENTITY_FIELD_TYPE } from '@cuboapp/constants'
2
+ import { Database, createDatabase, type Options } from '@cuboapp/database'
3
+ import { CuboApiEntitiesMap, CuboEntity, CuboEntityField, CuboEntityFieldExtra, CuboEntityFieldType } from '@cuboapp/types'
4
+
5
+ import { CuboBackendApi, CuboBackendApiDbConnectionType, CuboCrudFindQuery, CuboCrudRequest } from '../..'
6
+ import {
7
+ CUBO_CRUD_DEFAULT_FIELDS,
8
+ CUBO_CRUD_DEFAULT_PAGE_LIMIT,
9
+ CUBO_CRUD_MAX_PAGE_LIMIT,
10
+ CUBO_CRUD_QUERY_CONDITION,
11
+ CUBO_CRUD_QUERY_KEY_REGEX,
12
+ CUBO_CRUD_QUERY_SYMBOL_NOT
13
+ } from '../constants'
7
14
 
8
15
  import { ApiHelpersConvert } from './convert'
9
16
  import { ApiHelpersData } from './data'
10
- import { ApiHelpersEntities } from './entities'
11
- import { ApiHelpersQuery } from './query'
12
- import { ApiHelpersUsers } from './users'
17
+
13
18
  import { ApiHelpersWithes } from './withes'
14
19
 
15
20
  export class ApiHelpers<T extends CuboApiEntitiesMap<T>> {
16
21
  constructor(public api: CuboBackendApi<T>) {}
17
22
 
18
23
  public withes = new ApiHelpersWithes<T>(this)
19
- public query = new ApiHelpersQuery<T>(this)
20
24
  public convert = new ApiHelpersConvert<T>(this)
21
- public users = new ApiHelpersUsers<T>(this)
22
- public entities = new ApiHelpersEntities<T>(this)
23
25
  public data = new ApiHelpersData<T>(this)
24
26
 
25
27
  private cache: Record<string, any> = {}
26
28
  private fetching: Record<string, Promise<any> | null> = {}
27
- private connections: Record<string, Sequelize> = {}
28
- private connecting: Record<string, Promise<Sequelize>> = {}
29
+ private connections: Partial<Record<CuboBackendApiDbConnectionType, Database>> = {}
30
+ private connecting: Record<string, Promise<Database>> = {}
29
31
 
30
32
  public getCached(alias: string, cb: (force: boolean) => Promise<any>, force = false) {
31
33
  if (!force && this.cache[alias] !== undefined) return this.cache[alias]
@@ -49,7 +51,10 @@ export class ApiHelpers<T extends CuboApiEntitiesMap<T>> {
49
51
  return this.fetching[alias]
50
52
  }
51
53
 
52
- public clearFieldConditionValue(field: CuboEntityField | { type_id: number; alias: string; extra?: CuboEntityFieldExtra }, value: any) {
54
+ public clearFieldConditionValue(
55
+ field: CuboEntityField | { type: CuboEntityFieldType; alias: string; extra?: CuboEntityFieldExtra },
56
+ value: any
57
+ ) {
53
58
  const type = typeof value
54
59
 
55
60
  const parts = `${value}`.split(':')
@@ -87,7 +92,7 @@ export class ApiHelpers<T extends CuboApiEntitiesMap<T>> {
87
92
  throw new Error(`"${field.alias}": incorrect condition value "${value}"`)
88
93
  }
89
94
 
90
- switch (field.type_id) {
95
+ switch (field.type) {
91
96
  case CUBO_ENTITY_FIELD_TYPE.TEXT:
92
97
  case CUBO_ENTITY_FIELD_TYPE.TEXTAREA:
93
98
  switch (type) {
@@ -265,25 +270,23 @@ export class ApiHelpers<T extends CuboApiEntitiesMap<T>> {
265
270
  }
266
271
  }
267
272
 
268
- public async dropConnection(account_id: number) {
269
- if (this.connections[account_id]) {
270
- await this.connections[account_id].close()
271
- delete this.connections[account_id]
273
+ public async dropConnection(type: CuboBackendApiDbConnectionType) {
274
+ if (this.connections[type]) {
275
+ await this.connections[type].disconnect()
276
+ delete this.connections[type]
272
277
  }
273
278
  }
274
279
 
275
- public async getConnection(type: 'read' | 'write') {
276
- const cacheKey = `${type}_${this.api.account.id}`
277
-
278
- if (this.connections[cacheKey]) {
279
- return this.connections[cacheKey]
280
+ public async getConnection(type: CuboBackendApiDbConnectionType) {
281
+ if (this.connections[type]) {
282
+ return this.connections[type]
280
283
  }
281
284
 
282
- if (this.connecting[cacheKey] !== undefined) {
283
- return this.connecting[cacheKey]
285
+ if (this.connecting[type] !== undefined) {
286
+ return this.connecting[type]
284
287
  }
285
288
 
286
- this.connecting[cacheKey] = new Promise(async (success) => {
289
+ this.connecting[type] = new Promise(async (success) => {
287
290
  let database: string = ''
288
291
  let host: string = ''
289
292
  let port: number = 0
@@ -293,22 +296,23 @@ export class ApiHelpers<T extends CuboApiEntitiesMap<T>> {
293
296
  let dialect: string = ''
294
297
  let dialectModule: any = undefined
295
298
 
296
- if (!this.api.config.db) {
299
+ if (!this.api.options.db.options) {
297
300
  throw 'DB connection is not configured'
298
301
  }
299
302
 
300
- const dbConfig = this.api.config.db as any
301
- let cfg: CuboConfigDatabase = {} as any
303
+ const dbConfig = this.api.options.db.options
304
+
305
+ let cfg: Options = {}
302
306
  if (dbConfig) {
303
307
  if (dbConfig[type]) {
304
308
  cfg = dbConfig[type]
305
309
  } else {
306
- cfg = dbConfig as CuboConfigDatabase
310
+ cfg = dbConfig as Options
307
311
  }
308
312
  }
309
313
 
310
314
  database = cfg.database as string
311
- dialectModule = cfg.dialectModule as string
315
+ dialectModule = cfg.dialectModule
312
316
  host = cfg.host as string
313
317
  port = cfg.port as number
314
318
  username = cfg.username as string
@@ -316,10 +320,6 @@ export class ApiHelpers<T extends CuboApiEntitiesMap<T>> {
316
320
  schema = cfg.schema as string
317
321
  dialect = cfg.dialect as string
318
322
 
319
- if (!schema) {
320
- schema = `account_${this.api.account.id}`
321
- }
322
-
323
323
  const config: Options = {
324
324
  host,
325
325
  port,
@@ -351,26 +351,168 @@ export class ApiHelpers<T extends CuboApiEntitiesMap<T>> {
351
351
  }
352
352
  }
353
353
 
354
- const db = new Sequelize(config)
355
-
356
- this.connections[cacheKey] = db
357
- await db.authenticate()
354
+ this.connections[type] = createDatabase(config)
355
+ await this.connections[type].connect()
358
356
 
359
357
  if (dialect === 'postgres') {
360
- await db.query(`SET schema '${schema}';`)
358
+ await this.connections[type].connection.query(`SET schema '${schema}';`)
361
359
  }
362
360
 
363
- success(db)
361
+ success(this.connections[type])
364
362
 
365
- delete this.connecting[cacheKey]
363
+ delete this.connecting[type]
366
364
  })
367
365
 
368
- return this.connecting[cacheKey]
366
+ return this.connecting[type]
369
367
  }
370
368
 
371
- public async transaction(type: 'read' | 'write') {
369
+ public async transaction(type: CuboBackendApiDbConnectionType) {
372
370
  const db = await this.getConnection(type)
373
371
 
374
- return db.transaction()
372
+ return db.connection.transaction()
373
+ }
374
+
375
+ public async getEntityByAlias(alias: keyof T): Promise<CuboEntity | undefined> {
376
+ return this.api.options.entities.find((e) => e.alias === alias)
377
+ }
378
+
379
+ public async getEntityById(id: number): Promise<CuboEntity | undefined> {
380
+ return this.api.options.entities.find((e) => e.id === id)
381
+ }
382
+
383
+ public async createFindQuery(
384
+ req: CuboCrudRequest,
385
+ entity: CuboEntity,
386
+ queryDto?: Partial<CuboCrudFindQuery>,
387
+ isCount?: boolean
388
+ ): Promise<Partial<CuboCrudFindQuery>> {
389
+ const withes = queryDto?.withes !== undefined ? queryDto.withes : []
390
+ const selects = queryDto?.selects !== undefined ? queryDto.selects : []
391
+ const joins = queryDto?.joins !== undefined ? queryDto.joins : []
392
+ const conditions = queryDto?.conditions !== undefined ? queryDto.conditions : []
393
+ const havings = queryDto?.havings !== undefined ? queryDto.havings : []
394
+ const sorts = queryDto?.sorts !== undefined ? queryDto.sorts : []
395
+ const replacements = queryDto?.replacements !== undefined ? queryDto?.replacements : {}
396
+ const groupBy = queryDto?.groupBy !== undefined ? queryDto?.groupBy : undefined
397
+
398
+ const { sort, limit: _limit, page, with: _withes, ...query } = req.query || {}
399
+
400
+ // plain conditions (nested are in withes.ts)
401
+ for (const key in query || {}) {
402
+ const parts = key.replace(CUBO_CRUD_QUERY_KEY_REGEX, '')?.split('.')
403
+
404
+ if (parts.length === 1) {
405
+ const conditionField = entity.fields?.find((f) => f.alias === parts[0])
406
+ const defaultField = CUBO_CRUD_DEFAULT_FIELDS.find((f) => f.alias === parts[0])
407
+
408
+ if (!conditionField && !defaultField) {
409
+ throw new Error('condition field "' + parts[0] + '" not found in entity "' + entity.alias + '"')
410
+ }
411
+
412
+ const fieldAlias = conditionField ? conditionField.alias : defaultField!.alias
413
+ const preparedCondition = this.clearFieldConditionValue(conditionField ? conditionField : defaultField!, query[key])
414
+ const prepared = this.prepareConditionSql(
415
+ `t1.${fieldAlias}`,
416
+ `c_1_${fieldAlias}_value`,
417
+ preparedCondition.value,
418
+ preparedCondition.condition,
419
+ preparedCondition.is_not
420
+ )
421
+
422
+ conditions.push(...(prepared.conditions || []))
423
+ Object.assign(replacements, prepared.replacements || {})
424
+ }
425
+ }
426
+
427
+ // sort
428
+ if (!sorts.length) {
429
+ if (sort !== undefined && typeof sort === 'string') {
430
+ const sortParts = sort.split(',')
431
+
432
+ for (const sortPart of sortParts) {
433
+ const parts = sortPart.replace(CUBO_CRUD_QUERY_KEY_REGEX, '')?.split('.')
434
+
435
+ if (parts.length === 1) {
436
+ const sortDirection = sortPart[0] === '-' ? 'desc' : 'asc'
437
+ const sortKey = sortPart[0] === '-' ? sortPart.slice(1) : sortPart
438
+
439
+ const conditionField = entity.fields?.find((f) => f.alias === sortKey)
440
+ const defaultField = CUBO_CRUD_DEFAULT_FIELDS.find((f) => f.alias === sortKey)
441
+
442
+ if (!conditionField && !defaultField) {
443
+ throw new Error('sortPart field "' + sortKey + '" not found')
444
+ }
445
+
446
+ sorts.push('t1.' + sortKey + ' ' + sortDirection)
447
+ }
448
+ }
449
+ } else {
450
+ sorts.push('t1.id desc')
451
+ }
452
+ }
453
+
454
+ // limit & offset
455
+ let perPage = _limit !== undefined && !isNaN(+_limit) ? _limit : CUBO_CRUD_DEFAULT_PAGE_LIMIT
456
+ let pageNumber = page !== undefined && !isNaN(+page) ? +page : 1
457
+ if (pageNumber < 1) {
458
+ pageNumber = 1
459
+ }
460
+ if (perPage > CUBO_CRUD_MAX_PAGE_LIMIT) {
461
+ perPage = CUBO_CRUD_MAX_PAGE_LIMIT
462
+ }
463
+ if (perPage < 0) {
464
+ perPage = 0
465
+ }
466
+
467
+ const filtersLimit = perPage
468
+ const filtersOffset = (pageNumber - 1) * perPage
469
+
470
+ let offsetString = ''
471
+ let limitString = ''
472
+ let groupByString = queryDto?.groupBy ?? ''
473
+
474
+ if (!isCount && perPage > 0) {
475
+ if (filtersOffset > 0) {
476
+ offsetString = `offset ${filtersOffset}`
477
+ }
478
+ limitString = `limit ${filtersLimit}`
479
+ }
480
+
481
+ selects.push(...CUBO_CRUD_DEFAULT_FIELDS.map((f) => `t1.${f.alias}`))
482
+ selects.push(...entity.fields.map((f) => `t1.${f.alias}`))
483
+
484
+ // remove deleted rows
485
+ if (!queryDto?.withDeleted) {
486
+ conditions.push('t1.deleted_at is null')
487
+ }
488
+
489
+ const sql = `
490
+ select
491
+ ${isCount ? 'count(t1.id) total' : selects.join(',\n')}
492
+ from
493
+ ${entity.alias} t1
494
+ ${joins.join('\n')}
495
+ ${conditions.length > 0 ? `where (${conditions.join(') and (')})` : ''}
496
+ ${groupByString}
497
+ ${havings.length > 0 ? `having (${havings.join(') and (')})` : ''}
498
+ ${sorts.length > 0 && !isCount ? 'order by ' + sorts.join(', ') : ''}
499
+ ${limitString}
500
+ ${offsetString}
501
+ `
502
+
503
+ // console.log('conditions', { conditions })
504
+ // console.log('sql', sql)
505
+
506
+ return {
507
+ withes,
508
+ selects,
509
+ joins,
510
+ conditions,
511
+ groupBy,
512
+ havings,
513
+ sorts,
514
+ replacements,
515
+ sql
516
+ }
375
517
  }
376
518
  }
@@ -1,5 +1,5 @@
1
1
  import { CuboApiEntitiesMap, CuboApiEntityDefaultUsersKeys, CuboEntity, CuboEntityField } from '@cuboapp/types'
2
- import cloneDeep from 'lodash-es/cloneDeep'
2
+ import { cloneDeep } from '@cuboapp/utils'
3
3
 
4
4
  import { CUBO_CRUD_DEFAULT_FIELDS, CUBO_CRUD_QUERY_KEY_REGEX } from '../constants'
5
5
  import { CuboCrudFindQuery, CuboCrudMethodOptions, CuboCrudRequest, CuboCrudWith } from '../types'
@@ -79,11 +79,11 @@ export class ApiHelpersWithes<T extends CuboApiEntitiesMap<T>> {
79
79
  let relationEntity: CuboEntity | undefined
80
80
 
81
81
  if (relation_entity_id) {
82
- relationEntity = await this.helpers.entities.getById(relation_entity_id)
82
+ relationEntity = await this.helpers.getEntityById(relation_entity_id)
83
83
  }
84
84
 
85
85
  if (!relationEntity && relation_entity_alias) {
86
- relationEntity = await this.helpers.entities.getByAlias(relation_entity_alias)
86
+ relationEntity = await this.helpers.getEntityByAlias(relation_entity_alias)
87
87
  }
88
88
 
89
89
  lastEntity = relationEntity
@@ -238,9 +238,9 @@ export class ApiHelpersWithes<T extends CuboApiEntitiesMap<T>> {
238
238
  const havings: string[] = [...(opts?.queryOptions?.havings || [])]
239
239
  const replacements: Record<string, any> = { ...(opts?.queryOptions?.replacements || {}) }
240
240
  const groupBy: string = opts?.queryOptions?.groupBy || undefined
241
+ const sorts: string[] = opts?.queryOptions?.sorts || []
241
242
 
242
243
  const withes: CuboCrudWith[] = []
243
- const sorts: string[] = []
244
244
 
245
245
  // from sort key
246
246
  if (_sort) {