@things-factory/shell 7.0.33 → 7.0.44

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,23 +1,19 @@
1
1
  import { Brackets, EntityMetadata, Repository, SelectQueryBuilder, WhereExpressionBuilder } from 'typeorm'
2
2
  import { RelationMetadata } from 'typeorm/metadata/RelationMetadata'
3
-
4
- import { Filter, ListParam, InheritedValueType } from '../service/common-types/list-param'
3
+ import { Filter, Sorting, Pagination, ListParam, InheritedValueType } from '../service/common-types/list-param'
5
4
  import { Domain } from '../service/domain/domain'
6
5
 
7
6
  /**
8
- * Function to create a TypeORM SelectQueryBuilder for database queries.
7
+ * Creates a TypeORM SelectQueryBuilder based on the provided parameters.
9
8
  *
10
- * @param options - An object containing options for querying and building.
11
- * @param options.repository - TypeORM repository used for database operations.
12
- * @param options.params - ListParam object for data retrieval and manipulation.
13
- * @param [options.domain] - Optional domain object for applying domain-related filters.
14
- * @param [options.alias] - Alias to be used in SQL queries (optional).
15
- * @param [options.searchables] - List of searchable columns (optional).
16
- * @param [options.filtersMap] - Mapping of filter names to their corresponding columns or relation columns (optional).
17
- * @param [options.filtersMap.name] - Filter name.
18
- * @param [options.filtersMap.columnName] - Name of the column where the filter is applied.
19
- * @param [options.filtersMap.relationColumn] - If the filter is applied to a related column, the name of that relation column (optional).
20
- * @returns {SelectQueryBuilder<Type>} - The generated SelectQueryBuilder object.
9
+ * @param options - An object containing the query building options.
10
+ * @param options.repository - The TypeORM repository for database operations.
11
+ * @param options.params - The ListParam object containing filters, sortings, and pagination.
12
+ * @param [options.domain] - Optional domain object for applying domain-specific filters.
13
+ * @param [options.alias] - The alias to be used in the SQL queries.
14
+ * @param [options.searchables] - List of columns that are searchable.
15
+ * @param [options.filtersMap] - Mapping of filter names to their corresponding columns or relation columns.
16
+ * @returns {SelectQueryBuilder<Type>} - The constructed SelectQueryBuilder instance.
21
17
  */
22
18
  export function getQueryBuilderFromListParams<Type>(options: {
23
19
  repository: Repository<Type>
@@ -27,23 +23,25 @@ export function getQueryBuilderFromListParams<Type>(options: {
27
23
  searchables?: string[]
28
24
  filtersMap?: { [name: string]: { columnName: string; relationColumn?: string } }
29
25
  }): SelectQueryBuilder<Type> {
30
- var { repository, params, domain, alias, searchables, filtersMap = {} } = options
31
- var { inherited = InheritedValueType.None } = params || {}
26
+ const { repository, params, domain, alias, searchables, filtersMap = {} } = options
27
+ const { inherited = InheritedValueType.None } = params || {}
32
28
 
33
29
  const selectQueryBuilder = repository.createQueryBuilder(alias)
34
30
  const entityAlias = selectQueryBuilder.alias
35
31
 
32
+ // Apply filters to the query
36
33
  const columnFilters =
37
34
  params.filters?.filter(filter => {
38
35
  if (filter.operator === 'search') {
39
36
  return false
40
37
  }
41
38
  if (filter.operator.toLowerCase().includes('like') && (!searchables || !searchables.includes(filter.name))) {
42
- console.warn('"searchables" setting is required for like searches with a heavy database query load', filter.name)
39
+ console.warn('"searchables" setting is required for LIKE searches to avoid heavy database load', filter.name)
43
40
  return false
44
41
  }
45
42
  return true
46
43
  }) || []
44
+
47
45
  const searchFilters =
48
46
  searchables instanceof Array
49
47
  ? params.filters?.filter(filter => {
@@ -51,23 +49,28 @@ export function getQueryBuilderFromListParams<Type>(options: {
51
49
  return false
52
50
  }
53
51
  if (!searchables.includes(filter.name)) {
54
- console.warn('"searchables" setting is required for like searches with a heavy database query load', filter.name)
52
+ console.warn(
53
+ '"searchables" setting is required for LIKE searches to avoid heavy database load',
54
+ filter.name
55
+ )
55
56
  return false
56
57
  }
57
58
  return true
58
59
  }) || []
59
60
  : []
61
+
60
62
  const pagination = params.pagination
61
63
  const sortings = params.sortings
62
-
63
64
  const metadata = repository.metadata
64
65
 
65
- if (columnFilters && columnFilters.length > 0) {
66
+ // Apply column filters
67
+ if (columnFilters.length > 0) {
66
68
  columnFilters.forEach(filter => {
67
69
  addCondition(metadata, selectQueryBuilder, selectQueryBuilder, filter, filtersMap, true)
68
70
  })
69
71
  }
70
72
 
73
+ // Apply search filters
71
74
  if (searchFilters.length > 0) {
72
75
  selectQueryBuilder.andWhere(
73
76
  new Brackets(qb => {
@@ -78,48 +81,58 @@ export function getQueryBuilderFromListParams<Type>(options: {
78
81
  )
79
82
  }
80
83
 
84
+ // Apply domain filters
81
85
  if (domain) {
82
- if (!inherited || inherited == InheritedValueType.None) {
86
+ if (!inherited || inherited === InheritedValueType.None) {
83
87
  selectQueryBuilder.andWhere(`${entityAlias}.domain = :domain`, { domain: domain.id })
84
- } else if (inherited == InheritedValueType.Include) {
85
- selectQueryBuilder.andWhere(`${entityAlias}.domain In(:...domains)`, {
88
+ } else if (inherited === InheritedValueType.Include) {
89
+ selectQueryBuilder.andWhere(`${entityAlias}.domain IN (:...domains)`, {
86
90
  domains: [domain.id, domain.parentId].filter(Boolean)
87
91
  })
88
- } else if (inherited == InheritedValueType.Only) {
92
+ } else if (inherited === InheritedValueType.Only) {
89
93
  selectQueryBuilder.andWhere(`${entityAlias}.domain = :domain`, { domain: domain.parentId || 'Impossible' })
90
94
  } else {
91
95
  selectQueryBuilder.andWhere(`${entityAlias}.domain = :domain`, { domain: 'Impossible' })
92
96
  }
93
97
  }
94
98
 
95
- if (pagination && pagination.page > 0 && pagination.limit > 0) {
96
- selectQueryBuilder.skip(pagination.limit * (pagination.page - 1))
97
- selectQueryBuilder.take(pagination.limit)
98
- }
99
+ // Apply pagination
100
+ addPagination(selectQueryBuilder, pagination)
99
101
 
102
+ // Apply sorting
100
103
  if (sortings && sortings.length > 0) {
101
- sortings.forEach((sorting, index) => {
102
- const sortField = sorting.name.split('.').length > 1 ? sorting.name : `${entityAlias}.${sorting.name}`
103
- if (index === 0) {
104
- selectQueryBuilder.orderBy(sortField, sorting.desc ? 'DESC' : 'ASC')
105
- } else {
106
- selectQueryBuilder.addOrderBy(sortField, sorting.desc ? 'DESC' : 'ASC')
107
- }
108
- })
104
+ addSorting(selectQueryBuilder, sortings, entityAlias, filtersMap, metadata)
109
105
  }
110
106
 
111
107
  return selectQueryBuilder
112
108
  }
113
109
 
114
110
  /**
115
- * Add a condition to a TypeORM SelectQueryBuilder based on the provided filter and mapping options.
111
+ * Adds pagination to the SelectQueryBuilder based on the provided Pagination object.
112
+ *
113
+ * @param selectQueryBuilder - The SelectQueryBuilder to which pagination should be applied.
114
+ * @param pagination - The Pagination object containing page and limit information.
115
+ */
116
+ function addPagination<T>(selectQueryBuilder: SelectQueryBuilder<T>, pagination?: Pagination) {
117
+ if (pagination) {
118
+ const { page, limit } = pagination
119
+ if (page && limit && page > 0 && limit > 0) {
120
+ selectQueryBuilder.skip(limit * (page - 1)).take(limit)
121
+ } else if (limit && limit > 0) {
122
+ selectQueryBuilder.take(limit)
123
+ }
124
+ }
125
+ }
126
+
127
+ /**
128
+ * Adds a filtering condition to the SelectQueryBuilder based on the provided filter and mapping options.
116
129
  *
117
- * @param {EntityMetadata} metadata - The EntityMetadata for the TypeORM entity.
118
- * @param {SelectQueryBuilder<Type>} selectQueryBuilder - The SelectQueryBuilder to add the condition to.
119
- * @param {WhereExpressionBuilder} whereExpressionBuilder - The WhereExpressionBuilder to build the condition.
120
- * @param {Filter} filter - The filter object containing the filter conditions.
121
- * @param {Object} filtersMap - A mapping of filter names to column names and relation column names.
122
- * @param {boolean} andCondition - Flag indicating whether to use "AND" or "OR" for combining conditions.
130
+ * @param metadata - The EntityMetadata of the TypeORM entity.
131
+ * @param selectQueryBuilder - The SelectQueryBuilder to which the condition will be added.
132
+ * @param whereExpressionBuilder - The WhereExpressionBuilder to construct the where clause.
133
+ * @param filter - The Filter object containing the filter criteria.
134
+ * @param filtersMap - A mapping of filter names to column names and relation column names.
135
+ * @param andCondition - A flag indicating whether to use "AND" or "OR" for combining conditions.
123
136
  */
124
137
  function addCondition<T>(
125
138
  metadata: EntityMetadata,
@@ -130,210 +143,251 @@ function addCondition<T>(
130
143
  andCondition: boolean
131
144
  ): void {
132
145
  const { name, operator, value } = filter
133
- const values = value instanceof Array ? value : [value]
134
- var entityAlias = selectQueryBuilder.alias
135
-
136
- var { relationColumn, columnName } = filtersMap[name] || {}
137
- /*
138
- 1. relationColumn과 columnName이 지정된 경우
139
- - relation inverse 테이블에서, columnName을 찾는다.
140
- 2. relationColumn만 지정된 경우는 없어야 한다.
141
- - 이 경우 columnName 은 'name' 이라고 판단한다.
142
- 3. columnName이 지정된 경우.
143
- - 이 경우는 columnName 만 적용한다.
144
- */
146
+ let entityAlias = selectQueryBuilder.alias
147
+
148
+ const { relationColumn, columnName } = filtersMap[name] || {}
149
+
145
150
  if (relationColumn) {
146
- const columns = relationColumn.split('.')
147
- var entityMetadata: EntityMetadata
148
- var relation: RelationMetadata
149
-
150
- for (const rcolumn of columns) {
151
- if (relation) {
152
- const { propertyName } = relationColumnMeta
153
- const property = `${entityAlias}.${propertyName}`
154
-
155
- entityAlias = `${entityAlias}-${entityMetadata.tableName}-for-${columnName || 'name'}` as string
156
-
157
- if (andCondition) {
158
- selectQueryBuilder.innerJoin(property, entityAlias)
159
- } else {
160
- selectQueryBuilder.leftJoin(property, entityAlias)
161
- }
162
- } else {
163
- entityMetadata = metadata
164
- }
151
+ entityAlias = applyJoins(
152
+ selectQueryBuilder,
153
+ entityAlias,
154
+ relationColumn,
155
+ metadata,
156
+ andCondition ? 'innerJoin' : 'leftJoin',
157
+ columnName || name
158
+ )
159
+ }
165
160
 
166
- var relationColumnMeta = entityMetadata.columns.find(column => column.propertyName === rcolumn)
167
- if (!relationColumnMeta) {
168
- console.warn(`relationColumn "${relationColumn}" in filtersMap for "${name}" is not a relation column`)
169
- return
170
- }
161
+ const field = `${entityAlias}.${columnName || name}`
162
+ const { clause, parameters } = getClauseAndParameters(field, name, operator, value)
171
163
 
172
- relation = relationColumnMeta.relationMetadata
173
- entityMetadata = relation.inverseEntityMetadata
174
- }
164
+ if (andCondition) {
165
+ whereExpressionBuilder.andWhere(clause, parameters)
166
+ } else {
167
+ whereExpressionBuilder.orWhere(clause, parameters)
168
+ }
169
+ }
175
170
 
176
- var columnMeta = entityMetadata.columns.find(column => column.propertyName === (columnName || 'name'))
177
- if (!columnMeta) {
178
- console.warn(`columnName "${columnName}" in filtersMap for "${name}" is not a column`)
179
- return
171
+ /**
172
+ * Adds sorting to the SelectQueryBuilder based on the provided Sorting objects.
173
+ *
174
+ * @param selectQueryBuilder - The SelectQueryBuilder to which sorting should be applied.
175
+ * @param sortings - An array of Sorting objects defining the sort order.
176
+ * @param entityAlias - The alias of the entity in the query.
177
+ * @param filtersMap - A mapping of filter names to column names and relation column names.
178
+ * @param metadata - The EntityMetadata of the TypeORM entity.
179
+ */
180
+ function addSorting<T>(
181
+ selectQueryBuilder: SelectQueryBuilder<T>,
182
+ sortings: Sorting[],
183
+ entityAlias: string,
184
+ filtersMap: { [name: string]: { columnName: string; relationColumn?: string } },
185
+ metadata: EntityMetadata
186
+ ) {
187
+ sortings.forEach((sorting, index) => {
188
+ const sortField = determineSortField(sorting.name, entityAlias, filtersMap, selectQueryBuilder, metadata)
189
+ if (index === 0) {
190
+ selectQueryBuilder.orderBy(sortField, sorting.desc ? 'DESC' : 'ASC')
191
+ } else {
192
+ selectQueryBuilder.addOrderBy(sortField, sorting.desc ? 'DESC' : 'ASC')
180
193
  }
194
+ })
195
+ }
196
+
197
+ /**
198
+ * Determines the sorting field for a given sorting name, considering possible relation columns.
199
+ *
200
+ * @param sortingName - The name of the field to sort by.
201
+ * @param entityAlias - The alias of the entity in the query.
202
+ * @param filtersMap - A mapping of filter names to column names and relation column names.
203
+ * @param selectQueryBuilder - The SelectQueryBuilder instance to apply sorting to.
204
+ * @param metadata - The EntityMetadata of the TypeORM entity.
205
+ * @returns {string} - The fully qualified sorting field.
206
+ */
207
+ function determineSortField<T>(
208
+ sortingName: string,
209
+ entityAlias: string,
210
+ filtersMap: { [name: string]: { columnName: string; relationColumn?: string } },
211
+ selectQueryBuilder: SelectQueryBuilder<T>,
212
+ metadata: EntityMetadata
213
+ ): string {
214
+ const filter = filtersMap[sortingName]
215
+
216
+ if (!filter) {
217
+ return `${entityAlias}.${sortingName}`
218
+ }
219
+
220
+ const { columnName, relationColumn } = filter
221
+
222
+ if (relationColumn) {
223
+ const relationAlias = applyJoins(
224
+ selectQueryBuilder,
225
+ entityAlias,
226
+ relationColumn,
227
+ metadata,
228
+ 'leftJoin',
229
+ columnName || sortingName,
230
+ true
231
+ )
232
+ return `${relationAlias}.${columnName}`
181
233
  } else {
182
- var columnMeta = metadata.columns.find(column => column.propertyName === (columnName || name))
183
- if (!columnMeta) {
184
- /* relationId 에 대한 필터링은 해당 컬럼값 자체의 비교로 한다. */
185
- var relationIdMeta = metadata.relationIds.find(relationId => relationId.propertyName === (columnName || name))
186
- if (relationIdMeta) {
187
- columnMeta = relationIdMeta.relation.joinColumns[0]
188
- } else {
189
- columnName ? console.warn(`columnName "${columnName}" in filtersMap for "${name}" is not a column`) : console.warn(`name "${name}" is not a column`)
190
- }
191
- } else {
192
- var relation = columnMeta.relationMetadata
234
+ return `${entityAlias}.${columnName}`
235
+ }
236
+ }
237
+
238
+ /**
239
+ * Applies the necessary joins to the SelectQueryBuilder based on the relation column.
240
+ *
241
+ * @param selectQueryBuilder - The SelectQueryBuilder where the joins will be applied.
242
+ * @param entityAlias - The current alias of the entity in the query.
243
+ * @param relationColumn - The dot-notated string representing the relation chain (e.g., "user.profile.address").
244
+ * @param metadata - The EntityMetadata of the entity.
245
+ * @param joinType - The type of join to use ("innerJoin" or "leftJoin").
246
+ * @param columnName - The name of the column used for filtering or sorting, default to 'name'.
247
+ * @param selectField - Whether to include the field in the SELECT clause.
248
+ * @returns {string} - The alias to be used for the final field in the relation chain.
249
+ */
250
+ function applyJoins<T>(
251
+ selectQueryBuilder: SelectQueryBuilder<T>,
252
+ entityAlias: string,
253
+ relationColumn: string,
254
+ metadata: EntityMetadata,
255
+ joinType: 'innerJoin' | 'leftJoin' = 'leftJoin',
256
+ columnName: string = 'name',
257
+ selectField: boolean = false
258
+ ): string {
259
+ const columns = relationColumn.split('.')
260
+ let currentAlias = entityAlias
261
+ let currentMetadata = metadata
262
+
263
+ for (const column of columns) {
264
+ const relation = currentMetadata.relations.find(rel => rel.propertyName === column)
265
+
266
+ if (!relation) {
267
+ throw new Error(`Relation not found for column: ${column}`)
193
268
  }
194
269
 
195
- if (relation) {
196
- /* filterMap에 의해서 relationColumn 이 지정되지 않았더라도, name 또는 columnName의 column이 relation인 경우에는
197
- - 조건절 구성을 위한 타겟필드명은 'name' 으로만 한정된다.
198
- */
199
- var relationColumnMeta = columnMeta
200
- var entityMetadata = relation.inverseEntityMetadata
201
- columnMeta = entityMetadata.columns.find(column => column.propertyName === 'name')
202
- if (!columnMeta) {
203
- console.warn(`relation column "${columnName || name}" does not have "name" column`)
204
- return
205
- }
270
+ const nextAlias = `${currentAlias}_${relation.inverseEntityMetadata.tableName}_for_${columnName}`
271
+
272
+ if (!selectQueryBuilder.expressionMap.aliases.some(alias => alias.name === nextAlias)) {
273
+ selectQueryBuilder[joinType](`${currentAlias}.${column}`, nextAlias)
274
+ }
275
+ if (selectField && columns.at(-1) == column /* 최종 alias만 추가 */) {
276
+ selectQueryBuilder.addSelect(`${nextAlias}.${columnName}`, `${nextAlias}_${columnName}`)
206
277
  }
207
- }
208
278
 
209
- const dbNameForColumn = columnMeta.databaseName
210
- const alias = relationColumnMeta ? `${name}-filter` : entityAlias
279
+ currentAlias = nextAlias
280
+ currentMetadata = relation.inverseEntityMetadata
281
+ }
211
282
 
212
- /* relation columne인 경우 name을 alias로 사용한다. */
213
- const field = `${alias}.${dbNameForColumn}`
283
+ return currentAlias
284
+ }
214
285
 
215
- var clause = ''
216
- var parameters = {}
286
+ /**
287
+ * Generates the SQL clause and parameters based on the provided filter.
288
+ *
289
+ * @param field - The database field to filter on.
290
+ * @param name - The name of the filter.
291
+ * @param operator - The operator to use in the filter.
292
+ * @param value - The value to filter with.
293
+ * @returns An object containing the SQL clause and the parameters.
294
+ */
295
+ function getClauseAndParameters(
296
+ field: string,
297
+ name: string,
298
+ operator: string,
299
+ value: any
300
+ ): { clause: string; parameters: { [key: string]: any } } {
301
+ const values = value instanceof Array ? value : [value]
302
+ let clause = ''
303
+ let parameters: { [key: string]: any } = {}
217
304
 
218
305
  switch (operator) {
219
306
  case 'eq':
220
307
  clause = `${field} = :${name}`
221
308
  parameters = { [name]: value }
222
309
  break
223
-
224
310
  case 'like':
225
311
  clause = `${field} LIKE :${name}`
226
312
  parameters = { [name]: `%${value}%` }
227
313
  break
228
-
229
314
  case 'search':
230
315
  case 'i_like':
231
316
  clause = `LOWER(${field}) LIKE :${name}`
232
317
  parameters = { [name]: `%${String(value).toLowerCase()}%` }
233
318
  break
234
-
235
319
  case 'nlike':
236
320
  clause = `${field} NOT LIKE :${name}`
237
321
  parameters = { [name]: `%${value}%` }
238
322
  break
239
-
240
323
  case 'i_nlike':
241
324
  clause = `LOWER(${field}) NOT LIKE :${name}`
242
325
  parameters = { [name]: `%${String(value).toLowerCase()}%` }
243
326
  break
244
-
245
327
  case 'lt':
246
328
  clause = `${field} < :${name}`
247
329
  parameters = { [name]: value }
248
330
  break
249
-
250
331
  case 'gt':
251
332
  clause = `${field} > :${name}`
252
333
  parameters = { [name]: value }
253
334
  break
254
-
255
335
  case 'lte':
256
336
  clause = `${field} <= :${name}`
257
337
  parameters = { [name]: value }
258
338
  break
259
-
260
339
  case 'gte':
261
340
  clause = `${field} >= :${name}`
262
341
  parameters = { [name]: value }
263
342
  break
264
-
265
343
  case 'noteq':
266
344
  clause = `${field} != :${name}`
267
345
  parameters = { [name]: value }
268
346
  break
269
-
270
347
  case 'in':
271
348
  clause = `${field} IN (:...${name})`
272
349
  parameters = { [name]: values }
273
350
  break
274
-
275
351
  case 'notin':
276
352
  clause = `${field} NOT IN (:...${name})`
277
353
  parameters = { [name]: values }
278
354
  break
279
-
280
355
  case 'notin_with_null':
281
- clause = `${field} IS NULL OR ${field} NOT IN (:...${name}))`
356
+ clause = `${field} IS NULL OR ${field} NOT IN (:...${name})`
282
357
  parameters = { [name]: values }
283
358
  break
284
-
285
359
  case 'is_null':
286
360
  clause = `${field} IS NULL`
287
361
  break
288
-
289
362
  case 'is_not_null':
290
363
  clause = `${field} IS NOT NULL`
291
364
  break
292
-
293
365
  case 'is_false':
294
366
  clause = `${field} IS FALSE`
295
367
  break
296
-
297
368
  case 'is_true':
298
369
  clause = `${field} IS TRUE`
299
370
  break
300
-
301
371
  case 'is_not_false':
302
372
  clause = `${field} IS NOT FALSE`
303
373
  break
304
-
305
374
  case 'is_not_true':
306
375
  clause = `${field} IS NOT TRUE`
307
376
  break
308
-
309
377
  case 'is_present':
310
378
  clause = `${field} IS PRESENT`
311
379
  break
312
-
313
380
  case 'is_blank':
314
381
  clause = `${field} IS BLANK`
315
382
  break
316
-
317
383
  case 'is_empty_num_id':
318
384
  clause = `${field} IS EMPTY NUMERIC ID`
319
385
  break
320
-
321
386
  case 'between':
322
387
  clause = `${field} BETWEEN :${name}_1 AND :${name}_2`
323
388
  parameters = { [`${name}_1`]: values[0], [`${name}_2`]: values[1] }
324
389
  break
325
390
  }
326
391
 
327
- if (relationColumnMeta) {
328
- const { propertyName } = relationColumnMeta
329
- const property = `${entityAlias}.${propertyName}`
330
- if (andCondition) {
331
- selectQueryBuilder.innerJoin(property, alias, clause, parameters)
332
- } else {
333
- selectQueryBuilder.leftJoin(property, alias)
334
- whereExpressionBuilder.orWhere(clause, parameters)
335
- }
336
- } else {
337
- andCondition ? whereExpressionBuilder.andWhere(clause, parameters) : whereExpressionBuilder.orWhere(clause, parameters)
338
- }
392
+ return { clause, parameters }
339
393
  }