@things-factory/shell 10.1.234 → 10.1.246
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/dist-server/service/common-types/list-param.d.ts +4 -0
- package/dist-server/service/common-types/list-param.js +15 -1
- package/dist-server/service/common-types/list-param.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/dist-server/utils/get-query-builder-from-list-params.d.ts +29 -2
- package/dist-server/utils/get-query-builder-from-list-params.js +45 -2
- package/dist-server/utils/get-query-builder-from-list-params.js.map +1 -1
- package/package.json +2 -2
- package/tests/get-query-builder-features.test.ts +72 -2
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { Repository, SelectQueryBuilder } from 'typeorm';
|
|
2
|
-
import { ListParam } from '../service/common-types/list-param.js';
|
|
1
|
+
import { EntityMetadata, Repository, SelectQueryBuilder } from 'typeorm';
|
|
2
|
+
import { Sorting, ListParam } from '../service/common-types/list-param.js';
|
|
3
3
|
import { Domain } from '../service/domain/domain.js';
|
|
4
4
|
/**
|
|
5
5
|
* `search` 연산자의 매칭 방식.
|
|
@@ -87,4 +87,31 @@ export declare function getQueryBuilderFromListParams<Type>(options: {
|
|
|
87
87
|
* 행이 건너뛰어지거나 반복된다.
|
|
88
88
|
*/
|
|
89
89
|
cursorTiebreak?: string;
|
|
90
|
+
/**
|
|
91
|
+
* The order this list has when the reader sorts by nothing - and, after the reader's columns, the order that
|
|
92
|
+
* settles their ties. Left out, the newest row comes first (`createdAt`, when the entity has it): the latest on top
|
|
93
|
+
* is the rule for every list (the user, 2026-11-02). The primary key closes it, so a page boundary never repeats or
|
|
94
|
+
* drops a row. Keyset paging keeps its own axes (`cursorSortings`) and is not touched.
|
|
95
|
+
*/
|
|
96
|
+
defaultSortings?: Sorting[];
|
|
90
97
|
}): SelectQueryBuilder<Type>;
|
|
98
|
+
/**
|
|
99
|
+
* The reader's columns, then the list's own order (`defaultSortings`, or the newest first by `createdAt`), then the
|
|
100
|
+
* primary key - each column once, the reader's way kept where they named it.
|
|
101
|
+
*/
|
|
102
|
+
export declare function settledSortings(sortings: Sorting[] | undefined, defaultSortings: Sorting[] | undefined, metadata: EntityMetadata, tiebreak: string | undefined): Sorting[];
|
|
103
|
+
/**
|
|
104
|
+
* How many rows of a list hold each value of one field - the numbers beside a list's filter chips (an opportunity's
|
|
105
|
+
* stages, a request's statuses). Counted by the database, grouped by the field, under the list's own conditions:
|
|
106
|
+
* the same options as the list (domain, filters, search, searchables ...), less the conditions on `field` itself, so
|
|
107
|
+
* each chip says how many it would show whichever chips are pressed. The list's pagination and sorting play no part;
|
|
108
|
+
* the grouped rows are left unsorted (the builder sorts a list newest first, which a GROUP BY cannot carry).
|
|
109
|
+
*
|
|
110
|
+
* `field` is a plain column of the entity. A value no row holds is absent from the map - a caller lists its own values,
|
|
111
|
+
* each as a `ListValueCount`.
|
|
112
|
+
*/
|
|
113
|
+
export declare function countListByField<Type>(options: Parameters<typeof getQueryBuilderFromListParams<Type>>[0] & {
|
|
114
|
+
field: string;
|
|
115
|
+
/** The conditions a list door adds of its own after the builder (the caller's own rows, say) - added here alike. */
|
|
116
|
+
narrow?: (builder: SelectQueryBuilder<Type>) => void;
|
|
117
|
+
}): Promise<Map<string, number>>;
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.getQueryBuilderFromListParams = getQueryBuilderFromListParams;
|
|
4
|
+
exports.settledSortings = settledSortings;
|
|
5
|
+
exports.countListByField = countListByField;
|
|
4
6
|
const typeorm_1 = require("typeorm");
|
|
5
7
|
const DriverUtils_js_1 = require("typeorm/driver/DriverUtils.js");
|
|
6
8
|
const list_param_js_1 = require("../service/common-types/list-param.js");
|
|
@@ -49,7 +51,7 @@ const nextParamKey = (name) => `${name.replace(/[^A-Za-z0-9_]/g, '_')}_p${paramS
|
|
|
49
51
|
* @returns {SelectQueryBuilder<Type>} The constructed SelectQueryBuilder (not yet executed).
|
|
50
52
|
*/
|
|
51
53
|
function getQueryBuilderFromListParams(options) {
|
|
52
|
-
const { repository, params, domain, alias, searchables, filtersMap = {}, columnExpressions = {}, searchMode = 'contains', defaultLimit, maxLimit, sortables, filterables, cursorTiebreak } = options;
|
|
54
|
+
const { repository, params, domain, alias, searchables, filtersMap = {}, columnExpressions = {}, searchMode = 'contains', defaultLimit, maxLimit, sortables, filterables, cursorTiebreak, defaultSortings } = options;
|
|
53
55
|
const { inherited = list_param_js_1.InheritedValueType.None } = params || {};
|
|
54
56
|
const selectQueryBuilder = repository.createQueryBuilder(alias);
|
|
55
57
|
const entityAlias = selectQueryBuilder.alias;
|
|
@@ -140,7 +142,7 @@ function getQueryBuilderFromListParams(options) {
|
|
|
140
142
|
*/
|
|
141
143
|
const cursor = pagination?.after;
|
|
142
144
|
const tiebreak = cursorTiebreak ?? metadata.primaryColumns[0]?.propertyName;
|
|
143
|
-
const effectiveSortings = cursor && tiebreak ? (0, keyset_cursor_js_1.cursorSortings)(sortings, tiebreak) : sortings;
|
|
145
|
+
const effectiveSortings = cursor && tiebreak ? (0, keyset_cursor_js_1.cursorSortings)(sortings, tiebreak) : settledSortings(sortings, defaultSortings, metadata, tiebreak);
|
|
144
146
|
// Apply pagination
|
|
145
147
|
addPagination(selectQueryBuilder, pagination, {
|
|
146
148
|
defaultLimit,
|
|
@@ -161,6 +163,19 @@ function getQueryBuilderFromListParams(options) {
|
|
|
161
163
|
}
|
|
162
164
|
return selectQueryBuilder;
|
|
163
165
|
}
|
|
166
|
+
/**
|
|
167
|
+
* The reader's columns, then the list's own order (`defaultSortings`, or the newest first by `createdAt`), then the
|
|
168
|
+
* primary key - each column once, the reader's way kept where they named it.
|
|
169
|
+
*/
|
|
170
|
+
function settledSortings(sortings, defaultSortings, metadata, tiebreak) {
|
|
171
|
+
const own = defaultSortings ?? (metadata.findColumnWithPropertyName('createdAt') ? [{ name: 'createdAt', desc: true }] : []);
|
|
172
|
+
const settled = [];
|
|
173
|
+
for (const sorting of [...(sortings ?? []), ...own, ...(tiebreak ? [{ name: tiebreak, desc: true }] : [])]) {
|
|
174
|
+
if (!settled.some(one => one.name === sorting.name))
|
|
175
|
+
settled.push(sorting);
|
|
176
|
+
}
|
|
177
|
+
return settled;
|
|
178
|
+
}
|
|
164
179
|
/**
|
|
165
180
|
* Adds pagination to the SelectQueryBuilder based on the provided Pagination object.
|
|
166
181
|
*
|
|
@@ -559,4 +574,32 @@ function getClauseAndParameters(field, name, operator, value, searchMode = 'cont
|
|
|
559
574
|
}
|
|
560
575
|
return { clause, parameters };
|
|
561
576
|
}
|
|
577
|
+
/**
|
|
578
|
+
* How many rows of a list hold each value of one field - the numbers beside a list's filter chips (an opportunity's
|
|
579
|
+
* stages, a request's statuses). Counted by the database, grouped by the field, under the list's own conditions:
|
|
580
|
+
* the same options as the list (domain, filters, search, searchables ...), less the conditions on `field` itself, so
|
|
581
|
+
* each chip says how many it would show whichever chips are pressed. The list's pagination and sorting play no part;
|
|
582
|
+
* the grouped rows are left unsorted (the builder sorts a list newest first, which a GROUP BY cannot carry).
|
|
583
|
+
*
|
|
584
|
+
* `field` is a plain column of the entity. A value no row holds is absent from the map - a caller lists its own values,
|
|
585
|
+
* each as a `ListValueCount`.
|
|
586
|
+
*/
|
|
587
|
+
async function countListByField(options) {
|
|
588
|
+
const { field, params, narrow, ...rest } = options;
|
|
589
|
+
const builder = getQueryBuilderFromListParams({
|
|
590
|
+
...rest,
|
|
591
|
+
params: {
|
|
592
|
+
filters: (params?.filters ?? []).filter(filter => filter.name !== field),
|
|
593
|
+
search: params?.search,
|
|
594
|
+
inherited: params?.inherited
|
|
595
|
+
},
|
|
596
|
+
/* The groups are bounded by the field's values, not by the table; this only keeps the builder's unbounded warning quiet. */
|
|
597
|
+
defaultLimit: 1000,
|
|
598
|
+
maxLimit: undefined
|
|
599
|
+
});
|
|
600
|
+
narrow?.(builder);
|
|
601
|
+
const column = `${builder.alias}.${field}`;
|
|
602
|
+
const rows = await builder.select(column, 'value').addSelect('COUNT(*)', 'count').groupBy(column).orderBy().getRawMany();
|
|
603
|
+
return new Map(rows.map(row => [String(row.value), Number(row.count)]));
|
|
604
|
+
}
|
|
562
605
|
//# sourceMappingURL=get-query-builder-from-list-params.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"get-query-builder-from-list-params.js","sourceRoot":"","sources":["../../server/utils/get-query-builder-from-list-params.ts"],"names":[],"mappings":"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","sourcesContent":["import { Brackets, EntityMetadata, Repository, SelectQueryBuilder, WhereExpressionBuilder } from 'typeorm'\nimport { DriverUtils } from 'typeorm/driver/DriverUtils.js'\nimport { RelationMetadata } from 'typeorm/metadata/RelationMetadata.js'\nimport { Filter, Sorting, Pagination, ListParam, InheritedValueType } from '../service/common-types/list-param.js'\nimport { Domain } from '../service/domain/domain.js'\nimport { applyDomainInheritanceFilter } from './apply-domain-filter.js'\nimport { applyKeysetCursor, cursorSortings } from './keyset-cursor.js'\nimport { Refusal } from '../refusal/refusal.js'\nimport { membershipClause } from './membership-clause.js'\n\n/**\n * `search` 연산자의 매칭 방식.\n * - contains(기본): `%v%` — 어디든 포함(유연하나 인덱스 미활용).\n * - startsWith: `v%` — 접두 일치(인덱스 활용 가능 → 대용량에서 빠름).\n * - exact: `v` — 정확 일치(대소문자 무시).\n */\nexport type SearchMode = 'contains' | 'startsWith' | 'exact'\n\n/*\n * 바인딩 파라미터 이름 발급기 — 같은 컬럼에 조건이 여럿 걸려도 서로 값을 덮어쓰지 않게 한다.\n * 전역 증가 카운터로 충분하다(파라미터는 질의 하나 안에서만 의미가 있고, 유일하기만 하면 된다).\n */\nlet paramSeq = 0\nconst nextParamKey = (name: string): string => `${name.replace(/[^A-Za-z0-9_]/g, '_')}_p${paramSeq++}`\n\n/**\n * Builds a TypeORM SelectQueryBuilder from a generic ListParam (filters, search, sortings,\n * pagination) plus optional multi-tenant domain scoping. Returns the builder so callers can\n * further chain joins/select/where before executing (e.g. getMany / getManyAndCount).\n *\n * Supported filter operators (`Filter.operator`):\n * eq, noteq, like, nlike, i_like, i_nlike, search, lt, gt, lte, gte,\n * in, notin, notin_with_null, between,\n * is_null, is_not_null, is_true, is_false, is_not_true, is_not_false,\n * is_present, is_blank, is_empty_num_id.\n * An unknown operator throws a clear, actionable error.\n *\n * Behavior notes:\n * - `search` uses case-insensitive substring match (`LOWER(col) LIKE %v%`) and requires the column\n * to be listed in `searchables`.\n * - `like`/`i_like` filters are IGNORED (with a console.warn) unless the column is in `searchables`,\n * to avoid unindexed full-table scans.\n * - Boolean operators are emitted as parameter-bound comparisons (portable across all supported DBs).\n * - Domain scoping is applied only when `domain` is provided, honoring `params.inherited`\n * (None = own domain, Include = own + parent, Only = parent).\n * - Pagination uses skip/take; with joins this triggers TypeORM's two-query strategy for correctness.\n * - Unknown filter columns / operators / relations throw a clear, actionable error (fail-fast).\n *\n * @param options.repository TypeORM repository (or a tx-scoped repository) to build from.\n * @param options.params ListParam: `{ filters, sortings, pagination, inherited }`.\n * @param [options.domain] Optional current domain `{ id, parentId }` for tenant scoping.\n * @param [options.alias] Optional entity alias (defaults to the entity name).\n * @param [options.searchables] Columns eligible for `search`/`like` filtering.\n * @param [options.filtersMap] Maps a filter/sort name to a column or relation column, e.g.\n * `{ ownerName: { columnName: 'name', relationColumn: 'owner' } }`.\n * @param [options.searchMode] `search` matching mode: 'contains'(default) / 'startsWith' / 'exact'.\n * 'startsWith' emits `v%` (prefix) which can use an index on large tables.\n * @returns {SelectQueryBuilder<Type>} The constructed SelectQueryBuilder (not yet executed).\n */\nexport function getQueryBuilderFromListParams<Type>(options: {\n repository: Repository<Type>\n params: ListParam\n domain?: Domain\n alias?: string\n searchables?: string[]\n filtersMap?: { [name: string]: { columnName: string; relationColumn?: string } }\n /** Trusted server-defined joined column expressions. Never populate this from request input. */\n columnExpressions?: Record<string, string>\n searchMode?: SearchMode\n /**\n * 페이지 크기를 안 준 호출에 씌울 기본 상한.\n *\n * 지정하지 않으면 **예전과 똑같이 무제한**이다(하위호환). 다만 무제한으로 나가는 질의는 경고를 남긴다 —\n * 표가 작을 때는 아무 문제가 없다가 데이터가 늘면 조용히 무너지는 종류의 결함이라, 늘기 전에 보여야 한다.\n */\n defaultLimit?: number\n /** 호출자가 요구한 페이지 크기의 상한. 넘겨도 여기서 잘린다(서버 보호). */\n maxLimit?: number\n /**\n * 정렬 허용 컬럼. 주면 목록에 없는 정렬은 **명시 실패**한다.\n *\n * `searchables` 로 인덱스 없는 LIKE 는 막으면서 정렬은 아무 컬럼이나 받던 비대칭을 메운다 —\n * 인덱스 없는 컬럼 정렬은 큰 표에서 전체 정렬 스캔을 부른다.\n */\n sortables?: string[]\n /**\n * 거르기 허용 컬럼. 주면 목록에 없는 칸의 거르기(검색 연산 포함)는 **명시 실패**한다.\n *\n * 고르기 문(`<개념>Choices`, ADR-0139)이 쓴다. 고르기 문은 가리키는 칸만 내는데, 숨긴 칸으로 거를 수\n * 있으면 결과에 끼는지 아닌지로 그 칸의 내용을 맞혀 볼 수 있다. 내는 칸만 거를 수 있게 한다.\n */\n filterables?: string[]\n /**\n * 커서 페이징의 타이브레이커 컬럼. 기본은 엔티티 PK.\n *\n * 마지막 정렬 축은 반드시 **유일**해야 한다 — 같은 값이 여러 행에 있으면 페이지 경계에서\n * 행이 건너뛰어지거나 반복된다.\n */\n cursorTiebreak?: string\n}): SelectQueryBuilder<Type> {\n const {\n repository,\n params,\n domain,\n alias,\n searchables,\n filtersMap = {},\n columnExpressions = {},\n searchMode = 'contains',\n defaultLimit,\n maxLimit,\n sortables,\n filterables,\n cursorTiebreak\n } = options\n const { inherited = InheritedValueType.None } = params || {}\n\n const selectQueryBuilder = repository.createQueryBuilder(alias)\n const entityAlias = selectQueryBuilder.alias\n\n if (filterables) {\n for (const filter of params?.filters ?? []) {\n if (!filterables.includes(filter.name)) {\n throw new Error(\n `[getQueryBuilderFromListParams] filtering by \"${filter.name}\" is not allowed on entity \"${repository.metadata.name}\". ` +\n `Allowed: ${filterables.join(', ')}`\n )\n }\n }\n }\n\n // Apply filters to the query\n const columnFilters =\n params.filters?.filter(filter => {\n if (filter.operator === 'search') {\n return false\n }\n if (filter.operator.toLowerCase().includes('like') && (!searchables || !searchables.includes(filter.name))) {\n console.warn('\"searchables\" setting is required for LIKE searches to avoid heavy database load', filter.name)\n return false\n }\n return true\n }) || []\n\n /*\n * 전역 검색어(`params.search`) → 서버가 자기 `searchables` 로 필드별 search 필터를 만든다.\n * 클라이언트가 필드 목록을 알 필요가 없고, 서버 목록과 어긋나 조용히 무시될 일도 없다.\n * 아래 searchFilters 와 같은 통(OR 묶음)에 들어가므로, filters 로 보낸 것과 함께 써도 자연스럽게 합쳐진다.\n */\n const term = params.search?.trim()\n /*\n * A term this door cannot resolve is refused by name, not dropped (ADR-0060 decision 10). Dropped,\n * the whole list came back and read as \"nothing matches\" - the screen and the door disagreed and\n * the person paid for it. An empty list is the same case: it used to pass the Array check and drop\n * the term without even a warning. The old per-column path (`filters` with operator search) does\n * not come through here.\n */\n if (term && !(searchables instanceof Array && searchables.length > 0)) {\n const entity = repository.metadata.name\n throw new Refusal('list-not-searchable', { entity }, `the ${entity} list cannot be searched: it declares no searchable fields`)\n }\n const termFilters: Filter[] = term ? searchables!.map(name => ({ name, operator: 'search', value: term })) : []\n\n const searchFilters =\n searchables instanceof Array\n ? params.filters?.filter(filter => {\n if (filter.operator !== 'search') {\n return false\n }\n if (!searchables.includes(filter.name)) {\n console.warn(\n '\"searchables\" setting is required for LIKE searches to avoid heavy database load',\n filter.name\n )\n return false\n }\n return true\n }) || []\n : []\n\n const pagination = params.pagination\n const sortings = params.sortings\n const metadata = repository.metadata\n\n // Apply column filters\n if (columnFilters.length > 0) {\n columnFilters.forEach(filter => {\n addCondition(metadata, selectQueryBuilder, selectQueryBuilder, filter, filtersMap, true, searchMode, columnExpressions)\n })\n }\n\n // Apply search filters (explicit per-field search filters + the expanded global term)\n const allSearchFilters = [...searchFilters, ...termFilters]\n if (allSearchFilters.length > 0) {\n selectQueryBuilder.andWhere(\n new Brackets(qb => {\n allSearchFilters.forEach(filter => {\n addCondition(metadata, selectQueryBuilder, qb, filter, filtersMap, false, searchMode, columnExpressions)\n })\n })\n )\n }\n\n // Apply domain filters\n if (domain) {\n applyDomainInheritanceFilter(selectQueryBuilder, entityAlias, domain, inherited)\n }\n\n /* 정렬 화이트리스트 검증은 **호출자가 요구한 축**에만 적용한다(아래에서 우리가 덧붙이는\n * 타이브레이커까지 목록에 넣으라고 요구하면 호출부가 내부 사정을 알아야 한다). */\n if (sortings?.length && sortables) {\n for (const sorting of sortings) {\n if (!sortables.includes(sorting.name)) {\n throw new Error(\n `[getQueryBuilderFromListParams] sorting by \"${sorting.name}\" is not allowed on entity \"${metadata.name}\". ` +\n `Allowed: ${sortables.join(', ')}`\n )\n }\n }\n }\n\n /*\n * 커서(keyset) 페이징 — `after` 가 있으면 offset 을 쓰지 않는다.\n * 정렬 축 끝에 유일 컬럼(기본 PK)을 덧붙여 \"다음 행\" 이 항상 하나로 정해지게 한다.\n */\n const cursor = pagination?.after\n const tiebreak = cursorTiebreak ?? metadata.primaryColumns[0]?.propertyName\n const effectiveSortings = cursor && tiebreak ? cursorSortings(sortings, tiebreak) : sortings\n\n // Apply pagination\n addPagination(selectQueryBuilder, pagination, {\n defaultLimit,\n maxLimit,\n entityName: metadata.name,\n keyset: !!cursor\n })\n\n if (cursor) {\n if (!tiebreak) {\n throw new Error(\n `[getQueryBuilderFromListParams] cursor paging needs a unique tiebreak column but entity \"${metadata.name}\" ` +\n 'exposes no primary column. Pass cursorTiebreak explicitly.'\n )\n }\n applyKeysetCursor(selectQueryBuilder, { alias: entityAlias, sortings: effectiveSortings!, cursor, columnExpressions })\n }\n\n // Apply sorting\n if (effectiveSortings && effectiveSortings.length > 0) {\n addSorting(selectQueryBuilder, effectiveSortings, entityAlias, filtersMap, metadata, columnExpressions)\n }\n\n return selectQueryBuilder\n}\n\n/**\n * Adds pagination to the SelectQueryBuilder based on the provided Pagination object.\n *\n * @param selectQueryBuilder - The SelectQueryBuilder to which pagination should be applied.\n * @param pagination - The Pagination object containing page and limit information.\n */\nfunction addPagination<T>(\n selectQueryBuilder: SelectQueryBuilder<T>,\n pagination?: Pagination,\n guard: { defaultLimit?: number; maxLimit?: number; entityName?: string; keyset?: boolean } = {}\n) {\n const { defaultLimit, maxLimit, entityName, keyset } = guard\n const { page, limit } = pagination ?? {}\n\n /* 요구된 페이지 크기 → 상한으로 조인다. 상한이 없으면 요구를 그대로 존중(예전 동작). */\n const asked = limit && limit > 0 ? limit : undefined\n const effective = asked === undefined ? defaultLimit : maxLimit ? Math.min(asked, maxLimit) : asked\n\n if (effective === undefined) {\n /* 상한이 아무 데도 없다 = **표 전체가 나간다.** 표가 작을 때는 멀쩡하다가 데이터가 늘면 조용히\n * 무너지는 종류라, 늘기 전에 보이게 한다. 기존 호출부를 깨지 않으려고 막지는 않는다. */\n console.warn(\n `[getQueryBuilderFromListParams] unbounded list query on \"${entityName ?? 'entity'}\" — ` +\n 'no pagination.limit from the caller and no defaultLimit on the resolver, so every matching row will be returned. ' +\n 'Set defaultLimit (and maxLimit) to keep this safe as the table grows.'\n )\n return\n }\n\n /* 커서 모드에서는 offset 을 쓰지 않는다 — 위치가 아니라 값으로 이어 붙이는 것이 커서의 요점이다.\n * (둘을 같이 쓰면 커서로 좁힌 결과를 또 건너뛰어 행이 사라진다.) */\n if (keyset) {\n selectQueryBuilder.take(effective)\n return\n }\n\n if (page && page > 0) {\n selectQueryBuilder.skip(effective * (page - 1)).take(effective)\n } else {\n selectQueryBuilder.take(effective)\n }\n}\n\n/**\n * Adds a filtering condition to the SelectQueryBuilder based on the provided filter and mapping options.\n *\n * @param metadata - The EntityMetadata of the TypeORM entity.\n * @param selectQueryBuilder - The SelectQueryBuilder to which the condition will be added.\n * @param whereExpressionBuilder - The WhereExpressionBuilder to construct the where clause.\n * @param filter - The Filter object containing the filter criteria.\n * @param filtersMap - A mapping of filter names to column names and relation column names.\n * @param andCondition - A flag indicating whether to use \"AND\" or \"OR\" for combining conditions.\n */\nfunction addCondition<T>(\n metadata: EntityMetadata,\n selectQueryBuilder: SelectQueryBuilder<T>,\n whereExpressionBuilder: WhereExpressionBuilder,\n filter: Filter,\n filtersMap: { [name: string]: { columnName: string; relationColumn?: string } } = {},\n andCondition: boolean,\n searchMode: SearchMode = 'contains',\n columnExpressions: Record<string, string> = {}\n): void {\n const { name, operator, value } = filter\n if (Object.prototype.hasOwnProperty.call(columnExpressions, name)) {\n const { clause, parameters } = getClauseAndParameters(columnExpressions[name], nextParamKey(name), operator, value, searchMode)\n if (andCondition) whereExpressionBuilder.andWhere(clause, parameters)\n else whereExpressionBuilder.orWhere(clause, parameters)\n return\n }\n var entityAlias = selectQueryBuilder.alias\n\n var { relationColumn, columnName } = filtersMap[name] || {}\n /*\n 1. relationColumn과 columnName이 지정된 경우 \n - relation inverse 테이블에서, columnName을 찾는다.\n 2. relationColumn만 지정된 경우는 없어야 한다.\n - 이 경우 columnName 은 'name' 이라고 판단한다.\n 3. columnName이 지정된 경우.\n - 이 경우는 columnName 만 적용한다.\n */\n if (relationColumn) {\n const columns = relationColumn.split('.')\n var entityMetadata: EntityMetadata\n var relation: RelationMetadata\n\n for (const rcolumn of columns) {\n if (relation) {\n const { propertyName } = relationColumnMeta\n const property = `${entityAlias}.${propertyName}`\n\n entityAlias = `${entityAlias}-${entityMetadata.tableName}-for-${columnName || 'name'}` as string\n\n if (andCondition) {\n selectQueryBuilder.innerJoin(property, entityAlias)\n } else {\n selectQueryBuilder.leftJoin(property, entityAlias)\n }\n } else {\n entityMetadata = metadata\n }\n\n var relationColumnMeta = entityMetadata.columns.find(column => column.propertyName === rcolumn)\n if (!relationColumnMeta) {\n console.warn(`relationColumn \"${relationColumn}\" in filtersMap for \"${name}\" is not a relation column`)\n return\n }\n\n relation = relationColumnMeta.relationMetadata\n entityMetadata = relation.inverseEntityMetadata\n }\n\n var columnMeta = entityMetadata.columns.find(column => column.propertyName === (columnName || 'name'))\n if (!columnMeta) {\n console.warn(`columnName \"${columnName}\" in filtersMap for \"${name}\" is not a column`)\n return\n }\n } else {\n var columnMeta = metadata.columns.find(column => column.propertyName === (columnName || name))\n if (!columnMeta) {\n /* relationId 에 대한 필터링은 해당 컬럼값 자체의 비교로 한다. */\n var relationIdMeta = metadata.relationIds.find(relationId => relationId.propertyName === (columnName || name))\n if (relationIdMeta) {\n columnMeta = relationIdMeta.relation.joinColumns[0]\n } else {\n /* 미지의 컬럼: 이전엔 warn 후 흘러가 columnMeta.databaseName 에서 불투명 TypeError 로 크래시했다.\n 원인을 바로 알 수 있도록 명확·실행가능한 오류를 던진다(fail-fast). */\n const target = columnName || name\n const available = metadata.columns.map(column => column.propertyName).join(', ')\n throw new Error(\n `[getQueryBuilderFromListParams] unknown filter column \"${target}\" on entity \"${metadata.name}\". ` +\n `Add a filtersMap entry for \"${name}\", or use one of: ${available}`\n )\n }\n } else {\n var relation = columnMeta.relationMetadata\n }\n\n if (relation) {\n /* filterMap에 의해서 relationColumn 이 지정되지 않았더라도, name 또는 columnName의 column이 relation인 경우에는\n - 조건절 구성을 위한 타겟필드명은 'name' 으로만 한정된다.\n */\n var relationColumnMeta = columnMeta\n var entityMetadata = relation.inverseEntityMetadata\n columnMeta = entityMetadata.columns.find(column => column.propertyName === 'name')\n if (!columnMeta) {\n console.warn(`relation column \"${columnName || name}\" does not have \"name\" column`)\n return\n }\n }\n }\n\n const dbNameForColumn = columnMeta.databaseName\n const alias = relationColumnMeta ? `${name}-filter` : entityAlias\n\n /* relation columne인 경우 name을 alias로 사용한다. */\n const field = `${alias}.${dbNameForColumn}`\n\n /*\n * 파라미터 이름은 **조건마다 유일**해야 한다.\n *\n * 예전에는 필터 `name` 을 그대로 플레이스홀더로 썼다. 그래서 **같은 컬럼에 조건을 둘 이상** 걸면\n * 둘 다 `:같은이름` 이 되어 뒤엣것이 앞엣것의 값을 덮어썼다. SQL 에는 조건이 멀쩡히 둘 다 있는데\n * 값은 하나만 남으므로, 오류도 경고도 없이 **결과만 틀린다.**\n *\n * (구간 조회는 전용 `between` 연산자가 있고 그쪽은 `:name_1`·`:name_2` 로 나눠 써서 무사하다.\n * 문제가 되는 건 `gte`+`lte` 를 따로 거는 조합처럼 같은 이름이 두 번 나오는 모든 경우다.)\n */\n var { clause, parameters } = getClauseAndParameters(field, nextParamKey(name), operator, value, searchMode)\n\n if (relationColumnMeta) {\n const { propertyName } = relationColumnMeta\n const property = `${entityAlias}.${propertyName}`\n if (andCondition) {\n selectQueryBuilder.innerJoin(property, alias, clause, parameters)\n } else {\n selectQueryBuilder.leftJoin(property, alias)\n whereExpressionBuilder.orWhere(clause, parameters)\n }\n } else {\n andCondition\n ? whereExpressionBuilder.andWhere(clause, parameters)\n : whereExpressionBuilder.orWhere(clause, parameters)\n }\n}\n\n/**\n * Adds sorting to the SelectQueryBuilder based on the provided Sorting objects.\n *\n * @param selectQueryBuilder - The SelectQueryBuilder to which sorting should be applied.\n * @param sortings - An array of Sorting objects defining the sort order.\n * @param entityAlias - The alias of the entity in the query.\n * @param filtersMap - A mapping of filter names to column names and relation column names.\n * @param metadata - The EntityMetadata of the TypeORM entity.\n */\nfunction addSorting<T>(\n selectQueryBuilder: SelectQueryBuilder<T>,\n sortings: Sorting[],\n entityAlias: string,\n filtersMap: { [name: string]: { columnName: string; relationColumn?: string } },\n metadata: EntityMetadata,\n columnExpressions: Record<string, string> = {}\n) {\n sortings.forEach((sorting, index) => {\n const sortField = Object.prototype.hasOwnProperty.call(columnExpressions, sorting.name)\n ? columnExpressions[sorting.name]\n : determineSortField(sorting.name, entityAlias, filtersMap, selectQueryBuilder, metadata)\n if (index === 0) {\n selectQueryBuilder.orderBy(sortField, sorting.desc ? 'DESC' : 'ASC')\n } else {\n selectQueryBuilder.addOrderBy(sortField, sorting.desc ? 'DESC' : 'ASC')\n }\n })\n}\n\n/**\n * Determines the sorting field for a given sorting name, considering possible relation columns.\n *\n * @param sortingName - The name of the field to sort by.\n * @param entityAlias - The alias of the entity in the query.\n * @param filtersMap - A mapping of filter names to column names and relation column names.\n * @param selectQueryBuilder - The SelectQueryBuilder instance to apply sorting to.\n * @param metadata - The EntityMetadata of the TypeORM entity.\n * @returns {string} - The fully qualified sorting field.\n */\nfunction determineSortField<T>(\n sortingName: string,\n entityAlias: string,\n filtersMap: { [name: string]: { columnName: string; relationColumn?: string } },\n selectQueryBuilder: SelectQueryBuilder<T>,\n metadata: EntityMetadata\n): string {\n const filter = filtersMap[sortingName]\n\n if (!filter) {\n return sortingName.split('.').length > 1 ? sortingName : `${entityAlias}.${sortingName}`\n }\n\n const { columnName, relationColumn } = filter\n\n if (relationColumn) {\n const relationAlias = applyJoins(\n selectQueryBuilder,\n entityAlias,\n relationColumn,\n metadata,\n 'leftJoin',\n columnName || sortingName,\n true\n )\n return `${relationAlias}.${columnName}`\n } else {\n return `${entityAlias}.${columnName}`\n }\n}\n\n/**\n * Applies the necessary joins to the SelectQueryBuilder based on the relation column.\n *\n * @param selectQueryBuilder - The SelectQueryBuilder where the joins will be applied.\n * @param entityAlias - The current alias of the entity in the query.\n * @param relationColumn - The dot-notated string representing the relation chain (e.g., \"user.profile.address\").\n * @param metadata - The EntityMetadata of the entity.\n * @param joinType - The type of join to use (\"innerJoin\" or \"leftJoin\").\n * @param columnName - The name of the column used for filtering or sorting, default to 'name'.\n * @param selectField - Whether to include the field in the SELECT clause.\n * @returns {string} - The alias to be used for the final field in the relation chain.\n */\nfunction applyJoins<T>(\n selectQueryBuilder: SelectQueryBuilder<T>,\n entityAlias: string,\n relationColumn: string,\n metadata: EntityMetadata,\n joinType: 'innerJoin' | 'leftJoin' = 'leftJoin',\n columnName: string = 'name',\n selectField: boolean = false\n): string {\n const columns = relationColumn.split('.')\n let currentAlias = entityAlias\n let currentMetadata = metadata\n\n for (const column of columns) {\n const relation = currentMetadata.relations.find(rel => rel.propertyName === column)\n\n if (!relation) {\n const available = currentMetadata.relations.map(rel => rel.propertyName).join(', ')\n throw new Error(\n `[getQueryBuilderFromListParams] relation \"${column}\" not found on entity \"${currentMetadata.name}\"` +\n ` (relationColumn \"${relationColumn}\"). Available relations: ${available}`\n )\n }\n\n const nextAlias = `${currentAlias}_${relation.inverseEntityMetadata.tableName}_for_${columnName}`\n\n if (!selectQueryBuilder.expressionMap.aliases.some(alias => alias.name === nextAlias)) {\n selectQueryBuilder[joinType](`${currentAlias}.${column}`, nextAlias)\n }\n if (selectField && columns.at(-1) == column /* 최종 alias만 추가 */) {\n /*\n * The select alias has to be spelled the way TypeORM will look it up.\n *\n * Sorting by a joined column and paginating puts the query on TypeORM's distinct-id path,\n * which wraps everything in a subquery and re-addresses each ORDER BY as\n * `distinctAlias.<buildAlias(aliasName, column.databaseName)>` — the **database** name (see\n * SelectQueryBuilder.createOrderByCombinedWithSelectExpression). This addSelect used the\n * **property** name, so the two only met where the two spellings happen to be identical.\n *\n * name, description and priority are one lowercase word each, so they were identical and it\n * worked by luck. dueAt is not: the property is `dueAt`, the column is `due_at` under the\n * snake_case NamingStrategy, and sorting 내 결재함 by 기한 died with\n *\n * SQLITE_ERROR: no such column:\n * distinctAlias.ap_activity_threads_for_dueAt_activity_instances_for_dueAt_due_at\n *\n * Building the alias the same way TypeORM does makes them agree by construction. Lowercase\n * columns produce the identical string as before, so nothing else moves.\n */\n const targetColumn = relation.inverseEntityMetadata.findColumnWithPropertyName(columnName)\n\n if (!targetColumn) {\n /*\n * columnName is a property name, not a database column name — `dueAt`, never `due_at`.\n * Failing loudly here beats an alias nobody can resolve: TypeORM's own lookup on the\n * distinct path dereferences the column without a null check, so a wrong spelling would\n * surface much later as a TypeError with no mention of this map.\n */\n const available = relation.inverseEntityMetadata.columns.map(col => col.propertyName).join(', ')\n throw new Error(\n `[getQueryBuilderFromListParams] column \"${columnName}\" not found on entity` +\n ` \"${relation.inverseEntityMetadata.name}\" (relationColumn \"${relationColumn}\").` +\n ` Use the property name, not the database column name. Available: ${available}`\n )\n }\n\n selectQueryBuilder.addSelect(\n `${nextAlias}.${columnName}`,\n DriverUtils.buildAlias(selectQueryBuilder.connection.driver, undefined, nextAlias, targetColumn.databaseName)\n )\n }\n\n currentAlias = nextAlias\n currentMetadata = relation.inverseEntityMetadata\n }\n\n return currentAlias\n}\n\n/** search 값에 searchMode 에 맞는 와일드카드를 씌운다. */\nfunction wrapSearch(value: string, mode: SearchMode): string {\n switch (mode) {\n case 'startsWith':\n return `${value}%`\n case 'exact':\n return value\n case 'contains':\n default:\n return `%${value}%`\n }\n}\n\n/**\n * Generates the SQL clause and parameters based on the provided filter.\n *\n * @param field - The database field to filter on.\n * @param name - The name of the filter.\n * @param operator - The operator to use in the filter.\n * @param value - The value to filter with.\n * @returns An object containing the SQL clause and the parameters.\n */\nfunction getClauseAndParameters(\n field: string,\n name: string,\n operator: string,\n value: any,\n searchMode: SearchMode = 'contains'\n): { clause: string; parameters: { [key: string]: any } } {\n const values = value instanceof Array ? value : [value]\n let clause = ''\n let parameters: { [key: string]: any } = {}\n\n switch (operator) {\n case 'eq':\n clause = `${field} = :${name}`\n parameters = { [name]: value }\n break\n case 'like':\n clause = `${field} LIKE :${name}`\n parameters = { [name]: `%${value}%` }\n break\n case 'search':\n /* searchMode 로 매칭 방식 선택(기본 contains). startsWith 는 접두 인덱스 활용 가능. */\n clause = `LOWER(${field}) LIKE :${name}`\n parameters = { [name]: wrapSearch(String(value).toLowerCase(), searchMode) }\n break\n case 'i_like':\n clause = `LOWER(${field}) LIKE :${name}`\n parameters = { [name]: `%${String(value).toLowerCase()}%` }\n break\n case 'nlike':\n clause = `${field} NOT LIKE :${name}`\n parameters = { [name]: `%${value}%` }\n break\n case 'i_nlike':\n clause = `LOWER(${field}) NOT LIKE :${name}`\n parameters = { [name]: `%${String(value).toLowerCase()}%` }\n break\n case 'lt':\n clause = `${field} < :${name}`\n parameters = { [name]: value }\n break\n case 'gt':\n clause = `${field} > :${name}`\n parameters = { [name]: value }\n break\n case 'lte':\n clause = `${field} <= :${name}`\n parameters = { [name]: value }\n break\n case 'gte':\n clause = `${field} >= :${name}`\n parameters = { [name]: value }\n break\n case 'noteq':\n clause = `${field} != :${name}`\n parameters = { [name]: value }\n break\n /* What an empty list means, and the parentheses around OR, are decided in membershipClause alone. */\n case 'in':\n case 'notin':\n case 'notin_with_null':\n ;({ clause, parameters } = membershipClause(field, operator, values, name))\n break\n case 'is_null':\n clause = `${field} IS NULL`\n break\n case 'is_not_null':\n clause = `${field} IS NOT NULL`\n break\n /* boolean 비교는 파라미터 바인딩으로 (IS TRUE/FALSE 는 MSSQL/Oracle 미지원 → 이식성 확보) */\n case 'is_false':\n clause = `${field} = :${name}`\n parameters = { [name]: false }\n break\n case 'is_true':\n clause = `${field} = :${name}`\n parameters = { [name]: true }\n break\n case 'is_not_false':\n /* NOT FALSE = true 또는 NULL */\n clause = `(${field} IS NULL OR ${field} = :${name})`\n parameters = { [name]: true }\n break\n case 'is_not_true':\n /* NOT TRUE = false 또는 NULL */\n clause = `(${field} IS NULL OR ${field} = :${name})`\n parameters = { [name]: false }\n break\n case 'is_present':\n clause = `${field} IS NOT NULL`\n break\n case 'is_blank':\n clause = `${field} IS NULL`\n break\n case 'is_empty_num_id':\n clause = `(${field} IS NULL OR ${field} = :${name})`\n parameters = { [name]: 0 }\n break\n case 'between':\n clause = `${field} BETWEEN :${name}_1 AND :${name}_2`\n parameters = { [`${name}_1`]: values[0], [`${name}_2`]: values[1] }\n break\n default:\n /* 지원하지 않는 연산자: 이전엔 clause='' 로 andWhere('') 가 되어 malformed/무음이었다.\n 명확한 오류로 원인을 알린다(fail-fast). */\n throw new Error(\n `[getQueryBuilderFromListParams] unsupported filter operator \"${operator}\" for field \"${field}\".`\n )\n }\n\n return { clause, parameters }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"get-query-builder-from-list-params.js","sourceRoot":"","sources":["../../server/utils/get-query-builder-from-list-params.ts"],"names":[],"mappings":";;AA2DA,sEA0MC;AAMD,0CAYC;AA8eD,4CAuBC;AA5xBD,qCAA0G;AAC1G,kEAA2D;AAE3D,yEAAkH;AAElH,qEAAuE;AACvE,yDAAsE;AACtE,sDAA+C;AAC/C,iEAAyD;AAUzD;;;GAGG;AACH,IAAI,QAAQ,GAAG,CAAC,CAAA;AAChB,MAAM,YAAY,GAAG,CAAC,IAAY,EAAU,EAAE,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC,gBAAgB,EAAE,GAAG,CAAC,KAAK,QAAQ,EAAE,EAAE,CAAA;AAEtG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,SAAgB,6BAA6B,CAAO,OA+CnD;IACC,MAAM,EACJ,UAAU,EACV,MAAM,EACN,MAAM,EACN,KAAK,EACL,WAAW,EACX,UAAU,GAAG,EAAE,EACf,iBAAiB,GAAG,EAAE,EACtB,UAAU,GAAG,UAAU,EACvB,YAAY,EACZ,QAAQ,EACR,SAAS,EACT,WAAW,EACX,cAAc,EACd,eAAe,EAChB,GAAG,OAAO,CAAA;IACX,MAAM,EAAE,SAAS,GAAG,kCAAkB,CAAC,IAAI,EAAE,GAAG,MAAM,IAAI,EAAE,CAAA;IAE5D,MAAM,kBAAkB,GAAG,UAAU,CAAC,kBAAkB,CAAC,KAAK,CAAC,CAAA;IAC/D,MAAM,WAAW,GAAG,kBAAkB,CAAC,KAAK,CAAA;IAE5C,IAAI,WAAW,EAAE,CAAC;QAChB,KAAK,MAAM,MAAM,IAAI,MAAM,EAAE,OAAO,IAAI,EAAE,EAAE,CAAC;YAC3C,IAAI,CAAC,WAAW,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC;gBACvC,MAAM,IAAI,KAAK,CACb,iDAAiD,MAAM,CAAC,IAAI,+BAA+B,UAAU,CAAC,QAAQ,CAAC,IAAI,KAAK;oBACtH,YAAY,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CACvC,CAAA;YACH,CAAC;QACH,CAAC;IACH,CAAC;IAED,6BAA6B;IAC7B,MAAM,aAAa,GACjB,MAAM,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,CAAC,EAAE;QAC9B,IAAI,MAAM,CAAC,QAAQ,KAAK,QAAQ,EAAE,CAAC;YACjC,OAAO,KAAK,CAAA;QACd,CAAC;QACD,IAAI,MAAM,CAAC,QAAQ,CAAC,WAAW,EAAE,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,WAAW,IAAI,CAAC,WAAW,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC;YAC3G,OAAO,CAAC,IAAI,CAAC,kFAAkF,EAAE,MAAM,CAAC,IAAI,CAAC,CAAA;YAC7G,OAAO,KAAK,CAAA;QACd,CAAC;QACD,OAAO,IAAI,CAAA;IACb,CAAC,CAAC,IAAI,EAAE,CAAA;IAEV;;;;OAIG;IACH,MAAM,IAAI,GAAG,MAAM,CAAC,MAAM,EAAE,IAAI,EAAE,CAAA;IAClC;;;;;;OAMG;IACH,IAAI,IAAI,IAAI,CAAC,CAAC,WAAW,YAAY,KAAK,IAAI,WAAW,CAAC,MAAM,GAAG,CAAC,CAAC,EAAE,CAAC;QACtE,MAAM,MAAM,GAAG,UAAU,CAAC,QAAQ,CAAC,IAAI,CAAA;QACvC,MAAM,IAAI,oBAAO,CAAC,qBAAqB,EAAE,EAAE,MAAM,EAAE,EAAE,OAAO,MAAM,4DAA4D,CAAC,CAAA;IACjI,CAAC;IACD,MAAM,WAAW,GAAa,IAAI,CAAC,CAAC,CAAC,WAAY,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,QAAQ,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAA;IAE/G,MAAM,aAAa,GACjB,WAAW,YAAY,KAAK;QAC1B,CAAC,CAAC,MAAM,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,CAAC,EAAE;YAC9B,IAAI,MAAM,CAAC,QAAQ,KAAK,QAAQ,EAAE,CAAC;gBACjC,OAAO,KAAK,CAAA;YACd,CAAC;YACD,IAAI,CAAC,WAAW,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC;gBACvC,OAAO,CAAC,IAAI,CACV,kFAAkF,EAClF,MAAM,CAAC,IAAI,CACZ,CAAA;gBACD,OAAO,KAAK,CAAA;YACd,CAAC;YACD,OAAO,IAAI,CAAA;QACb,CAAC,CAAC,IAAI,EAAE;QACV,CAAC,CAAC,EAAE,CAAA;IAER,MAAM,UAAU,GAAG,MAAM,CAAC,UAAU,CAAA;IACpC,MAAM,QAAQ,GAAG,MAAM,CAAC,QAAQ,CAAA;IAChC,MAAM,QAAQ,GAAG,UAAU,CAAC,QAAQ,CAAA;IAEpC,uBAAuB;IACvB,IAAI,aAAa,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC7B,aAAa,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE;YAC7B,YAAY,CAAC,QAAQ,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,EAAE,UAAU,EAAE,IAAI,EAAE,UAAU,EAAE,iBAAiB,CAAC,CAAA;QACzH,CAAC,CAAC,CAAA;IACJ,CAAC;IAED,sFAAsF;IACtF,MAAM,gBAAgB,GAAG,CAAC,GAAG,aAAa,EAAE,GAAG,WAAW,CAAC,CAAA;IAC3D,IAAI,gBAAgB,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAChC,kBAAkB,CAAC,QAAQ,CACzB,IAAI,kBAAQ,CAAC,EAAE,CAAC,EAAE;YAChB,gBAAgB,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE;gBAChC,YAAY,CAAC,QAAQ,EAAE,kBAAkB,EAAE,EAAE,EAAE,MAAM,EAAE,UAAU,EAAE,KAAK,EAAE,UAAU,EAAE,iBAAiB,CAAC,CAAA;YAC1G,CAAC,CAAC,CAAA;QACJ,CAAC,CAAC,CACH,CAAA;IACH,CAAC;IAED,uBAAuB;IACvB,IAAI,MAAM,EAAE,CAAC;QACX,IAAA,qDAA4B,EAAC,kBAAkB,EAAE,WAAW,EAAE,MAAM,EAAE,SAAS,CAAC,CAAA;IAClF,CAAC;IAED;qDACiD;IACjD,IAAI,QAAQ,EAAE,MAAM,IAAI,SAAS,EAAE,CAAC;QAClC,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;YAC/B,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;gBACtC,MAAM,IAAI,KAAK,CACb,+CAA+C,OAAO,CAAC,IAAI,+BAA+B,QAAQ,CAAC,IAAI,KAAK;oBAC1G,YAAY,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CACrC,CAAA;YACH,CAAC;QACH,CAAC;IACH,CAAC;IAED;;;OAGG;IACH,MAAM,MAAM,GAAG,UAAU,EAAE,KAAK,CAAA;IAChC,MAAM,QAAQ,GAAG,cAAc,IAAI,QAAQ,CAAC,cAAc,CAAC,CAAC,CAAC,EAAE,YAAY,CAAA;IAC3E,MAAM,iBAAiB,GAAG,MAAM,IAAI,QAAQ,CAAC,CAAC,CAAC,IAAA,iCAAc,EAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC,eAAe,CAAC,QAAQ,EAAE,eAAe,EAAE,QAAQ,EAAE,QAAQ,CAAC,CAAA;IAElJ,mBAAmB;IACnB,aAAa,CAAC,kBAAkB,EAAE,UAAU,EAAE;QAC5C,YAAY;QACZ,QAAQ;QACR,UAAU,EAAE,QAAQ,CAAC,IAAI;QACzB,MAAM,EAAE,CAAC,CAAC,MAAM;KACjB,CAAC,CAAA;IAEF,IAAI,MAAM,EAAE,CAAC;QACX,IAAI,CAAC,QAAQ,EAAE,CAAC;YACd,MAAM,IAAI,KAAK,CACb,4FAA4F,QAAQ,CAAC,IAAI,IAAI;gBAC3G,4DAA4D,CAC/D,CAAA;QACH,CAAC;QACD,IAAA,oCAAiB,EAAC,kBAAkB,EAAE,EAAE,KAAK,EAAE,WAAW,EAAE,QAAQ,EAAE,iBAAkB,EAAE,MAAM,EAAE,iBAAiB,EAAE,CAAC,CAAA;IACxH,CAAC;IAED,gBAAgB;IAChB,IAAI,iBAAiB,IAAI,iBAAiB,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACtD,UAAU,CAAC,kBAAkB,EAAE,iBAAiB,EAAE,WAAW,EAAE,UAAU,EAAE,QAAQ,EAAE,iBAAiB,CAAC,CAAA;IACzG,CAAC;IAED,OAAO,kBAAkB,CAAA;AAC3B,CAAC;AAED;;;GAGG;AACH,SAAgB,eAAe,CAC7B,QAA+B,EAC/B,eAAsC,EACtC,QAAwB,EACxB,QAA4B;IAE5B,MAAM,GAAG,GAAG,eAAe,IAAI,CAAC,QAAQ,CAAC,0BAA0B,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAA;IAC5H,MAAM,OAAO,GAAc,EAAE,CAAA;IAC7B,KAAK,MAAM,OAAO,IAAI,CAAC,GAAG,CAAC,QAAQ,IAAI,EAAE,CAAC,EAAE,GAAG,GAAG,EAAE,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC;QAC3G,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,IAAI,KAAK,OAAO,CAAC,IAAI,CAAC;YAAE,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,CAAA;IAC5E,CAAC;IACD,OAAO,OAAO,CAAA;AAChB,CAAC;AAED;;;;;GAKG;AACH,SAAS,aAAa,CACpB,kBAAyC,EACzC,UAAuB,EACvB,QAA6F,EAAE;IAE/F,MAAM,EAAE,YAAY,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,EAAE,GAAG,KAAK,CAAA;IAC5D,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,UAAU,IAAI,EAAE,CAAA;IAExC,uDAAuD;IACvD,MAAM,KAAK,GAAG,KAAK,IAAI,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAA;IACpD,MAAM,SAAS,GAAG,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC,KAAK,CAAA;IAEnG,IAAI,SAAS,KAAK,SAAS,EAAE,CAAC;QAC5B;8DACsD;QACtD,OAAO,CAAC,IAAI,CACV,4DAA4D,UAAU,IAAI,QAAQ,MAAM;YACtF,mHAAmH;YACnH,uEAAuE,CAC1E,CAAA;QACD,OAAM;IACR,CAAC;IAED;+CAC2C;IAC3C,IAAI,MAAM,EAAE,CAAC;QACX,kBAAkB,CAAC,IAAI,CAAC,SAAS,CAAC,CAAA;QAClC,OAAM;IACR,CAAC;IAED,IAAI,IAAI,IAAI,IAAI,GAAG,CAAC,EAAE,CAAC;QACrB,kBAAkB,CAAC,IAAI,CAAC,SAAS,GAAG,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAA;IACjE,CAAC;SAAM,CAAC;QACN,kBAAkB,CAAC,IAAI,CAAC,SAAS,CAAC,CAAA;IACpC,CAAC;AACH,CAAC;AAED;;;;;;;;;GASG;AACH,SAAS,YAAY,CACnB,QAAwB,EACxB,kBAAyC,EACzC,sBAA8C,EAC9C,MAAc,EACd,aAAkF,EAAE,EACpF,YAAqB,EACrB,aAAyB,UAAU,EACnC,oBAA4C,EAAE;IAE9C,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,GAAG,MAAM,CAAA;IACxC,IAAI,MAAM,CAAC,SAAS,CAAC,cAAc,CAAC,IAAI,CAAC,iBAAiB,EAAE,IAAI,CAAC,EAAE,CAAC;QAClE,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,GAAG,sBAAsB,CAAC,iBAAiB,CAAC,IAAI,CAAC,EAAE,YAAY,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,KAAK,EAAE,UAAU,CAAC,CAAA;QAC/H,IAAI,YAAY;YAAE,sBAAsB,CAAC,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC,CAAA;;YAChE,sBAAsB,CAAC,OAAO,CAAC,MAAM,EAAE,UAAU,CAAC,CAAA;QACvD,OAAM;IACR,CAAC;IACD,IAAI,WAAW,GAAG,kBAAkB,CAAC,KAAK,CAAA;IAE1C,IAAI,EAAE,cAAc,EAAE,UAAU,EAAE,GAAG,UAAU,CAAC,IAAI,CAAC,IAAI,EAAE,CAAA;IAC3D;;;;;;;MAOE;IACF,IAAI,cAAc,EAAE,CAAC;QACnB,MAAM,OAAO,GAAG,cAAc,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;QACzC,IAAI,cAA8B,CAAA;QAClC,IAAI,QAA0B,CAAA;QAE9B,KAAK,MAAM,OAAO,IAAI,OAAO,EAAE,CAAC;YAC9B,IAAI,QAAQ,EAAE,CAAC;gBACb,MAAM,EAAE,YAAY,EAAE,GAAG,kBAAkB,CAAA;gBAC3C,MAAM,QAAQ,GAAG,GAAG,WAAW,IAAI,YAAY,EAAE,CAAA;gBAEjD,WAAW,GAAG,GAAG,WAAW,IAAI,cAAc,CAAC,SAAS,QAAQ,UAAU,IAAI,MAAM,EAAY,CAAA;gBAEhG,IAAI,YAAY,EAAE,CAAC;oBACjB,kBAAkB,CAAC,SAAS,CAAC,QAAQ,EAAE,WAAW,CAAC,CAAA;gBACrD,CAAC;qBAAM,CAAC;oBACN,kBAAkB,CAAC,QAAQ,CAAC,QAAQ,EAAE,WAAW,CAAC,CAAA;gBACpD,CAAC;YACH,CAAC;iBAAM,CAAC;gBACN,cAAc,GAAG,QAAQ,CAAA;YAC3B,CAAC;YAED,IAAI,kBAAkB,GAAG,cAAc,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,CAAC,YAAY,KAAK,OAAO,CAAC,CAAA;YAC/F,IAAI,CAAC,kBAAkB,EAAE,CAAC;gBACxB,OAAO,CAAC,IAAI,CAAC,mBAAmB,cAAc,wBAAwB,IAAI,4BAA4B,CAAC,CAAA;gBACvG,OAAM;YACR,CAAC;YAED,QAAQ,GAAG,kBAAkB,CAAC,gBAAgB,CAAA;YAC9C,cAAc,GAAG,QAAQ,CAAC,qBAAqB,CAAA;QACjD,CAAC;QAED,IAAI,UAAU,GAAG,cAAc,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,CAAC,YAAY,KAAK,CAAC,UAAU,IAAI,MAAM,CAAC,CAAC,CAAA;QACtG,IAAI,CAAC,UAAU,EAAE,CAAC;YAChB,OAAO,CAAC,IAAI,CAAC,eAAe,UAAU,wBAAwB,IAAI,mBAAmB,CAAC,CAAA;YACtF,OAAM;QACR,CAAC;IACH,CAAC;SAAM,CAAC;QACN,IAAI,UAAU,GAAG,QAAQ,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,CAAC,YAAY,KAAK,CAAC,UAAU,IAAI,IAAI,CAAC,CAAC,CAAA;QAC9F,IAAI,CAAC,UAAU,EAAE,CAAC;YAChB,6CAA6C;YAC7C,IAAI,cAAc,GAAG,QAAQ,CAAC,WAAW,CAAC,IAAI,CAAC,UAAU,CAAC,EAAE,CAAC,UAAU,CAAC,YAAY,KAAK,CAAC,UAAU,IAAI,IAAI,CAAC,CAAC,CAAA;YAC9G,IAAI,cAAc,EAAE,CAAC;gBACnB,UAAU,GAAG,cAAc,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC,CAAC,CAAA;YACrD,CAAC;iBAAM,CAAC;gBACN;iEACiD;gBACjD,MAAM,MAAM,GAAG,UAAU,IAAI,IAAI,CAAA;gBACjC,MAAM,SAAS,GAAG,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;gBAChF,MAAM,IAAI,KAAK,CACb,0DAA0D,MAAM,gBAAgB,QAAQ,CAAC,IAAI,KAAK;oBAChG,+BAA+B,IAAI,qBAAqB,SAAS,EAAE,CACtE,CAAA;YACH,CAAC;QACH,CAAC;aAAM,CAAC;YACN,IAAI,QAAQ,GAAG,UAAU,CAAC,gBAAgB,CAAA;QAC5C,CAAC;QAED,IAAI,QAAQ,EAAE,CAAC;YACb;;cAEE;YACF,IAAI,kBAAkB,GAAG,UAAU,CAAA;YACnC,IAAI,cAAc,GAAG,QAAQ,CAAC,qBAAqB,CAAA;YACnD,UAAU,GAAG,cAAc,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,CAAC,YAAY,KAAK,MAAM,CAAC,CAAA;YAClF,IAAI,CAAC,UAAU,EAAE,CAAC;gBAChB,OAAO,CAAC,IAAI,CAAC,oBAAoB,UAAU,IAAI,IAAI,+BAA+B,CAAC,CAAA;gBACnF,OAAM;YACR,CAAC;QACH,CAAC;IACH,CAAC;IAED,MAAM,eAAe,GAAG,UAAU,CAAC,YAAY,CAAA;IAC/C,MAAM,KAAK,GAAG,kBAAkB,CAAC,CAAC,CAAC,GAAG,IAAI,SAAS,CAAC,CAAC,CAAC,WAAW,CAAA;IAEjE,6CAA6C;IAC7C,MAAM,KAAK,GAAG,GAAG,KAAK,IAAI,eAAe,EAAE,CAAA;IAE3C;;;;;;;;;OASG;IACH,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,GAAG,sBAAsB,CAAC,KAAK,EAAE,YAAY,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,KAAK,EAAE,UAAU,CAAC,CAAA;IAE3G,IAAI,kBAAkB,EAAE,CAAC;QACvB,MAAM,EAAE,YAAY,EAAE,GAAG,kBAAkB,CAAA;QAC3C,MAAM,QAAQ,GAAG,GAAG,WAAW,IAAI,YAAY,EAAE,CAAA;QACjD,IAAI,YAAY,EAAE,CAAC;YACjB,kBAAkB,CAAC,SAAS,CAAC,QAAQ,EAAE,KAAK,EAAE,MAAM,EAAE,UAAU,CAAC,CAAA;QACnE,CAAC;aAAM,CAAC;YACN,kBAAkB,CAAC,QAAQ,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAA;YAC5C,sBAAsB,CAAC,OAAO,CAAC,MAAM,EAAE,UAAU,CAAC,CAAA;QACpD,CAAC;IACH,CAAC;SAAM,CAAC;QACN,YAAY;YACV,CAAC,CAAC,sBAAsB,CAAC,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC;YACrD,CAAC,CAAC,sBAAsB,CAAC,OAAO,CAAC,MAAM,EAAE,UAAU,CAAC,CAAA;IACxD,CAAC;AACH,CAAC;AAED;;;;;;;;GAQG;AACH,SAAS,UAAU,CACjB,kBAAyC,EACzC,QAAmB,EACnB,WAAmB,EACnB,UAA+E,EAC/E,QAAwB,EACxB,oBAA4C,EAAE;IAE9C,QAAQ,CAAC,OAAO,CAAC,CAAC,OAAO,EAAE,KAAK,EAAE,EAAE;QAClC,MAAM,SAAS,GAAG,MAAM,CAAC,SAAS,CAAC,cAAc,CAAC,IAAI,CAAC,iBAAiB,EAAE,OAAO,CAAC,IAAI,CAAC;YACrF,CAAC,CAAC,iBAAiB,CAAC,OAAO,CAAC,IAAI,CAAC;YACjC,CAAC,CAAC,kBAAkB,CAAC,OAAO,CAAC,IAAI,EAAE,WAAW,EAAE,UAAU,EAAE,kBAAkB,EAAE,QAAQ,CAAC,CAAA;QAC3F,IAAI,KAAK,KAAK,CAAC,EAAE,CAAC;YAChB,kBAAkB,CAAC,OAAO,CAAC,SAAS,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,CAAA;QACtE,CAAC;aAAM,CAAC;YACN,kBAAkB,CAAC,UAAU,CAAC,SAAS,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,CAAA;QACzE,CAAC;IACH,CAAC,CAAC,CAAA;AACJ,CAAC;AAED;;;;;;;;;GASG;AACH,SAAS,kBAAkB,CACzB,WAAmB,EACnB,WAAmB,EACnB,UAA+E,EAC/E,kBAAyC,EACzC,QAAwB;IAExB,MAAM,MAAM,GAAG,UAAU,CAAC,WAAW,CAAC,CAAA;IAEtC,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,OAAO,WAAW,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,GAAG,WAAW,IAAI,WAAW,EAAE,CAAA;IAC1F,CAAC;IAED,MAAM,EAAE,UAAU,EAAE,cAAc,EAAE,GAAG,MAAM,CAAA;IAE7C,IAAI,cAAc,EAAE,CAAC;QACnB,MAAM,aAAa,GAAG,UAAU,CAC9B,kBAAkB,EAClB,WAAW,EACX,cAAc,EACd,QAAQ,EACR,UAAU,EACV,UAAU,IAAI,WAAW,EACzB,IAAI,CACL,CAAA;QACD,OAAO,GAAG,aAAa,IAAI,UAAU,EAAE,CAAA;IACzC,CAAC;SAAM,CAAC;QACN,OAAO,GAAG,WAAW,IAAI,UAAU,EAAE,CAAA;IACvC,CAAC;AACH,CAAC;AAED;;;;;;;;;;;GAWG;AACH,SAAS,UAAU,CACjB,kBAAyC,EACzC,WAAmB,EACnB,cAAsB,EACtB,QAAwB,EACxB,WAAqC,UAAU,EAC/C,aAAqB,MAAM,EAC3B,cAAuB,KAAK;IAE5B,MAAM,OAAO,GAAG,cAAc,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;IACzC,IAAI,YAAY,GAAG,WAAW,CAAA;IAC9B,IAAI,eAAe,GAAG,QAAQ,CAAA;IAE9B,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;QAC7B,MAAM,QAAQ,GAAG,eAAe,CAAC,SAAS,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,YAAY,KAAK,MAAM,CAAC,CAAA;QAEnF,IAAI,CAAC,QAAQ,EAAE,CAAC;YACd,MAAM,SAAS,GAAG,eAAe,CAAC,SAAS,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;YACnF,MAAM,IAAI,KAAK,CACb,6CAA6C,MAAM,0BAA0B,eAAe,CAAC,IAAI,GAAG;gBAClG,qBAAqB,cAAc,4BAA4B,SAAS,EAAE,CAC7E,CAAA;QACH,CAAC;QAED,MAAM,SAAS,GAAG,GAAG,YAAY,IAAI,QAAQ,CAAC,qBAAqB,CAAC,SAAS,QAAQ,UAAU,EAAE,CAAA;QAEjG,IAAI,CAAC,kBAAkB,CAAC,aAAa,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,CAAC,IAAI,KAAK,SAAS,CAAC,EAAE,CAAC;YACtF,kBAAkB,CAAC,QAAQ,CAAC,CAAC,GAAG,YAAY,IAAI,MAAM,EAAE,EAAE,SAAS,CAAC,CAAA;QACtE,CAAC;QACD,IAAI,WAAW,IAAI,OAAO,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,MAAM,CAAC,kBAAkB,EAAE,CAAC;YAC/D;;;;;;;;;;;;;;;;;;eAkBG;YACH,MAAM,YAAY,GAAG,QAAQ,CAAC,qBAAqB,CAAC,0BAA0B,CAAC,UAAU,CAAC,CAAA;YAE1F,IAAI,CAAC,YAAY,EAAE,CAAC;gBAClB;;;;;mBAKG;gBACH,MAAM,SAAS,GAAG,QAAQ,CAAC,qBAAqB,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;gBAChG,MAAM,IAAI,KAAK,CACb,2CAA2C,UAAU,uBAAuB;oBAC1E,KAAK,QAAQ,CAAC,qBAAqB,CAAC,IAAI,sBAAsB,cAAc,KAAK;oBACjF,oEAAoE,SAAS,EAAE,CAClF,CAAA;YACH,CAAC;YAED,kBAAkB,CAAC,SAAS,CAC1B,GAAG,SAAS,IAAI,UAAU,EAAE,EAC5B,4BAAW,CAAC,UAAU,CAAC,kBAAkB,CAAC,UAAU,CAAC,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,YAAY,CAAC,YAAY,CAAC,CAC9G,CAAA;QACH,CAAC;QAED,YAAY,GAAG,SAAS,CAAA;QACxB,eAAe,GAAG,QAAQ,CAAC,qBAAqB,CAAA;IAClD,CAAC;IAED,OAAO,YAAY,CAAA;AACrB,CAAC;AAED,4CAA4C;AAC5C,SAAS,UAAU,CAAC,KAAa,EAAE,IAAgB;IACjD,QAAQ,IAAI,EAAE,CAAC;QACb,KAAK,YAAY;YACf,OAAO,GAAG,KAAK,GAAG,CAAA;QACpB,KAAK,OAAO;YACV,OAAO,KAAK,CAAA;QACd,KAAK,UAAU,CAAC;QAChB;YACE,OAAO,IAAI,KAAK,GAAG,CAAA;IACvB,CAAC;AACH,CAAC;AAED;;;;;;;;GAQG;AACH,SAAS,sBAAsB,CAC7B,KAAa,EACb,IAAY,EACZ,QAAgB,EAChB,KAAU,EACV,aAAyB,UAAU;IAEnC,MAAM,MAAM,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAA;IACvD,IAAI,MAAM,GAAG,EAAE,CAAA;IACf,IAAI,UAAU,GAA2B,EAAE,CAAA;IAE3C,QAAQ,QAAQ,EAAE,CAAC;QACjB,KAAK,IAAI;YACP,MAAM,GAAG,GAAG,KAAK,OAAO,IAAI,EAAE,CAAA;YAC9B,UAAU,GAAG,EAAE,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,CAAA;YAC9B,MAAK;QACP,KAAK,MAAM;YACT,MAAM,GAAG,GAAG,KAAK,UAAU,IAAI,EAAE,CAAA;YACjC,UAAU,GAAG,EAAE,CAAC,IAAI,CAAC,EAAE,IAAI,KAAK,GAAG,EAAE,CAAA;YACrC,MAAK;QACP,KAAK,QAAQ;YACX,oEAAoE;YACpE,MAAM,GAAG,SAAS,KAAK,WAAW,IAAI,EAAE,CAAA;YACxC,UAAU,GAAG,EAAE,CAAC,IAAI,CAAC,EAAE,UAAU,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,WAAW,EAAE,EAAE,UAAU,CAAC,EAAE,CAAA;YAC5E,MAAK;QACP,KAAK,QAAQ;YACX,MAAM,GAAG,SAAS,KAAK,WAAW,IAAI,EAAE,CAAA;YACxC,UAAU,GAAG,EAAE,CAAC,IAAI,CAAC,EAAE,IAAI,MAAM,CAAC,KAAK,CAAC,CAAC,WAAW,EAAE,GAAG,EAAE,CAAA;YAC3D,MAAK;QACP,KAAK,OAAO;YACV,MAAM,GAAG,GAAG,KAAK,cAAc,IAAI,EAAE,CAAA;YACrC,UAAU,GAAG,EAAE,CAAC,IAAI,CAAC,EAAE,IAAI,KAAK,GAAG,EAAE,CAAA;YACrC,MAAK;QACP,KAAK,SAAS;YACZ,MAAM,GAAG,SAAS,KAAK,eAAe,IAAI,EAAE,CAAA;YAC5C,UAAU,GAAG,EAAE,CAAC,IAAI,CAAC,EAAE,IAAI,MAAM,CAAC,KAAK,CAAC,CAAC,WAAW,EAAE,GAAG,EAAE,CAAA;YAC3D,MAAK;QACP,KAAK,IAAI;YACP,MAAM,GAAG,GAAG,KAAK,OAAO,IAAI,EAAE,CAAA;YAC9B,UAAU,GAAG,EAAE,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,CAAA;YAC9B,MAAK;QACP,KAAK,IAAI;YACP,MAAM,GAAG,GAAG,KAAK,OAAO,IAAI,EAAE,CAAA;YAC9B,UAAU,GAAG,EAAE,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,CAAA;YAC9B,MAAK;QACP,KAAK,KAAK;YACR,MAAM,GAAG,GAAG,KAAK,QAAQ,IAAI,EAAE,CAAA;YAC/B,UAAU,GAAG,EAAE,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,CAAA;YAC9B,MAAK;QACP,KAAK,KAAK;YACR,MAAM,GAAG,GAAG,KAAK,QAAQ,IAAI,EAAE,CAAA;YAC/B,UAAU,GAAG,EAAE,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,CAAA;YAC9B,MAAK;QACP,KAAK,OAAO;YACV,MAAM,GAAG,GAAG,KAAK,QAAQ,IAAI,EAAE,CAAA;YAC/B,UAAU,GAAG,EAAE,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,CAAA;YAC9B,MAAK;QACP,qGAAqG;QACrG,KAAK,IAAI,CAAC;QACV,KAAK,OAAO,CAAC;QACb,KAAK,iBAAiB;YACpB,CAAC;YAAA,CAAC,EAAE,MAAM,EAAE,UAAU,EAAE,GAAG,IAAA,uCAAgB,EAAC,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,CAAC,CAAC,CAAA;YAC3E,MAAK;QACP,KAAK,SAAS;YACZ,MAAM,GAAG,GAAG,KAAK,UAAU,CAAA;YAC3B,MAAK;QACP,KAAK,aAAa;YAChB,MAAM,GAAG,GAAG,KAAK,cAAc,CAAA;YAC/B,MAAK;QACP,wEAAwE;QACxE,KAAK,UAAU;YACb,MAAM,GAAG,GAAG,KAAK,OAAO,IAAI,EAAE,CAAA;YAC9B,UAAU,GAAG,EAAE,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,CAAA;YAC9B,MAAK;QACP,KAAK,SAAS;YACZ,MAAM,GAAG,GAAG,KAAK,OAAO,IAAI,EAAE,CAAA;YAC9B,UAAU,GAAG,EAAE,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,CAAA;YAC7B,MAAK;QACP,KAAK,cAAc;YACjB,8BAA8B;YAC9B,MAAM,GAAG,IAAI,KAAK,eAAe,KAAK,OAAO,IAAI,GAAG,CAAA;YACpD,UAAU,GAAG,EAAE,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,CAAA;YAC7B,MAAK;QACP,KAAK,aAAa;YAChB,8BAA8B;YAC9B,MAAM,GAAG,IAAI,KAAK,eAAe,KAAK,OAAO,IAAI,GAAG,CAAA;YACpD,UAAU,GAAG,EAAE,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,CAAA;YAC9B,MAAK;QACP,KAAK,YAAY;YACf,MAAM,GAAG,GAAG,KAAK,cAAc,CAAA;YAC/B,MAAK;QACP,KAAK,UAAU;YACb,MAAM,GAAG,GAAG,KAAK,UAAU,CAAA;YAC3B,MAAK;QACP,KAAK,iBAAiB;YACpB,MAAM,GAAG,IAAI,KAAK,eAAe,KAAK,OAAO,IAAI,GAAG,CAAA;YACpD,UAAU,GAAG,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,CAAA;YAC1B,MAAK;QACP,KAAK,SAAS;YACZ,MAAM,GAAG,GAAG,KAAK,cAAc,IAAI,WAAW,IAAI,IAAI,CAAA;YACtD,UAAU,GAAG,EAAE,CAAC,GAAG,IAAI,IAAI,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,GAAG,IAAI,IAAI,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC,EAAE,CAAA;YACnE,MAAK;QACP;YACE;6CACiC;YACjC,MAAM,IAAI,KAAK,CACb,gEAAgE,QAAQ,gBAAgB,KAAK,IAAI,CAClG,CAAA;IACL,CAAC;IAED,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,CAAA;AAC/B,CAAC;AAED;;;;;;;;;GASG;AACI,KAAK,UAAU,gBAAgB,CACpC,OAIC;IAED,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,GAAG,OAAO,CAAA;IAClD,MAAM,OAAO,GAAG,6BAA6B,CAAO;QAClD,GAAG,IAAI;QACP,MAAM,EAAE;YACN,OAAO,EAAE,CAAC,MAAM,EAAE,OAAO,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,CAAC,IAAI,KAAK,KAAK,CAAC;YACxE,MAAM,EAAE,MAAM,EAAE,MAAM;YACtB,SAAS,EAAE,MAAM,EAAE,SAAS;SAC7B;QACD,4HAA4H;QAC5H,YAAY,EAAE,IAAI;QAClB,QAAQ,EAAE,SAAS;KACpB,CAAC,CAAA;IACF,MAAM,EAAE,CAAC,OAAO,CAAC,CAAA;IACjB,MAAM,MAAM,GAAG,GAAG,OAAO,CAAC,KAAK,IAAI,KAAK,EAAE,CAAA;IAC1C,MAAM,IAAI,GAAiD,MAAM,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,SAAS,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,OAAO,EAAE,CAAC,UAAU,EAAE,CAAA;IACtK,OAAO,IAAI,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAA;AACzE,CAAC","sourcesContent":["import { Brackets, EntityMetadata, Repository, SelectQueryBuilder, WhereExpressionBuilder } from 'typeorm'\nimport { DriverUtils } from 'typeorm/driver/DriverUtils.js'\nimport { RelationMetadata } from 'typeorm/metadata/RelationMetadata.js'\nimport { Filter, Sorting, Pagination, ListParam, InheritedValueType } from '../service/common-types/list-param.js'\nimport { Domain } from '../service/domain/domain.js'\nimport { applyDomainInheritanceFilter } from './apply-domain-filter.js'\nimport { applyKeysetCursor, cursorSortings } from './keyset-cursor.js'\nimport { Refusal } from '../refusal/refusal.js'\nimport { membershipClause } from './membership-clause.js'\n\n/**\n * `search` 연산자의 매칭 방식.\n * - contains(기본): `%v%` — 어디든 포함(유연하나 인덱스 미활용).\n * - startsWith: `v%` — 접두 일치(인덱스 활용 가능 → 대용량에서 빠름).\n * - exact: `v` — 정확 일치(대소문자 무시).\n */\nexport type SearchMode = 'contains' | 'startsWith' | 'exact'\n\n/*\n * 바인딩 파라미터 이름 발급기 — 같은 컬럼에 조건이 여럿 걸려도 서로 값을 덮어쓰지 않게 한다.\n * 전역 증가 카운터로 충분하다(파라미터는 질의 하나 안에서만 의미가 있고, 유일하기만 하면 된다).\n */\nlet paramSeq = 0\nconst nextParamKey = (name: string): string => `${name.replace(/[^A-Za-z0-9_]/g, '_')}_p${paramSeq++}`\n\n/**\n * Builds a TypeORM SelectQueryBuilder from a generic ListParam (filters, search, sortings,\n * pagination) plus optional multi-tenant domain scoping. Returns the builder so callers can\n * further chain joins/select/where before executing (e.g. getMany / getManyAndCount).\n *\n * Supported filter operators (`Filter.operator`):\n * eq, noteq, like, nlike, i_like, i_nlike, search, lt, gt, lte, gte,\n * in, notin, notin_with_null, between,\n * is_null, is_not_null, is_true, is_false, is_not_true, is_not_false,\n * is_present, is_blank, is_empty_num_id.\n * An unknown operator throws a clear, actionable error.\n *\n * Behavior notes:\n * - `search` uses case-insensitive substring match (`LOWER(col) LIKE %v%`) and requires the column\n * to be listed in `searchables`.\n * - `like`/`i_like` filters are IGNORED (with a console.warn) unless the column is in `searchables`,\n * to avoid unindexed full-table scans.\n * - Boolean operators are emitted as parameter-bound comparisons (portable across all supported DBs).\n * - Domain scoping is applied only when `domain` is provided, honoring `params.inherited`\n * (None = own domain, Include = own + parent, Only = parent).\n * - Pagination uses skip/take; with joins this triggers TypeORM's two-query strategy for correctness.\n * - Unknown filter columns / operators / relations throw a clear, actionable error (fail-fast).\n *\n * @param options.repository TypeORM repository (or a tx-scoped repository) to build from.\n * @param options.params ListParam: `{ filters, sortings, pagination, inherited }`.\n * @param [options.domain] Optional current domain `{ id, parentId }` for tenant scoping.\n * @param [options.alias] Optional entity alias (defaults to the entity name).\n * @param [options.searchables] Columns eligible for `search`/`like` filtering.\n * @param [options.filtersMap] Maps a filter/sort name to a column or relation column, e.g.\n * `{ ownerName: { columnName: 'name', relationColumn: 'owner' } }`.\n * @param [options.searchMode] `search` matching mode: 'contains'(default) / 'startsWith' / 'exact'.\n * 'startsWith' emits `v%` (prefix) which can use an index on large tables.\n * @returns {SelectQueryBuilder<Type>} The constructed SelectQueryBuilder (not yet executed).\n */\nexport function getQueryBuilderFromListParams<Type>(options: {\n repository: Repository<Type>\n params: ListParam\n domain?: Domain\n alias?: string\n searchables?: string[]\n filtersMap?: { [name: string]: { columnName: string; relationColumn?: string } }\n /** Trusted server-defined joined column expressions. Never populate this from request input. */\n columnExpressions?: Record<string, string>\n searchMode?: SearchMode\n /**\n * 페이지 크기를 안 준 호출에 씌울 기본 상한.\n *\n * 지정하지 않으면 **예전과 똑같이 무제한**이다(하위호환). 다만 무제한으로 나가는 질의는 경고를 남긴다 —\n * 표가 작을 때는 아무 문제가 없다가 데이터가 늘면 조용히 무너지는 종류의 결함이라, 늘기 전에 보여야 한다.\n */\n defaultLimit?: number\n /** 호출자가 요구한 페이지 크기의 상한. 넘겨도 여기서 잘린다(서버 보호). */\n maxLimit?: number\n /**\n * 정렬 허용 컬럼. 주면 목록에 없는 정렬은 **명시 실패**한다.\n *\n * `searchables` 로 인덱스 없는 LIKE 는 막으면서 정렬은 아무 컬럼이나 받던 비대칭을 메운다 —\n * 인덱스 없는 컬럼 정렬은 큰 표에서 전체 정렬 스캔을 부른다.\n */\n sortables?: string[]\n /**\n * 거르기 허용 컬럼. 주면 목록에 없는 칸의 거르기(검색 연산 포함)는 **명시 실패**한다.\n *\n * 고르기 문(`<개념>Choices`, ADR-0139)이 쓴다. 고르기 문은 가리키는 칸만 내는데, 숨긴 칸으로 거를 수\n * 있으면 결과에 끼는지 아닌지로 그 칸의 내용을 맞혀 볼 수 있다. 내는 칸만 거를 수 있게 한다.\n */\n filterables?: string[]\n /**\n * 커서 페이징의 타이브레이커 컬럼. 기본은 엔티티 PK.\n *\n * 마지막 정렬 축은 반드시 **유일**해야 한다 — 같은 값이 여러 행에 있으면 페이지 경계에서\n * 행이 건너뛰어지거나 반복된다.\n */\n cursorTiebreak?: string\n /**\n * The order this list has when the reader sorts by nothing - and, after the reader's columns, the order that\n * settles their ties. Left out, the newest row comes first (`createdAt`, when the entity has it): the latest on top\n * is the rule for every list (the user, 2026-11-02). The primary key closes it, so a page boundary never repeats or\n * drops a row. Keyset paging keeps its own axes (`cursorSortings`) and is not touched.\n */\n defaultSortings?: Sorting[]\n}): SelectQueryBuilder<Type> {\n const {\n repository,\n params,\n domain,\n alias,\n searchables,\n filtersMap = {},\n columnExpressions = {},\n searchMode = 'contains',\n defaultLimit,\n maxLimit,\n sortables,\n filterables,\n cursorTiebreak,\n defaultSortings\n } = options\n const { inherited = InheritedValueType.None } = params || {}\n\n const selectQueryBuilder = repository.createQueryBuilder(alias)\n const entityAlias = selectQueryBuilder.alias\n\n if (filterables) {\n for (const filter of params?.filters ?? []) {\n if (!filterables.includes(filter.name)) {\n throw new Error(\n `[getQueryBuilderFromListParams] filtering by \"${filter.name}\" is not allowed on entity \"${repository.metadata.name}\". ` +\n `Allowed: ${filterables.join(', ')}`\n )\n }\n }\n }\n\n // Apply filters to the query\n const columnFilters =\n params.filters?.filter(filter => {\n if (filter.operator === 'search') {\n return false\n }\n if (filter.operator.toLowerCase().includes('like') && (!searchables || !searchables.includes(filter.name))) {\n console.warn('\"searchables\" setting is required for LIKE searches to avoid heavy database load', filter.name)\n return false\n }\n return true\n }) || []\n\n /*\n * 전역 검색어(`params.search`) → 서버가 자기 `searchables` 로 필드별 search 필터를 만든다.\n * 클라이언트가 필드 목록을 알 필요가 없고, 서버 목록과 어긋나 조용히 무시될 일도 없다.\n * 아래 searchFilters 와 같은 통(OR 묶음)에 들어가므로, filters 로 보낸 것과 함께 써도 자연스럽게 합쳐진다.\n */\n const term = params.search?.trim()\n /*\n * A term this door cannot resolve is refused by name, not dropped (ADR-0060 decision 10). Dropped,\n * the whole list came back and read as \"nothing matches\" - the screen and the door disagreed and\n * the person paid for it. An empty list is the same case: it used to pass the Array check and drop\n * the term without even a warning. The old per-column path (`filters` with operator search) does\n * not come through here.\n */\n if (term && !(searchables instanceof Array && searchables.length > 0)) {\n const entity = repository.metadata.name\n throw new Refusal('list-not-searchable', { entity }, `the ${entity} list cannot be searched: it declares no searchable fields`)\n }\n const termFilters: Filter[] = term ? searchables!.map(name => ({ name, operator: 'search', value: term })) : []\n\n const searchFilters =\n searchables instanceof Array\n ? params.filters?.filter(filter => {\n if (filter.operator !== 'search') {\n return false\n }\n if (!searchables.includes(filter.name)) {\n console.warn(\n '\"searchables\" setting is required for LIKE searches to avoid heavy database load',\n filter.name\n )\n return false\n }\n return true\n }) || []\n : []\n\n const pagination = params.pagination\n const sortings = params.sortings\n const metadata = repository.metadata\n\n // Apply column filters\n if (columnFilters.length > 0) {\n columnFilters.forEach(filter => {\n addCondition(metadata, selectQueryBuilder, selectQueryBuilder, filter, filtersMap, true, searchMode, columnExpressions)\n })\n }\n\n // Apply search filters (explicit per-field search filters + the expanded global term)\n const allSearchFilters = [...searchFilters, ...termFilters]\n if (allSearchFilters.length > 0) {\n selectQueryBuilder.andWhere(\n new Brackets(qb => {\n allSearchFilters.forEach(filter => {\n addCondition(metadata, selectQueryBuilder, qb, filter, filtersMap, false, searchMode, columnExpressions)\n })\n })\n )\n }\n\n // Apply domain filters\n if (domain) {\n applyDomainInheritanceFilter(selectQueryBuilder, entityAlias, domain, inherited)\n }\n\n /* 정렬 화이트리스트 검증은 **호출자가 요구한 축**에만 적용한다(아래에서 우리가 덧붙이는\n * 타이브레이커까지 목록에 넣으라고 요구하면 호출부가 내부 사정을 알아야 한다). */\n if (sortings?.length && sortables) {\n for (const sorting of sortings) {\n if (!sortables.includes(sorting.name)) {\n throw new Error(\n `[getQueryBuilderFromListParams] sorting by \"${sorting.name}\" is not allowed on entity \"${metadata.name}\". ` +\n `Allowed: ${sortables.join(', ')}`\n )\n }\n }\n }\n\n /*\n * 커서(keyset) 페이징 — `after` 가 있으면 offset 을 쓰지 않는다.\n * 정렬 축 끝에 유일 컬럼(기본 PK)을 덧붙여 \"다음 행\" 이 항상 하나로 정해지게 한다.\n */\n const cursor = pagination?.after\n const tiebreak = cursorTiebreak ?? metadata.primaryColumns[0]?.propertyName\n const effectiveSortings = cursor && tiebreak ? cursorSortings(sortings, tiebreak) : settledSortings(sortings, defaultSortings, metadata, tiebreak)\n\n // Apply pagination\n addPagination(selectQueryBuilder, pagination, {\n defaultLimit,\n maxLimit,\n entityName: metadata.name,\n keyset: !!cursor\n })\n\n if (cursor) {\n if (!tiebreak) {\n throw new Error(\n `[getQueryBuilderFromListParams] cursor paging needs a unique tiebreak column but entity \"${metadata.name}\" ` +\n 'exposes no primary column. Pass cursorTiebreak explicitly.'\n )\n }\n applyKeysetCursor(selectQueryBuilder, { alias: entityAlias, sortings: effectiveSortings!, cursor, columnExpressions })\n }\n\n // Apply sorting\n if (effectiveSortings && effectiveSortings.length > 0) {\n addSorting(selectQueryBuilder, effectiveSortings, entityAlias, filtersMap, metadata, columnExpressions)\n }\n\n return selectQueryBuilder\n}\n\n/**\n * The reader's columns, then the list's own order (`defaultSortings`, or the newest first by `createdAt`), then the\n * primary key - each column once, the reader's way kept where they named it.\n */\nexport function settledSortings(\n sortings: Sorting[] | undefined,\n defaultSortings: Sorting[] | undefined,\n metadata: EntityMetadata,\n tiebreak: string | undefined\n): Sorting[] {\n const own = defaultSortings ?? (metadata.findColumnWithPropertyName('createdAt') ? [{ name: 'createdAt', desc: true }] : [])\n const settled: Sorting[] = []\n for (const sorting of [...(sortings ?? []), ...own, ...(tiebreak ? [{ name: tiebreak, desc: true }] : [])]) {\n if (!settled.some(one => one.name === sorting.name)) settled.push(sorting)\n }\n return settled\n}\n\n/**\n * Adds pagination to the SelectQueryBuilder based on the provided Pagination object.\n *\n * @param selectQueryBuilder - The SelectQueryBuilder to which pagination should be applied.\n * @param pagination - The Pagination object containing page and limit information.\n */\nfunction addPagination<T>(\n selectQueryBuilder: SelectQueryBuilder<T>,\n pagination?: Pagination,\n guard: { defaultLimit?: number; maxLimit?: number; entityName?: string; keyset?: boolean } = {}\n) {\n const { defaultLimit, maxLimit, entityName, keyset } = guard\n const { page, limit } = pagination ?? {}\n\n /* 요구된 페이지 크기 → 상한으로 조인다. 상한이 없으면 요구를 그대로 존중(예전 동작). */\n const asked = limit && limit > 0 ? limit : undefined\n const effective = asked === undefined ? defaultLimit : maxLimit ? Math.min(asked, maxLimit) : asked\n\n if (effective === undefined) {\n /* 상한이 아무 데도 없다 = **표 전체가 나간다.** 표가 작을 때는 멀쩡하다가 데이터가 늘면 조용히\n * 무너지는 종류라, 늘기 전에 보이게 한다. 기존 호출부를 깨지 않으려고 막지는 않는다. */\n console.warn(\n `[getQueryBuilderFromListParams] unbounded list query on \"${entityName ?? 'entity'}\" — ` +\n 'no pagination.limit from the caller and no defaultLimit on the resolver, so every matching row will be returned. ' +\n 'Set defaultLimit (and maxLimit) to keep this safe as the table grows.'\n )\n return\n }\n\n /* 커서 모드에서는 offset 을 쓰지 않는다 — 위치가 아니라 값으로 이어 붙이는 것이 커서의 요점이다.\n * (둘을 같이 쓰면 커서로 좁힌 결과를 또 건너뛰어 행이 사라진다.) */\n if (keyset) {\n selectQueryBuilder.take(effective)\n return\n }\n\n if (page && page > 0) {\n selectQueryBuilder.skip(effective * (page - 1)).take(effective)\n } else {\n selectQueryBuilder.take(effective)\n }\n}\n\n/**\n * Adds a filtering condition to the SelectQueryBuilder based on the provided filter and mapping options.\n *\n * @param metadata - The EntityMetadata of the TypeORM entity.\n * @param selectQueryBuilder - The SelectQueryBuilder to which the condition will be added.\n * @param whereExpressionBuilder - The WhereExpressionBuilder to construct the where clause.\n * @param filter - The Filter object containing the filter criteria.\n * @param filtersMap - A mapping of filter names to column names and relation column names.\n * @param andCondition - A flag indicating whether to use \"AND\" or \"OR\" for combining conditions.\n */\nfunction addCondition<T>(\n metadata: EntityMetadata,\n selectQueryBuilder: SelectQueryBuilder<T>,\n whereExpressionBuilder: WhereExpressionBuilder,\n filter: Filter,\n filtersMap: { [name: string]: { columnName: string; relationColumn?: string } } = {},\n andCondition: boolean,\n searchMode: SearchMode = 'contains',\n columnExpressions: Record<string, string> = {}\n): void {\n const { name, operator, value } = filter\n if (Object.prototype.hasOwnProperty.call(columnExpressions, name)) {\n const { clause, parameters } = getClauseAndParameters(columnExpressions[name], nextParamKey(name), operator, value, searchMode)\n if (andCondition) whereExpressionBuilder.andWhere(clause, parameters)\n else whereExpressionBuilder.orWhere(clause, parameters)\n return\n }\n var entityAlias = selectQueryBuilder.alias\n\n var { relationColumn, columnName } = filtersMap[name] || {}\n /*\n 1. relationColumn과 columnName이 지정된 경우 \n - relation inverse 테이블에서, columnName을 찾는다.\n 2. relationColumn만 지정된 경우는 없어야 한다.\n - 이 경우 columnName 은 'name' 이라고 판단한다.\n 3. columnName이 지정된 경우.\n - 이 경우는 columnName 만 적용한다.\n */\n if (relationColumn) {\n const columns = relationColumn.split('.')\n var entityMetadata: EntityMetadata\n var relation: RelationMetadata\n\n for (const rcolumn of columns) {\n if (relation) {\n const { propertyName } = relationColumnMeta\n const property = `${entityAlias}.${propertyName}`\n\n entityAlias = `${entityAlias}-${entityMetadata.tableName}-for-${columnName || 'name'}` as string\n\n if (andCondition) {\n selectQueryBuilder.innerJoin(property, entityAlias)\n } else {\n selectQueryBuilder.leftJoin(property, entityAlias)\n }\n } else {\n entityMetadata = metadata\n }\n\n var relationColumnMeta = entityMetadata.columns.find(column => column.propertyName === rcolumn)\n if (!relationColumnMeta) {\n console.warn(`relationColumn \"${relationColumn}\" in filtersMap for \"${name}\" is not a relation column`)\n return\n }\n\n relation = relationColumnMeta.relationMetadata\n entityMetadata = relation.inverseEntityMetadata\n }\n\n var columnMeta = entityMetadata.columns.find(column => column.propertyName === (columnName || 'name'))\n if (!columnMeta) {\n console.warn(`columnName \"${columnName}\" in filtersMap for \"${name}\" is not a column`)\n return\n }\n } else {\n var columnMeta = metadata.columns.find(column => column.propertyName === (columnName || name))\n if (!columnMeta) {\n /* relationId 에 대한 필터링은 해당 컬럼값 자체의 비교로 한다. */\n var relationIdMeta = metadata.relationIds.find(relationId => relationId.propertyName === (columnName || name))\n if (relationIdMeta) {\n columnMeta = relationIdMeta.relation.joinColumns[0]\n } else {\n /* 미지의 컬럼: 이전엔 warn 후 흘러가 columnMeta.databaseName 에서 불투명 TypeError 로 크래시했다.\n 원인을 바로 알 수 있도록 명확·실행가능한 오류를 던진다(fail-fast). */\n const target = columnName || name\n const available = metadata.columns.map(column => column.propertyName).join(', ')\n throw new Error(\n `[getQueryBuilderFromListParams] unknown filter column \"${target}\" on entity \"${metadata.name}\". ` +\n `Add a filtersMap entry for \"${name}\", or use one of: ${available}`\n )\n }\n } else {\n var relation = columnMeta.relationMetadata\n }\n\n if (relation) {\n /* filterMap에 의해서 relationColumn 이 지정되지 않았더라도, name 또는 columnName의 column이 relation인 경우에는\n - 조건절 구성을 위한 타겟필드명은 'name' 으로만 한정된다.\n */\n var relationColumnMeta = columnMeta\n var entityMetadata = relation.inverseEntityMetadata\n columnMeta = entityMetadata.columns.find(column => column.propertyName === 'name')\n if (!columnMeta) {\n console.warn(`relation column \"${columnName || name}\" does not have \"name\" column`)\n return\n }\n }\n }\n\n const dbNameForColumn = columnMeta.databaseName\n const alias = relationColumnMeta ? `${name}-filter` : entityAlias\n\n /* relation columne인 경우 name을 alias로 사용한다. */\n const field = `${alias}.${dbNameForColumn}`\n\n /*\n * 파라미터 이름은 **조건마다 유일**해야 한다.\n *\n * 예전에는 필터 `name` 을 그대로 플레이스홀더로 썼다. 그래서 **같은 컬럼에 조건을 둘 이상** 걸면\n * 둘 다 `:같은이름` 이 되어 뒤엣것이 앞엣것의 값을 덮어썼다. SQL 에는 조건이 멀쩡히 둘 다 있는데\n * 값은 하나만 남으므로, 오류도 경고도 없이 **결과만 틀린다.**\n *\n * (구간 조회는 전용 `between` 연산자가 있고 그쪽은 `:name_1`·`:name_2` 로 나눠 써서 무사하다.\n * 문제가 되는 건 `gte`+`lte` 를 따로 거는 조합처럼 같은 이름이 두 번 나오는 모든 경우다.)\n */\n var { clause, parameters } = getClauseAndParameters(field, nextParamKey(name), operator, value, searchMode)\n\n if (relationColumnMeta) {\n const { propertyName } = relationColumnMeta\n const property = `${entityAlias}.${propertyName}`\n if (andCondition) {\n selectQueryBuilder.innerJoin(property, alias, clause, parameters)\n } else {\n selectQueryBuilder.leftJoin(property, alias)\n whereExpressionBuilder.orWhere(clause, parameters)\n }\n } else {\n andCondition\n ? whereExpressionBuilder.andWhere(clause, parameters)\n : whereExpressionBuilder.orWhere(clause, parameters)\n }\n}\n\n/**\n * Adds sorting to the SelectQueryBuilder based on the provided Sorting objects.\n *\n * @param selectQueryBuilder - The SelectQueryBuilder to which sorting should be applied.\n * @param sortings - An array of Sorting objects defining the sort order.\n * @param entityAlias - The alias of the entity in the query.\n * @param filtersMap - A mapping of filter names to column names and relation column names.\n * @param metadata - The EntityMetadata of the TypeORM entity.\n */\nfunction addSorting<T>(\n selectQueryBuilder: SelectQueryBuilder<T>,\n sortings: Sorting[],\n entityAlias: string,\n filtersMap: { [name: string]: { columnName: string; relationColumn?: string } },\n metadata: EntityMetadata,\n columnExpressions: Record<string, string> = {}\n) {\n sortings.forEach((sorting, index) => {\n const sortField = Object.prototype.hasOwnProperty.call(columnExpressions, sorting.name)\n ? columnExpressions[sorting.name]\n : determineSortField(sorting.name, entityAlias, filtersMap, selectQueryBuilder, metadata)\n if (index === 0) {\n selectQueryBuilder.orderBy(sortField, sorting.desc ? 'DESC' : 'ASC')\n } else {\n selectQueryBuilder.addOrderBy(sortField, sorting.desc ? 'DESC' : 'ASC')\n }\n })\n}\n\n/**\n * Determines the sorting field for a given sorting name, considering possible relation columns.\n *\n * @param sortingName - The name of the field to sort by.\n * @param entityAlias - The alias of the entity in the query.\n * @param filtersMap - A mapping of filter names to column names and relation column names.\n * @param selectQueryBuilder - The SelectQueryBuilder instance to apply sorting to.\n * @param metadata - The EntityMetadata of the TypeORM entity.\n * @returns {string} - The fully qualified sorting field.\n */\nfunction determineSortField<T>(\n sortingName: string,\n entityAlias: string,\n filtersMap: { [name: string]: { columnName: string; relationColumn?: string } },\n selectQueryBuilder: SelectQueryBuilder<T>,\n metadata: EntityMetadata\n): string {\n const filter = filtersMap[sortingName]\n\n if (!filter) {\n return sortingName.split('.').length > 1 ? sortingName : `${entityAlias}.${sortingName}`\n }\n\n const { columnName, relationColumn } = filter\n\n if (relationColumn) {\n const relationAlias = applyJoins(\n selectQueryBuilder,\n entityAlias,\n relationColumn,\n metadata,\n 'leftJoin',\n columnName || sortingName,\n true\n )\n return `${relationAlias}.${columnName}`\n } else {\n return `${entityAlias}.${columnName}`\n }\n}\n\n/**\n * Applies the necessary joins to the SelectQueryBuilder based on the relation column.\n *\n * @param selectQueryBuilder - The SelectQueryBuilder where the joins will be applied.\n * @param entityAlias - The current alias of the entity in the query.\n * @param relationColumn - The dot-notated string representing the relation chain (e.g., \"user.profile.address\").\n * @param metadata - The EntityMetadata of the entity.\n * @param joinType - The type of join to use (\"innerJoin\" or \"leftJoin\").\n * @param columnName - The name of the column used for filtering or sorting, default to 'name'.\n * @param selectField - Whether to include the field in the SELECT clause.\n * @returns {string} - The alias to be used for the final field in the relation chain.\n */\nfunction applyJoins<T>(\n selectQueryBuilder: SelectQueryBuilder<T>,\n entityAlias: string,\n relationColumn: string,\n metadata: EntityMetadata,\n joinType: 'innerJoin' | 'leftJoin' = 'leftJoin',\n columnName: string = 'name',\n selectField: boolean = false\n): string {\n const columns = relationColumn.split('.')\n let currentAlias = entityAlias\n let currentMetadata = metadata\n\n for (const column of columns) {\n const relation = currentMetadata.relations.find(rel => rel.propertyName === column)\n\n if (!relation) {\n const available = currentMetadata.relations.map(rel => rel.propertyName).join(', ')\n throw new Error(\n `[getQueryBuilderFromListParams] relation \"${column}\" not found on entity \"${currentMetadata.name}\"` +\n ` (relationColumn \"${relationColumn}\"). Available relations: ${available}`\n )\n }\n\n const nextAlias = `${currentAlias}_${relation.inverseEntityMetadata.tableName}_for_${columnName}`\n\n if (!selectQueryBuilder.expressionMap.aliases.some(alias => alias.name === nextAlias)) {\n selectQueryBuilder[joinType](`${currentAlias}.${column}`, nextAlias)\n }\n if (selectField && columns.at(-1) == column /* 최종 alias만 추가 */) {\n /*\n * The select alias has to be spelled the way TypeORM will look it up.\n *\n * Sorting by a joined column and paginating puts the query on TypeORM's distinct-id path,\n * which wraps everything in a subquery and re-addresses each ORDER BY as\n * `distinctAlias.<buildAlias(aliasName, column.databaseName)>` — the **database** name (see\n * SelectQueryBuilder.createOrderByCombinedWithSelectExpression). This addSelect used the\n * **property** name, so the two only met where the two spellings happen to be identical.\n *\n * name, description and priority are one lowercase word each, so they were identical and it\n * worked by luck. dueAt is not: the property is `dueAt`, the column is `due_at` under the\n * snake_case NamingStrategy, and sorting 내 결재함 by 기한 died with\n *\n * SQLITE_ERROR: no such column:\n * distinctAlias.ap_activity_threads_for_dueAt_activity_instances_for_dueAt_due_at\n *\n * Building the alias the same way TypeORM does makes them agree by construction. Lowercase\n * columns produce the identical string as before, so nothing else moves.\n */\n const targetColumn = relation.inverseEntityMetadata.findColumnWithPropertyName(columnName)\n\n if (!targetColumn) {\n /*\n * columnName is a property name, not a database column name — `dueAt`, never `due_at`.\n * Failing loudly here beats an alias nobody can resolve: TypeORM's own lookup on the\n * distinct path dereferences the column without a null check, so a wrong spelling would\n * surface much later as a TypeError with no mention of this map.\n */\n const available = relation.inverseEntityMetadata.columns.map(col => col.propertyName).join(', ')\n throw new Error(\n `[getQueryBuilderFromListParams] column \"${columnName}\" not found on entity` +\n ` \"${relation.inverseEntityMetadata.name}\" (relationColumn \"${relationColumn}\").` +\n ` Use the property name, not the database column name. Available: ${available}`\n )\n }\n\n selectQueryBuilder.addSelect(\n `${nextAlias}.${columnName}`,\n DriverUtils.buildAlias(selectQueryBuilder.connection.driver, undefined, nextAlias, targetColumn.databaseName)\n )\n }\n\n currentAlias = nextAlias\n currentMetadata = relation.inverseEntityMetadata\n }\n\n return currentAlias\n}\n\n/** search 값에 searchMode 에 맞는 와일드카드를 씌운다. */\nfunction wrapSearch(value: string, mode: SearchMode): string {\n switch (mode) {\n case 'startsWith':\n return `${value}%`\n case 'exact':\n return value\n case 'contains':\n default:\n return `%${value}%`\n }\n}\n\n/**\n * Generates the SQL clause and parameters based on the provided filter.\n *\n * @param field - The database field to filter on.\n * @param name - The name of the filter.\n * @param operator - The operator to use in the filter.\n * @param value - The value to filter with.\n * @returns An object containing the SQL clause and the parameters.\n */\nfunction getClauseAndParameters(\n field: string,\n name: string,\n operator: string,\n value: any,\n searchMode: SearchMode = 'contains'\n): { clause: string; parameters: { [key: string]: any } } {\n const values = value instanceof Array ? value : [value]\n let clause = ''\n let parameters: { [key: string]: any } = {}\n\n switch (operator) {\n case 'eq':\n clause = `${field} = :${name}`\n parameters = { [name]: value }\n break\n case 'like':\n clause = `${field} LIKE :${name}`\n parameters = { [name]: `%${value}%` }\n break\n case 'search':\n /* searchMode 로 매칭 방식 선택(기본 contains). startsWith 는 접두 인덱스 활용 가능. */\n clause = `LOWER(${field}) LIKE :${name}`\n parameters = { [name]: wrapSearch(String(value).toLowerCase(), searchMode) }\n break\n case 'i_like':\n clause = `LOWER(${field}) LIKE :${name}`\n parameters = { [name]: `%${String(value).toLowerCase()}%` }\n break\n case 'nlike':\n clause = `${field} NOT LIKE :${name}`\n parameters = { [name]: `%${value}%` }\n break\n case 'i_nlike':\n clause = `LOWER(${field}) NOT LIKE :${name}`\n parameters = { [name]: `%${String(value).toLowerCase()}%` }\n break\n case 'lt':\n clause = `${field} < :${name}`\n parameters = { [name]: value }\n break\n case 'gt':\n clause = `${field} > :${name}`\n parameters = { [name]: value }\n break\n case 'lte':\n clause = `${field} <= :${name}`\n parameters = { [name]: value }\n break\n case 'gte':\n clause = `${field} >= :${name}`\n parameters = { [name]: value }\n break\n case 'noteq':\n clause = `${field} != :${name}`\n parameters = { [name]: value }\n break\n /* What an empty list means, and the parentheses around OR, are decided in membershipClause alone. */\n case 'in':\n case 'notin':\n case 'notin_with_null':\n ;({ clause, parameters } = membershipClause(field, operator, values, name))\n break\n case 'is_null':\n clause = `${field} IS NULL`\n break\n case 'is_not_null':\n clause = `${field} IS NOT NULL`\n break\n /* boolean 비교는 파라미터 바인딩으로 (IS TRUE/FALSE 는 MSSQL/Oracle 미지원 → 이식성 확보) */\n case 'is_false':\n clause = `${field} = :${name}`\n parameters = { [name]: false }\n break\n case 'is_true':\n clause = `${field} = :${name}`\n parameters = { [name]: true }\n break\n case 'is_not_false':\n /* NOT FALSE = true 또는 NULL */\n clause = `(${field} IS NULL OR ${field} = :${name})`\n parameters = { [name]: true }\n break\n case 'is_not_true':\n /* NOT TRUE = false 또는 NULL */\n clause = `(${field} IS NULL OR ${field} = :${name})`\n parameters = { [name]: false }\n break\n case 'is_present':\n clause = `${field} IS NOT NULL`\n break\n case 'is_blank':\n clause = `${field} IS NULL`\n break\n case 'is_empty_num_id':\n clause = `(${field} IS NULL OR ${field} = :${name})`\n parameters = { [name]: 0 }\n break\n case 'between':\n clause = `${field} BETWEEN :${name}_1 AND :${name}_2`\n parameters = { [`${name}_1`]: values[0], [`${name}_2`]: values[1] }\n break\n default:\n /* 지원하지 않는 연산자: 이전엔 clause='' 로 andWhere('') 가 되어 malformed/무음이었다.\n 명확한 오류로 원인을 알린다(fail-fast). */\n throw new Error(\n `[getQueryBuilderFromListParams] unsupported filter operator \"${operator}\" for field \"${field}\".`\n )\n }\n\n return { clause, parameters }\n}\n\n/**\n * How many rows of a list hold each value of one field - the numbers beside a list's filter chips (an opportunity's\n * stages, a request's statuses). Counted by the database, grouped by the field, under the list's own conditions:\n * the same options as the list (domain, filters, search, searchables ...), less the conditions on `field` itself, so\n * each chip says how many it would show whichever chips are pressed. The list's pagination and sorting play no part;\n * the grouped rows are left unsorted (the builder sorts a list newest first, which a GROUP BY cannot carry).\n *\n * `field` is a plain column of the entity. A value no row holds is absent from the map - a caller lists its own values,\n * each as a `ListValueCount`.\n */\nexport async function countListByField<Type>(\n options: Parameters<typeof getQueryBuilderFromListParams<Type>>[0] & {\n field: string\n /** The conditions a list door adds of its own after the builder (the caller's own rows, say) - added here alike. */\n narrow?: (builder: SelectQueryBuilder<Type>) => void\n }\n): Promise<Map<string, number>> {\n const { field, params, narrow, ...rest } = options\n const builder = getQueryBuilderFromListParams<Type>({\n ...rest,\n params: {\n filters: (params?.filters ?? []).filter(filter => filter.name !== field),\n search: params?.search,\n inherited: params?.inherited\n },\n /* The groups are bounded by the field's values, not by the table; this only keeps the builder's unbounded warning quiet. */\n defaultLimit: 1000,\n maxLimit: undefined\n })\n narrow?.(builder)\n const column = `${builder.alias}.${field}`\n const rows: { value: unknown; count: string | number }[] = await builder.select(column, 'value').addSelect('COUNT(*)', 'count').groupBy(column).orderBy().getRawMany()\n return new Map(rows.map(row => [String(row.value), Number(row.count)]))\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@things-factory/shell",
|
|
3
|
-
"version": "10.1.
|
|
3
|
+
"version": "10.1.246",
|
|
4
4
|
"description": "Core module for framework",
|
|
5
5
|
"bin": {
|
|
6
6
|
"things-factory": "bin/things-factory",
|
|
@@ -142,5 +142,5 @@
|
|
|
142
142
|
"pg": "^8.7.3",
|
|
143
143
|
"sqlite3": "^5.0.8"
|
|
144
144
|
},
|
|
145
|
-
"gitHead": "
|
|
145
|
+
"gitHead": "46c0d3c93edf765cc1f13040b1f9d03fd1d9e60b"
|
|
146
146
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import 'reflect-metadata'
|
|
2
2
|
import { Column, DataSource, Entity, ManyToOne, PrimaryColumn, Repository } from 'typeorm'
|
|
3
3
|
|
|
4
|
-
import { getQueryBuilderFromListParams } from '../server/utils/get-query-builder-from-list-params.js'
|
|
4
|
+
import { countListByField, getQueryBuilderFromListParams } from '../server/utils/get-query-builder-from-list-params.js'
|
|
5
5
|
import { InheritedValueType, ListParam } from '../server/service/common-types/list-param.js'
|
|
6
6
|
import { Domain } from '../server/service/domain/domain.js'
|
|
7
7
|
|
|
@@ -41,6 +41,16 @@ class Item {
|
|
|
41
41
|
category!: Category | null
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
/* A row with the day it was made - the list's newest-first rule reads it. */
|
|
45
|
+
@Entity()
|
|
46
|
+
class Entry {
|
|
47
|
+
@PrimaryColumn({ type: 'integer' })
|
|
48
|
+
id!: number
|
|
49
|
+
|
|
50
|
+
@Column({ type: 'text' })
|
|
51
|
+
createdAt!: string
|
|
52
|
+
}
|
|
53
|
+
|
|
44
54
|
/* 도메인 상속 테스트용 stub (빌더는 domain.id / domain.parentId 만 참조) */
|
|
45
55
|
const DOMAIN_A = { id: 'A', parentId: 'ROOT' } as unknown as Domain
|
|
46
56
|
|
|
@@ -52,7 +62,7 @@ beforeAll(async () => {
|
|
|
52
62
|
type: 'better-sqlite3',
|
|
53
63
|
database: ':memory:',
|
|
54
64
|
synchronize: true,
|
|
55
|
-
entities: [Item, Category]
|
|
65
|
+
entities: [Item, Category, Entry]
|
|
56
66
|
})
|
|
57
67
|
await dataSource.initialize()
|
|
58
68
|
|
|
@@ -70,6 +80,15 @@ beforeAll(async () => {
|
|
|
70
80
|
])
|
|
71
81
|
})
|
|
72
82
|
|
|
83
|
+
beforeAll(async () => {
|
|
84
|
+
await dataSource.getRepository(Entry).save([
|
|
85
|
+
{ id: 1, createdAt: '2026-10-01' },
|
|
86
|
+
{ id: 2, createdAt: '2026-11-01' },
|
|
87
|
+
{ id: 3, createdAt: '2026-09-01' },
|
|
88
|
+
{ id: 4, createdAt: '2026-11-01' }
|
|
89
|
+
])
|
|
90
|
+
})
|
|
91
|
+
|
|
73
92
|
afterAll(async () => {
|
|
74
93
|
await dataSource?.destroy()
|
|
75
94
|
})
|
|
@@ -169,6 +188,37 @@ describe('정렬', () => {
|
|
|
169
188
|
})
|
|
170
189
|
})
|
|
171
190
|
|
|
191
|
+
describe('the order a list has when the reader sorts by nothing (the user, 2026-11-02)', () => {
|
|
192
|
+
const order = async (repo: Repository<any>, params: any, extra: any = {}) =>
|
|
193
|
+
((await getQueryBuilderFromListParams({ repository: repo, params: P([], params), ...extra }).getMany()) as any[]).map(r => r.id)
|
|
194
|
+
|
|
195
|
+
it('the newest first by createdAt, the primary key settling a tie', async () => {
|
|
196
|
+
expect(await order(dataSource.getRepository(Entry), {})).toEqual([4, 2, 1, 3])
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
it('a page boundary neither repeats nor drops a row', async () => {
|
|
200
|
+
const repo = dataSource.getRepository(Entry)
|
|
201
|
+
const pages = [
|
|
202
|
+
...(await order(repo, { pagination: { page: 1, limit: 2 } })),
|
|
203
|
+
...(await order(repo, { pagination: { page: 2, limit: 2 } }))
|
|
204
|
+
]
|
|
205
|
+
expect(pages).toEqual([4, 2, 1, 3])
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
it('without createdAt, the primary key alone - the latest key first', async () => {
|
|
209
|
+
expect(await order(repository, {})).toEqual([5, 4, 3, 2, 1])
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
it("the list's own order when it declares one", async () => {
|
|
213
|
+
expect(await order(repository, {}, { defaultSortings: [{ name: 'qty' }] })).toEqual([4, 2, 3, 1, 5])
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
it("the reader's column first, the list's own order settling its ties", async () => {
|
|
217
|
+
expect(await order(dataSource.getRepository(Entry), { sortings: [{ name: 'createdAt' }] })).toEqual([3, 1, 4, 2])
|
|
218
|
+
expect(await order(repository, { sortings: [{ name: 'domain' }] }, { defaultSortings: [{ name: 'qty', desc: true }] })).toEqual([5, 1, 2, 3, 4])
|
|
219
|
+
})
|
|
220
|
+
})
|
|
221
|
+
|
|
172
222
|
describe('도메인 상속 (None / Include / Only / 없음)', () => {
|
|
173
223
|
it('None: 해당 도메인만', async () => {
|
|
174
224
|
expect(await ids({ params: P([]), domain: DOMAIN_A })).toEqual([1, 2, 5])
|
|
@@ -266,3 +316,23 @@ describe('거르기 허용 목록 (filterables) — 고르기 문, ADR-0139', ()
|
|
|
266
316
|
expect(await ids({ params: P([{ name: 'qty', operator: 'eq', value: 10 }]) })).toEqual([1])
|
|
267
317
|
})
|
|
268
318
|
})
|
|
319
|
+
|
|
320
|
+
describe('필터 알약 건수 (countListByField)', () => {
|
|
321
|
+
const counts = async (params: ListParam, extra: any = {}) => Object.fromEntries(await countListByField<Item>({ repository, field: 'qty', params, ...extra }))
|
|
322
|
+
|
|
323
|
+
it('★ 한 칸으로 묶어 세되 그 칸의 조건은 빼고, 나머지 조건(검색·필터·도메인)은 목록과 같게 건다', async () => {
|
|
324
|
+
expect(await counts(P([]), { domain: DOMAIN_A })).toEqual({ '0': 1, '10': 1, '20': 1 })
|
|
325
|
+
expect(await counts(P([{ name: 'qty', operator: 'in', value: [10] }]), { domain: DOMAIN_A })).toEqual({ '0': 1, '10': 1, '20': 1 })
|
|
326
|
+
expect(await counts(P([], { search: 'ap' }), { domain: DOMAIN_A, searchables: ['name'] })).toEqual({ '0': 1, '10': 1 })
|
|
327
|
+
expect(await counts(P([{ name: 'name', operator: 'eq', value: 'Banana' }]))).toEqual({ '5': 1 })
|
|
328
|
+
})
|
|
329
|
+
|
|
330
|
+
it('목록 문이 빌더 뒤에 덧붙이는 자기 조건(narrow)도 건수에 같게 걸린다', async () => {
|
|
331
|
+
expect(await counts(P([]), { narrow: (qb: any) => qb.andWhere(`${qb.alias}.qty >= :least`, { least: 10 }) })).toEqual({ '10': 1, '20': 1 })
|
|
332
|
+
})
|
|
333
|
+
|
|
334
|
+
it('목록의 정렬·페이지는 건수에 끼지 않는다 — 정렬을 붙이는 빌더 위에서도 묶음 질의가 선다', async () => {
|
|
335
|
+
const all = await counts(P([], { sortings: [{ name: 'name' }], pagination: { page: 2, limit: 1 } }))
|
|
336
|
+
expect(Object.values(all).reduce((sum, one) => sum + one, 0)).toBe(5)
|
|
337
|
+
})
|
|
338
|
+
})
|