@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.
- package/.prettierrc +8 -0
- package/package.json +25 -0
- package/pnpm-lock.yaml +378 -0
- package/src/constants/backend.ts +74 -0
- package/src/constants/base.ts +11 -0
- package/src/constants/index.ts +2 -0
- package/src/helpers/convert.ts +135 -0
- package/src/helpers/data.ts +147 -0
- package/src/helpers/entities.ts +30 -0
- package/src/helpers/index.ts +381 -0
- package/src/helpers/query.ts +145 -0
- package/src/helpers/users.ts +38 -0
- package/src/helpers/withes.ts +292 -0
- package/src/index.ts +367 -0
- package/src/types/augmentation.ts +29 -0
- package/src/types/basic.ts +59 -0
- package/src/types/db.ts +24 -0
- package/src/types/index.ts +3 -0
- package/src/utils/index.ts +128 -0
- package/tsconfig.build.json +4 -0
- package/tsconfig.json +21 -0
|
@@ -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
|
+
}
|