@things-factory/shell 10.1.205 → 10.1.206

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.
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.buildCondition = void 0;
4
4
  const tslib_1 = require("tslib");
5
5
  const lodash_1 = tslib_1.__importDefault(require("lodash"));
6
+ const membership_clause_js_1 = require("./membership-clause.js");
6
7
  /**
7
8
  * Generates and returns a condition clause based on the provided parameters.
8
9
  *
@@ -70,27 +71,14 @@ const buildCondition = function (alias, fieldName, operator, value, relation, se
70
71
  clause: `${alias}.${fieldName} != :args${seq}`,
71
72
  parameters: { [`args${seq}`]: value }
72
73
  };
74
+ /* What an empty list means, and the parentheses around OR, are decided in membershipClause alone. */
73
75
  case 'in':
74
- return {
75
- clause: relation ? `${fieldName}.id IN (:...args${seq})` : `${alias}.${fieldName} IN (:...args${seq})`,
76
- parameters: { [`args${seq}`]: values }
77
- };
78
76
  case 'notin':
79
- return {
80
- clause: relation ? `${fieldName}.id NOT IN (:...args${seq})` : `${alias}.${fieldName} NOT IN (:...args${seq})`,
81
- parameters: { [`args${seq}`]: values }
82
- };
83
- /* OR 를 품은 절은 **반드시 스스로 괄호로 닫는다** — 호출부는 이 문자열을 andWhere 에 그대로 넘기고,
84
- * TypeORM 은 문자열 조건을 감싸주지 않는다(isolateWhereStatements 기본 false). 괄호가 없으면
85
- * `A AND B OR C` 가 되어 앞의 조건(도메인·테넌트 포함)이 통째로 무력화된다.
86
- * 예전 코드는 양쪽 다 괄호 짝이 어긋나 있었다(여는 것만 / 닫는 것만) — SQL 문법 오류. */
87
- case 'notin_with_null':
88
- return {
89
- clause: relation
90
- ? `(${fieldName}.id IS NULL OR ${fieldName}.id NOT IN (:...args${seq}))`
91
- : `(${alias}.${fieldName} IS NULL OR ${alias}.${fieldName} NOT IN (:...args${seq}))`,
92
- parameters: { [`args${seq}`]: values }
93
- };
77
+ case 'notin_with_null': {
78
+ const field = relation ? `${fieldName}.id` : `${alias}.${fieldName}`;
79
+ const { clause, parameters } = (0, membership_clause_js_1.membershipClause)(field, operator, values, `args${seq}`);
80
+ return { clause, parameters };
81
+ }
94
82
  case 'is_null':
95
83
  return {
96
84
  clause: `${alias}.${fieldName} IS NULL`
@@ -1 +1 @@
1
- {"version":3,"file":"condition-builder.js","sourceRoot":"","sources":["../../server/utils/condition-builder.ts"],"names":[],"mappings":"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","sourcesContent":["import _ from 'lodash'\n\n/**\n * Generates and returns a condition clause based on the provided parameters.\n *\n * @param alias {string} Alias or table name of the entity.\n * @param fieldName {string} Field or column name.\n * @param operator {string} Comparison operator.\n * @param value {any} Value or an array of values to compare.\n * @param relation {boolean | string} Indicates if it's a related field or the name of the relation (optional).\n * @param seq {number} Integer representing the order of condition generation (internal use).\n * @returns {Object} An object containing the generated condition clause and parameters.\n */\nexport const buildCondition = function (alias: string, fieldName: string, operator: string, value: any, relation: boolean | string, seq: number) {\n seq++\n\n fieldName = _.snakeCase(fieldName)\n const values = value instanceof Array ? value : [value]\n\n switch (operator) {\n case 'eq':\n return {\n clause: `${alias}.${fieldName} = :args${seq}`,\n parameters: { [`args${seq}`]: value }\n }\n\n case 'like':\n return {\n clause: `${alias}.${fieldName} LIKE :args${seq}`,\n parameters: { [`args${seq}`]: `${value}` }\n }\n\n case 'search':\n case 'i_like':\n return {\n clause: `LOWER(${alias}.${fieldName}) LIKE :args${seq}`,\n parameters: { [`args${seq}`]: `${String(value).toLowerCase()}` }\n }\n\n case 'nlike':\n return {\n clause: `${alias}.${fieldName} NOT LIKE :args${seq}`,\n value: { [`args${seq}`]: `${value}` }\n }\n\n case 'i_nlike':\n return {\n clause: `LOWER(${alias}.${fieldName}) NOT LIKE :args${seq}`,\n value: { [`args${seq}`]: `${String(value).toLowerCase()}` }\n }\n\n case 'lt':\n return {\n clause: `${alias}.${fieldName} < :args${seq}`,\n parameters: { [`args${seq}`]: value }\n }\n\n case 'gt':\n return {\n clause: `${alias}.${fieldName} > :args${seq}`,\n parameters: { [`args${seq}`]: value }\n }\n\n case 'lte':\n return {\n clause: `${alias}.${fieldName} <= :args${seq}`,\n parameters: { [`args${seq}`]: value }\n }\n\n case 'gte':\n return {\n clause: `${alias}.${fieldName} >= :args${seq}`,\n parameters: { [`args${seq}`]: value }\n }\n\n case 'noteq':\n return {\n clause: `${alias}.${fieldName} != :args${seq}`,\n parameters: { [`args${seq}`]: value }\n }\n\n case 'in':\n return {\n clause: relation ? `${fieldName}.id IN (:...args${seq})` : `${alias}.${fieldName} IN (:...args${seq})`,\n parameters: { [`args${seq}`]: values }\n }\n\n case 'notin':\n return {\n clause: relation ? `${fieldName}.id NOT IN (:...args${seq})` : `${alias}.${fieldName} NOT IN (:...args${seq})`,\n parameters: { [`args${seq}`]: values }\n }\n\n /* OR 를 품은 절은 **반드시 스스로 괄호로 닫는다** — 호출부는 이 문자열을 andWhere 에 그대로 넘기고,\n * TypeORM 은 문자열 조건을 감싸주지 않는다(isolateWhereStatements 기본 false). 괄호가 없으면\n * `A AND B OR C` 가 되어 앞의 조건(도메인·테넌트 포함)이 통째로 무력화된다.\n * 예전 코드는 양쪽 다 괄호 짝이 어긋나 있었다(여는 것만 / 닫는 것만) — SQL 문법 오류. */\n case 'notin_with_null':\n return {\n clause: relation\n ? `(${fieldName}.id IS NULL OR ${fieldName}.id NOT IN (:...args${seq}))`\n : `(${alias}.${fieldName} IS NULL OR ${alias}.${fieldName} NOT IN (:...args${seq}))`,\n parameters: { [`args${seq}`]: values }\n }\n\n case 'is_null':\n return {\n clause: `${alias}.${fieldName} IS NULL`\n }\n case 'is_not_null':\n return {\n clause: `${alias}.${fieldName} IS NOT NULL`\n }\n /* boolean 비교는 파라미터 바인딩으로 (IS TRUE/FALSE 는 MSSQL/Oracle 미지원 → 이식성 확보) */\n case 'is_false':\n return {\n clause: `${alias}.${fieldName} = :args${seq}`,\n parameters: { [`args${seq}`]: false }\n }\n case 'is_true':\n return {\n clause: `${alias}.${fieldName} = :args${seq}`,\n parameters: { [`args${seq}`]: true }\n }\n case 'is_not_false':\n /* NOT FALSE = true 또는 NULL */\n return {\n clause: `(${alias}.${fieldName} IS NULL OR ${alias}.${fieldName} = :args${seq})`,\n parameters: { [`args${seq}`]: true }\n }\n case 'is_not_true':\n /* NOT TRUE = false 또는 NULL */\n return {\n clause: `(${alias}.${fieldName} IS NULL OR ${alias}.${fieldName} = :args${seq})`,\n parameters: { [`args${seq}`]: false }\n }\n case 'is_present':\n return {\n clause: `${alias}.${fieldName} IS NOT NULL`\n }\n case 'is_blank':\n return {\n clause: `${alias}.${fieldName} IS NULL`\n }\n case 'is_empty_num_id':\n return {\n clause: `(${alias}.${fieldName} IS NULL OR ${alias}.${fieldName} = :args${seq})`,\n parameters: { [`args${seq}`]: 0 }\n }\n\n case 'between':\n return {\n clause: `${alias}.${fieldName} BETWEEN :args${seq}_1 AND :args${seq}_2`,\n parameters: { [`args${seq}_1`]: values[0], [`args${seq}_2`]: values[1] }\n }\n }\n}\n"]}
1
+ {"version":3,"file":"condition-builder.js","sourceRoot":"","sources":["../../server/utils/condition-builder.ts"],"names":[],"mappings":"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","sourcesContent":["import _ from 'lodash'\n\nimport { membershipClause } from './membership-clause.js'\n\n/**\n * Generates and returns a condition clause based on the provided parameters.\n *\n * @param alias {string} Alias or table name of the entity.\n * @param fieldName {string} Field or column name.\n * @param operator {string} Comparison operator.\n * @param value {any} Value or an array of values to compare.\n * @param relation {boolean | string} Indicates if it's a related field or the name of the relation (optional).\n * @param seq {number} Integer representing the order of condition generation (internal use).\n * @returns {Object} An object containing the generated condition clause and parameters.\n */\nexport const buildCondition = function (alias: string, fieldName: string, operator: string, value: any, relation: boolean | string, seq: number) {\n seq++\n\n fieldName = _.snakeCase(fieldName)\n const values = value instanceof Array ? value : [value]\n\n switch (operator) {\n case 'eq':\n return {\n clause: `${alias}.${fieldName} = :args${seq}`,\n parameters: { [`args${seq}`]: value }\n }\n\n case 'like':\n return {\n clause: `${alias}.${fieldName} LIKE :args${seq}`,\n parameters: { [`args${seq}`]: `${value}` }\n }\n\n case 'search':\n case 'i_like':\n return {\n clause: `LOWER(${alias}.${fieldName}) LIKE :args${seq}`,\n parameters: { [`args${seq}`]: `${String(value).toLowerCase()}` }\n }\n\n case 'nlike':\n return {\n clause: `${alias}.${fieldName} NOT LIKE :args${seq}`,\n value: { [`args${seq}`]: `${value}` }\n }\n\n case 'i_nlike':\n return {\n clause: `LOWER(${alias}.${fieldName}) NOT LIKE :args${seq}`,\n value: { [`args${seq}`]: `${String(value).toLowerCase()}` }\n }\n\n case 'lt':\n return {\n clause: `${alias}.${fieldName} < :args${seq}`,\n parameters: { [`args${seq}`]: value }\n }\n\n case 'gt':\n return {\n clause: `${alias}.${fieldName} > :args${seq}`,\n parameters: { [`args${seq}`]: value }\n }\n\n case 'lte':\n return {\n clause: `${alias}.${fieldName} <= :args${seq}`,\n parameters: { [`args${seq}`]: value }\n }\n\n case 'gte':\n return {\n clause: `${alias}.${fieldName} >= :args${seq}`,\n parameters: { [`args${seq}`]: value }\n }\n\n case 'noteq':\n return {\n clause: `${alias}.${fieldName} != :args${seq}`,\n parameters: { [`args${seq}`]: value }\n }\n\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 const field = relation ? `${fieldName}.id` : `${alias}.${fieldName}`\n const { clause, parameters } = membershipClause(field, operator, values, `args${seq}`)\n return { clause, parameters }\n }\n\n case 'is_null':\n return {\n clause: `${alias}.${fieldName} IS NULL`\n }\n case 'is_not_null':\n return {\n clause: `${alias}.${fieldName} IS NOT NULL`\n }\n /* boolean 비교는 파라미터 바인딩으로 (IS TRUE/FALSE 는 MSSQL/Oracle 미지원 → 이식성 확보) */\n case 'is_false':\n return {\n clause: `${alias}.${fieldName} = :args${seq}`,\n parameters: { [`args${seq}`]: false }\n }\n case 'is_true':\n return {\n clause: `${alias}.${fieldName} = :args${seq}`,\n parameters: { [`args${seq}`]: true }\n }\n case 'is_not_false':\n /* NOT FALSE = true 또는 NULL */\n return {\n clause: `(${alias}.${fieldName} IS NULL OR ${alias}.${fieldName} = :args${seq})`,\n parameters: { [`args${seq}`]: true }\n }\n case 'is_not_true':\n /* NOT TRUE = false 또는 NULL */\n return {\n clause: `(${alias}.${fieldName} IS NULL OR ${alias}.${fieldName} = :args${seq})`,\n parameters: { [`args${seq}`]: false }\n }\n case 'is_present':\n return {\n clause: `${alias}.${fieldName} IS NOT NULL`\n }\n case 'is_blank':\n return {\n clause: `${alias}.${fieldName} IS NULL`\n }\n case 'is_empty_num_id':\n return {\n clause: `(${alias}.${fieldName} IS NULL OR ${alias}.${fieldName} = :args${seq})`,\n parameters: { [`args${seq}`]: 0 }\n }\n\n case 'between':\n return {\n clause: `${alias}.${fieldName} BETWEEN :args${seq}_1 AND :args${seq}_2`,\n parameters: { [`args${seq}_1`]: values[0], [`args${seq}_2`]: values[1] }\n }\n }\n}\n"]}
@@ -7,6 +7,7 @@ const list_param_js_1 = require("../service/common-types/list-param.js");
7
7
  const apply_domain_filter_js_1 = require("./apply-domain-filter.js");
8
8
  const keyset_cursor_js_1 = require("./keyset-cursor.js");
9
9
  const refusal_js_1 = require("../refusal/refusal.js");
10
+ const membership_clause_js_1 = require("./membership-clause.js");
10
11
  /*
11
12
  * 바인딩 파라미터 이름 발급기 — 같은 컬럼에 조건이 여럿 걸려도 서로 값을 덮어쓰지 않게 한다.
12
13
  * 전역 증가 카운터로 충분하다(파라미터는 질의 하나 안에서만 의미가 있고, 유일하기만 하면 된다).
@@ -505,23 +506,12 @@ function getClauseAndParameters(field, name, operator, value, searchMode = 'cont
505
506
  clause = `${field} != :${name}`;
506
507
  parameters = { [name]: value };
507
508
  break;
509
+ /* What an empty list means, and the parentheses around OR, are decided in membershipClause alone. */
508
510
  case 'in':
509
- clause = `${field} IN (:...${name})`;
510
- parameters = { [name]: values };
511
- break;
512
511
  case 'notin':
513
- clause = `${field} NOT IN (:...${name})`;
514
- parameters = { [name]: values };
515
- break;
516
- /* OR 를 품은 절은 **반드시 스스로 괄호로 닫는다.** 이 문자열은 andWhere 로 들어가는데 TypeORM 은
517
- * 문자열 조건을 감싸주지 않는다(isolateWhereStatements 기본 false). 괄호가 없으면 실제 SQL 이
518
- * `... AND domain_id = ? AND instance_id IN (?) AND col IS NULL OR col NOT IN (?)`
519
- * 가 되고, AND 가 OR 보다 강해 `(앞의 모든 조건) OR (col NOT IN ...)` 으로 해석된다 —
520
- * **도메인·테넌트 조건이 통째로 무력화되어 남의 데이터가 섞여 나온다.**
521
- * (아래 is_not_true/is_not_false/is_empty_num_id 는 원래 괄호를 갖고 있었고, 이것만 빠져 있었다.) */
522
512
  case 'notin_with_null':
523
- clause = `(${field} IS NULL OR ${field} NOT IN (:...${name}))`;
524
- parameters = { [name]: values };
513
+ ;
514
+ ({ clause, parameters } = (0, membership_clause_js_1.membershipClause)(field, operator, values, name));
525
515
  break;
526
516
  case 'is_null':
527
517
  clause = `${field} IS NULL`;
@@ -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":";;AA0DA,sEAkMC;AA5PD,qCAA0G;AAC1G,kEAA2D;AAE3D,yEAAkH;AAElH,qEAAuE;AACvE,yDAAsE;AACtE,sDAA+C;AAU/C;;;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,OAwCnD;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,EACf,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,QAAQ,CAAA;IAE5F,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;;;;;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,KAAK,IAAI;YACP,MAAM,GAAG,GAAG,KAAK,YAAY,IAAI,GAAG,CAAA;YACpC,UAAU,GAAG,EAAE,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,CAAA;YAC/B,MAAK;QACP,KAAK,OAAO;YACV,MAAM,GAAG,GAAG,KAAK,gBAAgB,IAAI,GAAG,CAAA;YACxC,UAAU,GAAG,EAAE,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,CAAA;YAC/B,MAAK;QACP;;;;;wFAKgF;QAChF,KAAK,iBAAiB;YACpB,MAAM,GAAG,IAAI,KAAK,eAAe,KAAK,gBAAgB,IAAI,IAAI,CAAA;YAC9D,UAAU,GAAG,EAAE,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,CAAA;YAC/B,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","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'\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 case 'in':\n clause = `${field} IN (:...${name})`\n parameters = { [name]: values }\n break\n case 'notin':\n clause = `${field} NOT IN (:...${name})`\n parameters = { [name]: values }\n break\n /* OR 를 품은 절은 **반드시 스스로 괄호로 닫는다.** 이 문자열은 andWhere 로 들어가는데 TypeORM 은\n * 문자열 조건을 감싸주지 않는다(isolateWhereStatements 기본 false). 괄호가 없으면 실제 SQL 이\n * `... AND domain_id = ? AND instance_id IN (?) AND col IS NULL OR col NOT IN (?)`\n * 가 되고, AND 가 OR 보다 강해 `(앞의 모든 조건) OR (col NOT IN ...)` 으로 해석된다 —\n * **도메인·테넌트 조건이 통째로 무력화되어 남의 데이터가 섞여 나온다.**\n * (아래 is_not_true/is_not_false/is_empty_num_id 는 원래 괄호를 갖고 있었고, 이것만 빠져 있었다.) */\n case 'notin_with_null':\n clause = `(${field} IS NULL OR ${field} NOT IN (:...${name}))`\n parameters = { [name]: values }\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":"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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"]}
@@ -0,0 +1,19 @@
1
+ /**
2
+ * The clause of a list membership filter: `in`, `notin`, `notin_with_null`.
3
+ *
4
+ * An empty list has a meaning of its own and never reaches the database as `IN ()`, which postgres, mysql, mssql and
5
+ * oracle refuse as a syntax error (sqlite accepts it, so a sqlite test never sees it):
6
+ * - `in []` matches nothing -> `1 = 0`
7
+ * - `notin []` constrains nothing -> `1 = 1`
8
+ * - `notin_with_null []` constrains nothing -> `1 = 1`
9
+ *
10
+ * A clause that holds OR closes its own parentheses: callers hand the string to andWhere as it is, and TypeORM does
11
+ * not wrap string conditions (isolateWhereStatements is off by default). Without them `A AND B OR C` would drop every
12
+ * condition before it, the domain and tenant ones included.
13
+ */
14
+ export declare function membershipClause(field: string, operator: 'in' | 'notin' | 'notin_with_null', values: any[], param: string): {
15
+ clause: string;
16
+ parameters: {
17
+ [key: string]: any;
18
+ };
19
+ };
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.membershipClause = membershipClause;
4
+ /**
5
+ * The clause of a list membership filter: `in`, `notin`, `notin_with_null`.
6
+ *
7
+ * An empty list has a meaning of its own and never reaches the database as `IN ()`, which postgres, mysql, mssql and
8
+ * oracle refuse as a syntax error (sqlite accepts it, so a sqlite test never sees it):
9
+ * - `in []` matches nothing -> `1 = 0`
10
+ * - `notin []` constrains nothing -> `1 = 1`
11
+ * - `notin_with_null []` constrains nothing -> `1 = 1`
12
+ *
13
+ * A clause that holds OR closes its own parentheses: callers hand the string to andWhere as it is, and TypeORM does
14
+ * not wrap string conditions (isolateWhereStatements is off by default). Without them `A AND B OR C` would drop every
15
+ * condition before it, the domain and tenant ones included.
16
+ */
17
+ function membershipClause(field, operator, values, param) {
18
+ if (values.length === 0) {
19
+ return { clause: operator === 'in' ? '1 = 0' : '1 = 1', parameters: {} };
20
+ }
21
+ switch (operator) {
22
+ case 'in':
23
+ return { clause: `${field} IN (:...${param})`, parameters: { [param]: values } };
24
+ case 'notin':
25
+ return { clause: `${field} NOT IN (:...${param})`, parameters: { [param]: values } };
26
+ case 'notin_with_null':
27
+ return { clause: `(${field} IS NULL OR ${field} NOT IN (:...${param}))`, parameters: { [param]: values } };
28
+ }
29
+ }
30
+ //# sourceMappingURL=membership-clause.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"membership-clause.js","sourceRoot":"","sources":["../../server/utils/membership-clause.ts"],"names":[],"mappings":";;AAaA,4CAkBC;AA/BD;;;;;;;;;;;;GAYG;AACH,SAAgB,gBAAgB,CAC9B,KAAa,EACb,QAA4C,EAC5C,MAAa,EACb,KAAa;IAEb,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACxB,OAAO,EAAE,MAAM,EAAE,QAAQ,KAAK,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,EAAE,UAAU,EAAE,EAAE,EAAE,CAAA;IAC1E,CAAC;IAED,QAAQ,QAAQ,EAAE,CAAC;QACjB,KAAK,IAAI;YACP,OAAO,EAAE,MAAM,EAAE,GAAG,KAAK,YAAY,KAAK,GAAG,EAAE,UAAU,EAAE,EAAE,CAAC,KAAK,CAAC,EAAE,MAAM,EAAE,EAAE,CAAA;QAClF,KAAK,OAAO;YACV,OAAO,EAAE,MAAM,EAAE,GAAG,KAAK,gBAAgB,KAAK,GAAG,EAAE,UAAU,EAAE,EAAE,CAAC,KAAK,CAAC,EAAE,MAAM,EAAE,EAAE,CAAA;QACtF,KAAK,iBAAiB;YACpB,OAAO,EAAE,MAAM,EAAE,IAAI,KAAK,eAAe,KAAK,gBAAgB,KAAK,IAAI,EAAE,UAAU,EAAE,EAAE,CAAC,KAAK,CAAC,EAAE,MAAM,EAAE,EAAE,CAAA;IAC9G,CAAC;AACH,CAAC","sourcesContent":["/**\n * The clause of a list membership filter: `in`, `notin`, `notin_with_null`.\n *\n * An empty list has a meaning of its own and never reaches the database as `IN ()`, which postgres, mysql, mssql and\n * oracle refuse as a syntax error (sqlite accepts it, so a sqlite test never sees it):\n * - `in []` matches nothing -> `1 = 0`\n * - `notin []` constrains nothing -> `1 = 1`\n * - `notin_with_null []` constrains nothing -> `1 = 1`\n *\n * A clause that holds OR closes its own parentheses: callers hand the string to andWhere as it is, and TypeORM does\n * not wrap string conditions (isolateWhereStatements is off by default). Without them `A AND B OR C` would drop every\n * condition before it, the domain and tenant ones included.\n */\nexport function membershipClause(\n field: string,\n operator: 'in' | 'notin' | 'notin_with_null',\n values: any[],\n param: string\n): { clause: string; parameters: { [key: string]: any } } {\n if (values.length === 0) {\n return { clause: operator === 'in' ? '1 = 0' : '1 = 1', parameters: {} }\n }\n\n switch (operator) {\n case 'in':\n return { clause: `${field} IN (:...${param})`, parameters: { [param]: values } }\n case 'notin':\n return { clause: `${field} NOT IN (:...${param})`, parameters: { [param]: values } }\n case 'notin_with_null':\n return { clause: `(${field} IS NULL OR ${field} NOT IN (:...${param}))`, parameters: { [param]: values } }\n }\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@things-factory/shell",
3
- "version": "10.1.205",
3
+ "version": "10.1.206",
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": "7311492aa5eaa00993c0325053f5f7ae8b1fb024"
145
+ "gitHead": "18588a058a4a6cb21c379b2da5517e077c6442ba"
146
146
  }
@@ -0,0 +1,117 @@
1
+ import 'reflect-metadata'
2
+ import { Column, DataSource, Entity, PrimaryColumn, Repository } from 'typeorm'
3
+
4
+ import { buildCondition } from '../server/utils/condition-builder.js'
5
+ import { getQueryBuilderFromListParams } from '../server/utils/get-query-builder-from-list-params.js'
6
+ import { ListParam } from '../server/service/common-types/list-param.js'
7
+
8
+ /**
9
+ * A list membership filter with an empty list: `in []` matches nothing, `notin []` and `notin_with_null []`
10
+ * constrain nothing.
11
+ *
12
+ * The board list once sent `state in []`. Both builders wrote it as `IN ()`, which postgres refuses as a syntax
13
+ * error, so the whole list failed in production. sqlite accepts `IN ()`, so a sqlite run alone never sees it: the
14
+ * postgres SQL is checked from the dialect without a server, and run on a real one when SHELL_PG_URL names it:
15
+ *
16
+ * docker run -d --name shell-pg -e POSTGRES_PASSWORD=shell -e POSTGRES_USER=shell -e POSTGRES_DB=shell \
17
+ * -p 55434:5432 postgres:14-alpine
18
+ * SHELL_PG_URL=postgres://shell:shell@localhost:55434/shell npx jest tests/empty-membership.test.ts
19
+ */
20
+
21
+ @Entity()
22
+ class Gadget {
23
+ @PrimaryColumn({ type: 'integer' })
24
+ id!: number
25
+
26
+ @Column({ type: 'text', nullable: true })
27
+ state!: string | null
28
+ }
29
+
30
+ const ROWS = [
31
+ { id: 1, state: 'draft' },
32
+ { id: 2, state: 'released' },
33
+ { id: 3, state: null }
34
+ ]
35
+
36
+ const CASES: [operator: string, expected: number[]][] = [
37
+ ['in', []],
38
+ ['notin', [1, 2, 3]],
39
+ ['notin_with_null', [1, 2, 3]]
40
+ ]
41
+
42
+ function listParamsQuery(repository: Repository<Gadget>, operator: string, value: any) {
43
+ const params = { filters: [{ name: 'state', operator, value }], pagination: { page: 1, limit: 10 } } as unknown as ListParam
44
+ return getQueryBuilderFromListParams({ repository, params })
45
+ }
46
+
47
+ function conditionQuery(repository: Repository<Gadget>, operator: string, value: any) {
48
+ const queryBuilder = repository.createQueryBuilder('gadget')
49
+ const { clause, parameters } = buildCondition('gadget', 'state', operator, value, false, 0) as any
50
+ return queryBuilder.andWhere(clause, parameters)
51
+ }
52
+
53
+ const BUILDERS = [
54
+ ['getQueryBuilderFromListParams', listParamsQuery],
55
+ ['buildCondition', conditionQuery]
56
+ ] as const
57
+
58
+ async function idsOf(query: ReturnType<typeof conditionQuery>): Promise<number[]> {
59
+ return (await query.getMany()).map(row => row.id).sort((a, b) => a - b)
60
+ }
61
+
62
+ /** What each builder reads from the same rows on a database that runs the SQL. */
63
+ function readsOn(describeOn: jest.Describe, title: string, options: () => any) {
64
+ describeOn(title, () => {
65
+ let dataSource: DataSource
66
+ let repository: Repository<Gadget>
67
+
68
+ beforeAll(async () => {
69
+ dataSource = new DataSource({ ...options(), synchronize: true, entities: [Gadget] })
70
+ await dataSource.initialize()
71
+ repository = dataSource.getRepository(Gadget)
72
+ await repository.save(ROWS)
73
+ })
74
+
75
+ afterAll(async () => {
76
+ await dataSource?.destroy()
77
+ })
78
+
79
+ describe.each(BUILDERS)('%s', (_, query) => {
80
+ it.each(CASES)('%s [] reads %j', async (operator, expected) => {
81
+ expect(await idsOf(query(repository, operator, []))).toEqual(expected)
82
+ })
83
+
84
+ it('a list with a choice still filters by it', async () => {
85
+ expect(await idsOf(query(repository, 'in', ['draft']))).toEqual([1])
86
+ expect(await idsOf(query(repository, 'notin', ['draft']))).toEqual([2])
87
+ expect(await idsOf(query(repository, 'notin_with_null', ['draft']))).toEqual([2, 3])
88
+ })
89
+ })
90
+ })
91
+ }
92
+
93
+ readsOn(describe, 'an empty membership list on sqlite', () => ({ type: 'better-sqlite3', database: ':memory:' }))
94
+
95
+ describe('an empty membership list in postgres SQL, without a server', () => {
96
+ let repository: Repository<Gadget>
97
+
98
+ beforeAll(async () => {
99
+ const dataSource = new DataSource({ type: 'postgres', host: 'localhost', database: 'unused', entities: [Gadget] })
100
+ await (dataSource as any).buildMetadatas()
101
+ repository = dataSource.getRepository(Gadget)
102
+ })
103
+
104
+ describe.each(BUILDERS)('%s', (_, query) => {
105
+ it.each(CASES)('%s [] writes no empty IN list', operator => {
106
+ const [sql] = query(repository, operator, []).getQueryAndParameters()
107
+ expect(sql).not.toMatch(/IN \(\s*\)/)
108
+ })
109
+ })
110
+ })
111
+
112
+ const url = process.env.SHELL_PG_URL
113
+ readsOn(url ? describe : describe.skip, 'an empty membership list on postgres', () => ({
114
+ type: 'postgres',
115
+ url,
116
+ dropSchema: true
117
+ }))