@cuboapp/api-backend 1.0.0

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.
@@ -0,0 +1,147 @@
1
+ import keyBy from 'lodash-es/keyBy'
2
+
3
+ import { CUBO_ENTITY_FIELD_TYPE } from '../../constants'
4
+ import { CuboEntitiesMap, CuboEntityFieldModel } from '../../types'
5
+ import { isDateValid } from '../../utils'
6
+
7
+ import { ApiHelpers } from '.'
8
+
9
+ export class ApiHelpersData<T extends CuboEntitiesMap<T>> {
10
+ constructor(private helpers: ApiHelpers<T>) {}
11
+
12
+ public prepare(type: 'create' | 'update' | 'delete', fields: CuboEntityFieldModel[], dto?: any, performer_id?: number) {
13
+ const fieldsByAlias = keyBy(fields, (i) => i.alias)
14
+
15
+ const errors: string[] = []
16
+ const prepared: any = {}
17
+
18
+ switch (type) {
19
+ case 'create':
20
+ prepared.created_at = dto.created_at !== undefined ? dto.created_at : new Date().toISOString()
21
+ prepared.created_by = dto.created_by !== undefined ? dto.created_by : performer_id
22
+ break
23
+ case 'update':
24
+ prepared.modified_at = dto.modified_at !== undefined ? dto.modified_at : new Date().toISOString()
25
+ prepared.modified_by = dto.modified_by !== undefined ? dto.modified_by : performer_id
26
+
27
+ if (dto?.deleted_at !== undefined) {
28
+ prepared.deleted_at = dto.deleted_at
29
+ }
30
+ if (dto?.deleted_by !== undefined) {
31
+ prepared.deleted_by = dto.deleted_by
32
+ }
33
+ break
34
+ case 'delete':
35
+ prepared.deleted_at = dto?.deleted_at !== undefined ? dto.deleted_at : new Date().toISOString()
36
+ prepared.deleted_by = dto?.deleted_by !== undefined ? dto.deleted_by : performer_id
37
+ break
38
+ }
39
+
40
+ for (const key in dto || {}) {
41
+ if (!fieldsByAlias[key]) {
42
+ errors.push(`"${key}": unknown field`)
43
+ } else {
44
+ const value = dto[key]
45
+ const tval = typeof value
46
+
47
+ switch (fieldsByAlias[key].type_id) {
48
+ case CUBO_ENTITY_FIELD_TYPE.TEXT:
49
+ {
50
+ if (value === null) {
51
+ prepared[key] = value
52
+ } else if (tval !== 'string') {
53
+ errors.push(`"${key}": type of value must be string`)
54
+ } else if (tval.length > 2048) {
55
+ errors.push(`"${key}": max length of char field is ${2048}`)
56
+ } else {
57
+ prepared[key] = value
58
+ }
59
+ }
60
+ break
61
+ case CUBO_ENTITY_FIELD_TYPE.NUMBER:
62
+ {
63
+ if (value === null) {
64
+ prepared[key] = value
65
+ } else if (tval !== 'number') {
66
+ errors.push(`"${key}": type of value must be number`)
67
+ } else {
68
+ prepared[key] = value
69
+ }
70
+ }
71
+ break
72
+ case CUBO_ENTITY_FIELD_TYPE.TEXTAREA:
73
+ {
74
+ if (value === null) {
75
+ prepared[key] = value
76
+ } else if (tval !== 'string') {
77
+ errors.push(`"${key}": type of value must be string`)
78
+ } else if (value.length > 65535 * 10) {
79
+ errors.push(`"${key}": max length of text field is ${65535 * 10}`)
80
+ } else {
81
+ prepared[key] = value
82
+ }
83
+ }
84
+ break
85
+ case CUBO_ENTITY_FIELD_TYPE.BOOLEAN:
86
+ {
87
+ if (value === null) {
88
+ prepared[key] = value
89
+ } else if (tval !== 'boolean') {
90
+ errors.push(`"${key}": type of value must be boolean`)
91
+ } else {
92
+ prepared[key] = value
93
+ }
94
+ }
95
+ break
96
+ case CUBO_ENTITY_FIELD_TYPE.SELECT:
97
+ {
98
+ if (value === null) {
99
+ prepared[key] = value
100
+ } else if (tval !== 'number') {
101
+ errors.push(`"${key}": type of value must be number`)
102
+ } else {
103
+ prepared[key] = value
104
+ }
105
+ // errors.push(`"${key}": select field type is not already implemented`)
106
+ }
107
+ break
108
+ case CUBO_ENTITY_FIELD_TYPE.DATE:
109
+ {
110
+ if (value === null) {
111
+ prepared[key] = value
112
+ } else if (tval !== 'string' || !isDateValid(value)) {
113
+ errors.push(`"${key}": value must be a valid date string in format YYYY-MM-DD`)
114
+ } else {
115
+ prepared[key] = value
116
+ }
117
+ }
118
+ break
119
+ case CUBO_ENTITY_FIELD_TYPE.TIMESTAMP: {
120
+ if (value === null) {
121
+ prepared[key] = value
122
+ } else if (tval !== 'string' || !(new Date(value).getTime() > 0)) {
123
+ errors.push(`"${key}": value must be a valid date string in format YYYY-MM-DD HH:mm:ss`)
124
+ } else {
125
+ prepared[key] = value
126
+ }
127
+ break
128
+ }
129
+ case CUBO_ENTITY_FIELD_TYPE.FILE:
130
+ errors.push(`"${key}": file field type is not already implemented`)
131
+ break
132
+ case CUBO_ENTITY_FIELD_TYPE.JSON:
133
+ if (value === null) {
134
+ prepared[key] = value
135
+ } else if (Buffer.byteLength(JSON.stringify(value)) >= 65535 * 50) {
136
+ errors.push(`"${key}": max size of json field id ${65535 * 50} bytes`)
137
+ } else {
138
+ prepared[key] = value
139
+ }
140
+ break
141
+ }
142
+ }
143
+ }
144
+
145
+ return prepared
146
+ }
147
+ }
@@ -0,0 +1,30 @@
1
+ import { CuboEntitiesMap, CuboEntityModel } from '../../types'
2
+
3
+ import { ApiHelpers } from '.'
4
+
5
+ export class ApiHelpersEntities<T extends CuboEntitiesMap<T>> {
6
+ constructor(private helpers: ApiHelpers<T>) {}
7
+
8
+ public async getMany(force = false): Promise<CuboEntityModel[]> {
9
+ return (
10
+ this.helpers?.api?.options?.entities ??
11
+ this.helpers.getCached(
12
+ `get_entities`,
13
+ () => this.helpers.api.apiRequest<{ items: CuboEntityModel[] }>(`/api/v1/entities?with=fields`).then((res) => res.items),
14
+ force
15
+ )
16
+ )
17
+ }
18
+
19
+ public async getByAlias(alias: keyof T): Promise<CuboEntityModel | undefined> {
20
+ const entities = await this.getMany()
21
+
22
+ return entities.find((e) => e.alias === alias)
23
+ }
24
+
25
+ public async getById(id: number): Promise<CuboEntityModel | undefined> {
26
+ const entities = await this.getMany()
27
+
28
+ return entities.find((e) => e.id === id)
29
+ }
30
+ }
@@ -0,0 +1,381 @@
1
+ import { CuboConfigDatabase } from '@cuboapp/config'
2
+ import { Sequelize, type Options } from 'sequelize'
3
+
4
+ import { CuboBackendApi } from '../..'
5
+ import { CUBO_CRUD_QUERY_CONDITION, CUBO_CRUD_QUERY_SYMBOL_NOT, CUBO_ENTITY_FIELD_TYPE } from '../../constants'
6
+ import { CuboEntitiesMap, CuboEntityFieldModel, CuboEntityFieldModelExtra } from '../../types'
7
+
8
+ import { ApiHelpersConvert } from './convert'
9
+ import { ApiHelpersData } from './data'
10
+ import { ApiHelpersEntities } from './entities'
11
+ import { ApiHelpersQuery } from './query'
12
+ import { ApiHelpersUsers } from './users'
13
+ import { ApiHelpersWithes } from './withes'
14
+
15
+ export class ApiHelpers<T extends CuboEntitiesMap<T>> {
16
+ constructor(public api: CuboBackendApi<T>) {}
17
+
18
+ public withes = new ApiHelpersWithes<T>(this)
19
+ public query = new ApiHelpersQuery<T>(this)
20
+ public convert = new ApiHelpersConvert<T>(this)
21
+ public users = new ApiHelpersUsers<T>(this)
22
+ public entities = new ApiHelpersEntities<T>(this)
23
+ public data = new ApiHelpersData<T>(this)
24
+
25
+ private cache: Record<string, any> = {}
26
+ private fetching: Record<string, Promise<any> | null> = {}
27
+ private connections: Record<string, Sequelize> = {}
28
+ private connecting: Record<string, Promise<Sequelize>> = {}
29
+
30
+ public getCached(alias: string, cb: (force: boolean) => Promise<any>, force = false) {
31
+ if (!force && this.cache[alias] !== undefined) return this.cache[alias]
32
+
33
+ if (this.fetching[alias]) {
34
+ return this.fetching[alias]
35
+ }
36
+
37
+ this.fetching[alias] = new Promise(async (resolve, reject) => {
38
+ try {
39
+ this.cache[alias] = await cb(force)
40
+
41
+ resolve(this.cache[alias])
42
+ } catch (e: any) {
43
+ reject(e)
44
+ } finally {
45
+ delete this.fetching[alias]
46
+ }
47
+ })
48
+
49
+ return this.fetching[alias]
50
+ }
51
+
52
+ public clearFieldConditionValue(
53
+ field: CuboEntityFieldModel | { type_id: number; alias: string; extra: CuboEntityFieldModelExtra },
54
+ value: any
55
+ ) {
56
+ const type = typeof value
57
+
58
+ const parts = `${value}`.split(':')
59
+
60
+ let is_not = false
61
+ let condition = 'eq'
62
+
63
+ // condition parts
64
+ switch (parts.length) {
65
+ case 3:
66
+ if (parts[0] !== CUBO_CRUD_QUERY_SYMBOL_NOT) {
67
+ throw new Error(`"${field.alias}": three-level value allow only "not" condition at first level (${value})`)
68
+ } else if (!Object.values(CUBO_CRUD_QUERY_CONDITION).includes(parts[1])) {
69
+ throw new Error(`"${field.alias}": incorrect condition "${parts[1]}" in second level (${value})`)
70
+ } else {
71
+ is_not = true
72
+ condition = parts[1]
73
+ value = parts[2]
74
+ }
75
+ break
76
+ case 2:
77
+ if (parts[0] === CUBO_CRUD_QUERY_SYMBOL_NOT) {
78
+ is_not = true
79
+ value = parts[1]
80
+ } else if (Object.values(CUBO_CRUD_QUERY_CONDITION).includes(parts[0])) {
81
+ condition = parts[0]
82
+ value = parts[1]
83
+ } else {
84
+ throw new Error(`"${field.alias}": incorrect condition "${parts[0]}" in second level (${value})`)
85
+ }
86
+ break
87
+ case 1:
88
+ break
89
+ default:
90
+ throw new Error(`"${field.alias}": incorrect condition value "${value}"`)
91
+ }
92
+
93
+ switch (field.type_id) {
94
+ case CUBO_ENTITY_FIELD_TYPE.TEXT:
95
+ case CUBO_ENTITY_FIELD_TYPE.TEXTAREA:
96
+ switch (type) {
97
+ case 'object':
98
+ if (value !== null) {
99
+ throw new Error('"' + field.alias + '" must be string')
100
+ }
101
+ break
102
+ case 'string':
103
+ if (value === 'null') {
104
+ value = null
105
+ }
106
+ break
107
+ case 'number':
108
+ value = `${value}`
109
+ break
110
+ default:
111
+ throw new Error('"' + field.alias + '" must be string')
112
+ }
113
+ break
114
+ case CUBO_ENTITY_FIELD_TYPE.SELECT:
115
+ if (value === 'null') {
116
+ value = null
117
+ } else {
118
+ if ([CUBO_CRUD_QUERY_CONDITION.IN, CUBO_CRUD_QUERY_CONDITION.NIN].includes(condition)) {
119
+ value = value.split(',')
120
+ }
121
+ }
122
+ break
123
+ case CUBO_ENTITY_FIELD_TYPE.NUMBER:
124
+ const precision = field.extra?.number_precision || 0
125
+
126
+ switch (type) {
127
+ case 'object':
128
+ if (value !== null) {
129
+ throw new Error('"' + field.alias + '" must be string')
130
+ }
131
+ break
132
+ case 'string':
133
+ if (value === 'null') {
134
+ value = null
135
+ } else {
136
+ if ([CUBO_CRUD_QUERY_CONDITION.IN, CUBO_CRUD_QUERY_CONDITION.NIN].includes(condition)) {
137
+ value = value.split(',')
138
+ } else {
139
+ const potentialValue = precision === 0 ? parseInt(value) : parseFloat(value)
140
+
141
+ if (isNaN(potentialValue)) {
142
+ throw new Error('"' + field.alias + '" must be number')
143
+ } else {
144
+ if (precision > 0) {
145
+ value = parseFloat(potentialValue.toFixed(precision))
146
+ } else {
147
+ value = potentialValue
148
+ }
149
+ }
150
+ }
151
+ }
152
+ break
153
+ case 'number':
154
+ if (precision > 0) {
155
+ value = parseFloat(parseFloat(value).toFixed(precision))
156
+ } else {
157
+ value = parseInt(value)
158
+ }
159
+ break
160
+ default:
161
+ throw new Error('"' + field.alias + '" must be number')
162
+ }
163
+ break
164
+ case CUBO_ENTITY_FIELD_TYPE.BOOLEAN:
165
+ switch (type) {
166
+ case 'object':
167
+ if (value !== null) {
168
+ throw new Error('"' + field.alias + '" must be string')
169
+ }
170
+ break
171
+ case 'string':
172
+ if (value === 'null') {
173
+ value = null
174
+ } else if (!['true', 'false'].includes(value)) {
175
+ throw new Error('"' + field.alias + '" must be true or false')
176
+ }
177
+ break
178
+ case 'number':
179
+ if (![0, 1].includes(value)) {
180
+ throw new Error('"' + field.alias + '" must be true or false')
181
+ }
182
+ break
183
+ case 'boolean':
184
+ break
185
+ default:
186
+ throw new Error('"' + field.alias + '" must be true or false')
187
+ }
188
+ break
189
+ default:
190
+ break
191
+ }
192
+
193
+ return {
194
+ value,
195
+ is_not,
196
+ condition
197
+ }
198
+ }
199
+
200
+ public prepareConditionSql(
201
+ field: string,
202
+ replacement: string,
203
+ value: any,
204
+ condition: string,
205
+ is_not: boolean
206
+ ): { conditions: string[]; replacements: Record<string, any> } {
207
+ const conditions: string[] = []
208
+ const replacements: Record<string, any> = {}
209
+
210
+ switch (condition) {
211
+ case CUBO_CRUD_QUERY_CONDITION.ILIKE:
212
+ conditions.push(`${field} ${is_not ? 'not ilike' : 'ilike'} :${replacement}`)
213
+ replacements[replacement] = '%' + value + '%'
214
+ break
215
+ case CUBO_CRUD_QUERY_CONDITION.LIKE:
216
+ conditions.push(`${field} ${is_not ? 'not like' : 'like'} :${replacement}`)
217
+ replacements[replacement] = '%' + value + '%'
218
+ break
219
+ case CUBO_CRUD_QUERY_CONDITION.EX:
220
+ conditions.push(`${is_not ? 'not exists' : 'exists'} ${field}`)
221
+ replacements[replacement] = value
222
+ break
223
+ case CUBO_CRUD_QUERY_CONDITION.IN:
224
+ if (Array.isArray(value)) {
225
+ conditions.push(`${field} ${is_not ? 'not in' : 'in'} (:${replacement})`)
226
+ replacements[replacement] = value
227
+ }
228
+ break
229
+ case CUBO_CRUD_QUERY_CONDITION.NIN:
230
+ if (Array.isArray(value)) {
231
+ conditions.push(`${field} ${is_not ? 'in' : 'not in'} (:${replacement})`)
232
+ replacements[replacement] = value
233
+ }
234
+ break
235
+ case CUBO_CRUD_QUERY_CONDITION.GT:
236
+ conditions.push(`${field} ${is_not ? '<' : '>'} :${replacement}`)
237
+ replacements[replacement] = value
238
+ break
239
+ case CUBO_CRUD_QUERY_CONDITION.GTE:
240
+ conditions.push(`${field} ${is_not ? '<=' : '>='} :${replacement}`)
241
+ replacements[replacement] = value
242
+ break
243
+ case CUBO_CRUD_QUERY_CONDITION.LT:
244
+ conditions.push(`${field} ${is_not ? '>' : '<'} :${replacement}`)
245
+ replacements[replacement] = value
246
+ break
247
+ case CUBO_CRUD_QUERY_CONDITION.LTE:
248
+ conditions.push(`${field} ${is_not ? '>=' : '<='} :${replacement}`)
249
+ replacements[replacement] = value
250
+ break
251
+ case CUBO_CRUD_QUERY_CONDITION.EQ:
252
+ default:
253
+ if (value === null) {
254
+ conditions.push(`${field} ${is_not ? 'is not' : 'is'} null`)
255
+ } else {
256
+ conditions.push(`${field} ${is_not ? '!=' : '='} :${replacement}`)
257
+ }
258
+ replacements[replacement] = value
259
+ break
260
+ }
261
+
262
+ return {
263
+ conditions,
264
+ replacements
265
+ }
266
+ }
267
+
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]
272
+ }
273
+ }
274
+
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
+ }
281
+
282
+ if (this.connecting[cacheKey] !== undefined) {
283
+ return this.connecting[cacheKey]
284
+ }
285
+
286
+ this.connecting[cacheKey] = new Promise(async (success) => {
287
+ const { storage } = this.api.account
288
+
289
+ let database: string = ''
290
+ let host: string = ''
291
+ let port: number = 0
292
+ let username: string = ''
293
+ let password: string = ''
294
+ let schema: string = ''
295
+ let dialect: string = ''
296
+
297
+ if (storage?.database) {
298
+ database = storage?.database
299
+ host = storage?.[type]?.host
300
+ port = storage?.[type]?.port
301
+ username = storage?.[type]?.username
302
+ password = storage?.[type]?.password
303
+ schema = storage?.[type]?.schema
304
+ } else {
305
+ if (!this.api.config.db) {
306
+ throw 'DB connection is not configured'
307
+ }
308
+
309
+ const dbConfig = this.api.config.db as any
310
+ let cfg: CuboConfigDatabase = {} as any
311
+ if (dbConfig) {
312
+ if (dbConfig[type]) {
313
+ cfg = dbConfig[type]
314
+ } else {
315
+ cfg = dbConfig as CuboConfigDatabase
316
+ }
317
+ }
318
+
319
+ database = cfg.database as string
320
+ host = cfg.host as string
321
+ port = cfg.port as number
322
+ username = cfg.username as string
323
+ password = cfg.password as string
324
+ schema = cfg.schema as string
325
+ dialect = cfg.dialect as string
326
+ }
327
+
328
+ if (!schema) {
329
+ schema = `account_${this.api.account.id}`
330
+ }
331
+
332
+ const config: Options = {
333
+ host,
334
+ port,
335
+ username,
336
+ password,
337
+ database,
338
+ dialect: dialect as any,
339
+ timezone: '+03:00',
340
+ logging: false,
341
+ schema
342
+ }
343
+
344
+ // cert for yandex cloud
345
+ if (/yandexcloud/.test(config.host!)) {
346
+ config.dialectOptions = {
347
+ ...(config.dialectOptions || {}),
348
+ ...{
349
+ ssl: {
350
+ ca: await fetch('https://storage.yandexcloud.net/cloud-certs/CA.pem').then((res) => res.text()),
351
+ rejectUnauthorized: true,
352
+ target_session_attrs: 'read-write',
353
+ require: true
354
+ }
355
+ }
356
+ }
357
+ }
358
+
359
+ const db = new Sequelize(config)
360
+
361
+ this.connections[cacheKey] = db
362
+ await db.authenticate()
363
+
364
+ if (dialect === 'postgres') {
365
+ await db.query(`SET schema '${schema}';`)
366
+ }
367
+
368
+ success(db)
369
+
370
+ delete this.connecting[cacheKey]
371
+ })
372
+
373
+ return this.connecting[cacheKey]
374
+ }
375
+
376
+ public async transaction(type: 'read' | 'write') {
377
+ const db = await this.getConnection(type)
378
+
379
+ return db.transaction()
380
+ }
381
+ }
@@ -0,0 +1,145 @@
1
+ import { CUBO_CRUD_DEFAULT_FIELDS, CUBO_CRUD_DEFAULT_PAGE_LIMIT, CUBO_CRUD_MAX_PAGE_LIMIT, CUBO_CRUD_QUERY_KEY_REGEX } from '../../constants'
2
+ import { CuboCrudFindQuery, CuboCrudRequest, CuboEntitiesMap, CuboEntityModel } from '../../types'
3
+ import { dbPrepareUpdateQueryString } from '../../utils'
4
+
5
+ import { ApiHelpers } from '.'
6
+
7
+ export class ApiHelpersQuery<T extends CuboEntitiesMap<T>> {
8
+ constructor(private helpers: ApiHelpers<T>) {}
9
+
10
+ public async createFindQuery(
11
+ req: CuboCrudRequest,
12
+ entity: CuboEntityModel,
13
+ queryDto?: Partial<CuboCrudFindQuery>,
14
+ isCount?: boolean
15
+ ): Promise<Partial<CuboCrudFindQuery>> {
16
+ const withes = queryDto?.withes !== undefined ? queryDto.withes : []
17
+ const selects = queryDto?.selects !== undefined ? queryDto.selects : []
18
+ const joins = queryDto?.joins !== undefined ? queryDto.joins : []
19
+ const conditions = queryDto?.conditions !== undefined ? queryDto.conditions : []
20
+ const sorts = queryDto?.sorts !== undefined ? queryDto.sorts : []
21
+ const replacements = queryDto?.replacements !== undefined ? queryDto?.replacements : {}
22
+
23
+ const { sort, limit: _limit, page, with: _withes, ...query } = req.query || {}
24
+
25
+ // plain conditions (nested are in withes.ts)
26
+ for (const key in query || {}) {
27
+ const parts = key.replace(CUBO_CRUD_QUERY_KEY_REGEX, '')?.split('.')
28
+
29
+ if (parts.length === 1) {
30
+ const conditionField = entity.fields?.find((f) => f.alias === parts[0])
31
+ const defaultField = CUBO_CRUD_DEFAULT_FIELDS.find((f) => f.alias === parts[0])
32
+
33
+ if (!conditionField && !defaultField) {
34
+ throw new Error('condition field "' + parts[0] + '" not found in entity "' + entity.alias + '"')
35
+ }
36
+
37
+ const fieldAlias = conditionField ? conditionField.alias : defaultField!.alias
38
+ const preparedCondition = this.helpers.clearFieldConditionValue(conditionField ? conditionField : defaultField!, query[key])
39
+ const prepared = this.helpers.prepareConditionSql(
40
+ `t1.${fieldAlias}`,
41
+ `c_1_${fieldAlias}_value`,
42
+ preparedCondition.value,
43
+ preparedCondition.condition,
44
+ preparedCondition.is_not
45
+ )
46
+
47
+ conditions.push(...(prepared.conditions || []))
48
+ Object.assign(replacements, prepared.replacements || {})
49
+ }
50
+ }
51
+
52
+ // sort
53
+ if (sort !== undefined && typeof sort === 'string') {
54
+ const sortParts = sort.split(',')
55
+
56
+ for (const sortPart of sortParts) {
57
+ const parts = sortPart.replace(CUBO_CRUD_QUERY_KEY_REGEX, '')?.split('.')
58
+
59
+ if (parts.length === 1) {
60
+ const sortDirection = sortPart[0] === '-' ? 'desc' : 'asc'
61
+ const sortKey = sortPart[0] === '-' ? sortPart.slice(1) : sortPart
62
+
63
+ const conditionField = entity.fields?.find((f) => f.alias === sortKey)
64
+ const defaultField = CUBO_CRUD_DEFAULT_FIELDS.find((f) => f.alias === sortKey)
65
+
66
+ if (!conditionField && !defaultField) {
67
+ throw new Error('sortPart field "' + sortKey + '" not found')
68
+ }
69
+
70
+ sorts.push('t1.' + sortKey + ' ' + sortDirection)
71
+ }
72
+ }
73
+ } else {
74
+ sorts.push('t1.id desc')
75
+ }
76
+
77
+ // limit & offset
78
+ let perPage = _limit !== undefined && !isNaN(+_limit) ? _limit : CUBO_CRUD_DEFAULT_PAGE_LIMIT
79
+ let pageNumber = page !== undefined && !isNaN(+page) ? +page : 1
80
+ if (pageNumber < 1) {
81
+ pageNumber = 1
82
+ }
83
+ if (perPage > CUBO_CRUD_MAX_PAGE_LIMIT) {
84
+ perPage = CUBO_CRUD_MAX_PAGE_LIMIT
85
+ }
86
+ if (perPage < 0) {
87
+ perPage = 0
88
+ }
89
+
90
+ const filtersLimit = perPage
91
+ const filtersOffset = (pageNumber - 1) * perPage
92
+
93
+ let offsetString = ''
94
+ let limitString = ''
95
+ let groupByString = ''
96
+
97
+ if (!isCount && perPage > 0) {
98
+ if (filtersOffset > 0) {
99
+ offsetString = `offset ${filtersOffset}`
100
+ }
101
+ limitString = `limit ${filtersLimit}`
102
+ }
103
+
104
+ selects.push(...CUBO_CRUD_DEFAULT_FIELDS.map((f) => `t1.${f.alias}`))
105
+ selects.push(...entity.fields.map((f) => `t1.${f.alias}`))
106
+
107
+ // remove deleted rows
108
+ if (!queryDto?.withDeleted) {
109
+ conditions.push('t1.deleted_at is null')
110
+ }
111
+
112
+ const sql = `
113
+ select
114
+ ${isCount ? 'count(t1.id) total' : selects.join(',\n')}
115
+ from
116
+ ${entity.alias} t1
117
+ ${joins.join('\n')}
118
+ ${conditions.length > 0 ? `where (${conditions.join(') and (')})` : ''}
119
+ ${groupByString}
120
+ ${sorts.length > 0 && !isCount ? 'order by ' + sorts.join(', ') : ''}
121
+ ${limitString}
122
+ ${offsetString}
123
+ `
124
+
125
+ return {
126
+ withes,
127
+ selects,
128
+ joins,
129
+ conditions,
130
+ sorts,
131
+ replacements,
132
+ sql
133
+ }
134
+ }
135
+
136
+ public async createUpdateQuery(req: CuboCrudRequest, entity: CuboEntityModel, queryDto?: Partial<CuboCrudFindQuery>) {
137
+ const dto = await this.helpers.data.prepare('update', entity.fields, req.body)
138
+
139
+ if (!Object.keys(dto)) {
140
+ throw { status: 400, text: 'No keys to update' }
141
+ }
142
+
143
+ return dbPrepareUpdateQueryString(dto)
144
+ }
145
+ }