@things-factory/shell 10.0.0-zeta.8 → 10.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/client/entries/public/home.js +1 -1
- package/client/themes/app-theme.css +8 -24
- package/client/themes/calendar-theme.css +0 -7
- package/client/themes/grist-theme.css +13 -37
- package/client/themes/layout-theme.css +3 -23
- package/client/themes/report-theme.css +0 -9
- package/client/themes/theme-base.css +30 -0
- package/client/themes/tooltip-theme.css +0 -4
- package/dist-server/bootstrap-hooks.d.ts +51 -0
- package/dist-server/bootstrap-hooks.js +39 -0
- package/dist-server/bootstrap-hooks.js.map +1 -0
- package/dist-server/index.d.ts +1 -0
- package/dist-server/index.js +1 -0
- package/dist-server/index.js.map +1 -1
- package/dist-server/initializers/database.js +4 -1
- package/dist-server/initializers/database.js.map +1 -1
- package/dist-server/migrations/1785369600000-IndexDomainOwner.d.ts +5 -0
- package/dist-server/migrations/1785369600000-IndexDomainOwner.js +49 -0
- package/dist-server/migrations/1785369600000-IndexDomainOwner.js.map +1 -0
- package/dist-server/server-dev.js +13 -10
- package/dist-server/server-dev.js.map +1 -1
- package/dist-server/server.js +12 -8
- package/dist-server/server.js.map +1 -1
- package/dist-server/service/domain/domain-mutation.js +3 -2
- package/dist-server/service/domain/domain-mutation.js.map +1 -1
- package/dist-server/service/domain/domain.js +12 -1
- package/dist-server/service/domain/domain.js.map +1 -1
- package/dist-server/service/env-var/env-var-mutation.js +6 -1
- package/dist-server/service/env-var/env-var-mutation.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/dist-server/typeorm/get-data-encryption-key.js +11 -1
- package/dist-server/typeorm/get-data-encryption-key.js.map +1 -1
- package/dist-server/utils/apply-domain-filter.d.ts +17 -0
- package/dist-server/utils/apply-domain-filter.js +30 -0
- package/dist-server/utils/apply-domain-filter.js.map +1 -0
- package/dist-server/utils/condition-builder.js +15 -7
- package/dist-server/utils/condition-builder.js.map +1 -1
- package/dist-server/utils/get-query-builder-from-list-params.d.ts +39 -9
- package/dist-server/utils/get-query-builder-from-list-params.js +83 -40
- package/dist-server/utils/get-query-builder-from-list-params.js.map +1 -1
- package/dist-server/utils/index.d.ts +1 -0
- package/dist-server/utils/index.js +1 -0
- package/dist-server/utils/index.js.map +1 -1
- package/dist-server/utils/list-params-converter.js +8 -6
- package/dist-server/utils/list-params-converter.js.map +1 -1
- package/dist-server/utils/list-query-builder.js +2 -14
- package/dist-server/utils/list-query-builder.js.map +1 -1
- package/jest.config.js +5 -0
- package/package.json +12 -12
- package/tests/domain-owner-index.test.ts +121 -0
- package/tests/env-var-tenant-isolation.test.ts +118 -0
- package/tests/get-query-builder-features.test.ts +229 -0
- package/tests/list-param-operators.test.ts +87 -0
- package/tests/manifest-contract.test.ts +38 -0
- package/client/store.ts +0 -15
|
@@ -2,14 +2,24 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.dataEncryptionKey = void 0;
|
|
4
4
|
const env_1 = require("@things-factory/env");
|
|
5
|
+
/* shell 이 dev 편의를 위해 제공하는 기본 키. 프로덕션에서는 앱이 자기 config 로
|
|
6
|
+
반드시 override 하는 것이 정석/권장이다. */
|
|
7
|
+
const DEFAULT_DATA_ENCRYPTION_KEY = 'V6g5oHJZb7KcYzIyL6cM95XvIDouon5b';
|
|
5
8
|
var _DATA_ENCRYPTION_KEY = env_1.config.get('dataEncryptionKey');
|
|
6
9
|
if (!_DATA_ENCRYPTION_KEY) {
|
|
7
10
|
if (process.env.NODE_ENV == 'production') {
|
|
8
11
|
throw new TypeError('dataEncryptionKey not configured.');
|
|
9
12
|
}
|
|
10
13
|
else {
|
|
11
|
-
_DATA_ENCRYPTION_KEY =
|
|
14
|
+
_DATA_ENCRYPTION_KEY = DEFAULT_DATA_ENCRYPTION_KEY;
|
|
12
15
|
}
|
|
13
16
|
}
|
|
17
|
+
/* 프로덕션인데 앱이 override 하지 않아 레포에 공개된 기본 키가 그대로 쓰이는 경우:
|
|
18
|
+
암호화가 사실상 무의미하므로 강한 보안 경고를 남긴다(부팅·데이터는 건드리지 않음). */
|
|
19
|
+
if (process.env.NODE_ENV == 'production' && _DATA_ENCRYPTION_KEY == DEFAULT_DATA_ENCRYPTION_KEY) {
|
|
20
|
+
env_1.logger.warn('[SECURITY] dataEncryptionKey is using the built-in default committed in the repository, ' +
|
|
21
|
+
'which provides NO real protection for encrypted data. ' +
|
|
22
|
+
'Override `dataEncryptionKey` in your application config for production.');
|
|
23
|
+
}
|
|
14
24
|
exports.dataEncryptionKey = _DATA_ENCRYPTION_KEY;
|
|
15
25
|
//# sourceMappingURL=get-data-encryption-key.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"get-data-encryption-key.js","sourceRoot":"","sources":["../../server/typeorm/get-data-encryption-key.ts"],"names":[],"mappings":";;;AAAA,
|
|
1
|
+
{"version":3,"file":"get-data-encryption-key.js","sourceRoot":"","sources":["../../server/typeorm/get-data-encryption-key.ts"],"names":[],"mappings":";;;AAAA,6CAAoD;AAEpD;iCACiC;AACjC,MAAM,2BAA2B,GAAG,kCAAkC,CAAA;AAEtE,IAAI,oBAAoB,GAAG,YAAM,CAAC,GAAG,CAAC,mBAAmB,CAAC,CAAA;AAE1D,IAAI,CAAC,oBAAoB,EAAE,CAAC;IAC1B,IAAI,OAAO,CAAC,GAAG,CAAC,QAAQ,IAAI,YAAY,EAAE,CAAC;QACzC,MAAM,IAAI,SAAS,CAAC,mCAAmC,CAAC,CAAA;IAC1D,CAAC;SAAM,CAAC;QACN,oBAAoB,GAAG,2BAA2B,CAAA;IACpD,CAAC;AACH,CAAC;AAED;qDACqD;AACrD,IAAI,OAAO,CAAC,GAAG,CAAC,QAAQ,IAAI,YAAY,IAAI,oBAAoB,IAAI,2BAA2B,EAAE,CAAC;IAChG,YAAM,CAAC,IAAI,CACT,0FAA0F;QACxF,wDAAwD;QACxD,yEAAyE,CAC5E,CAAA;AACH,CAAC;AAEY,QAAA,iBAAiB,GAAG,oBAAoB,CAAA","sourcesContent":["import { config, logger } from '@things-factory/env'\n\n/* shell 이 dev 편의를 위해 제공하는 기본 키. 프로덕션에서는 앱이 자기 config 로\n 반드시 override 하는 것이 정석/권장이다. */\nconst DEFAULT_DATA_ENCRYPTION_KEY = 'V6g5oHJZb7KcYzIyL6cM95XvIDouon5b'\n\nvar _DATA_ENCRYPTION_KEY = config.get('dataEncryptionKey')\n\nif (!_DATA_ENCRYPTION_KEY) {\n if (process.env.NODE_ENV == 'production') {\n throw new TypeError('dataEncryptionKey not configured.')\n } else {\n _DATA_ENCRYPTION_KEY = DEFAULT_DATA_ENCRYPTION_KEY\n }\n}\n\n/* 프로덕션인데 앱이 override 하지 않아 레포에 공개된 기본 키가 그대로 쓰이는 경우:\n 암호화가 사실상 무의미하므로 강한 보안 경고를 남긴다(부팅·데이터는 건드리지 않음). */\nif (process.env.NODE_ENV == 'production' && _DATA_ENCRYPTION_KEY == DEFAULT_DATA_ENCRYPTION_KEY) {\n logger.warn(\n '[SECURITY] dataEncryptionKey is using the built-in default committed in the repository, ' +\n 'which provides NO real protection for encrypted data. ' +\n 'Override `dataEncryptionKey` in your application config for production.'\n )\n}\n\nexport const dataEncryptionKey = _DATA_ENCRYPTION_KEY\n"]}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { WhereExpressionBuilder } from 'typeorm';
|
|
2
|
+
import { InheritedValueType } from '../service/common-types/list-param.js';
|
|
3
|
+
/**
|
|
4
|
+
* 멀티테넌트 도메인 상속 필터를 쿼리에 적용한다.
|
|
5
|
+
*
|
|
6
|
+
* `getQueryBuilderFromListParams` 와 (deprecated) `buildQuery` 양쪽에 동일하게 중복돼 있던
|
|
7
|
+
* 도메인 필터 로직의 단일 소스. 도메인 필터는 테넌트 격리(보안)에 직결되므로 한 곳에서만 정의한다.
|
|
8
|
+
*
|
|
9
|
+
* @param qb 조건을 추가할 대상(WhereExpressionBuilder / SelectQueryBuilder)
|
|
10
|
+
* @param alias 엔티티 alias
|
|
11
|
+
* @param domain 현재 도메인 (id, parentId)
|
|
12
|
+
* @param inherited 상속 방식: None(자기 도메인) / Include(자기+부모) / Only(부모만)
|
|
13
|
+
*/
|
|
14
|
+
export declare function applyDomainInheritanceFilter(qb: WhereExpressionBuilder, alias: string, domain: {
|
|
15
|
+
id: string;
|
|
16
|
+
parentId?: string | null;
|
|
17
|
+
}, inherited?: InheritedValueType): void;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.applyDomainInheritanceFilter = applyDomainInheritanceFilter;
|
|
4
|
+
const list_param_js_1 = require("../service/common-types/list-param.js");
|
|
5
|
+
/**
|
|
6
|
+
* 멀티테넌트 도메인 상속 필터를 쿼리에 적용한다.
|
|
7
|
+
*
|
|
8
|
+
* `getQueryBuilderFromListParams` 와 (deprecated) `buildQuery` 양쪽에 동일하게 중복돼 있던
|
|
9
|
+
* 도메인 필터 로직의 단일 소스. 도메인 필터는 테넌트 격리(보안)에 직결되므로 한 곳에서만 정의한다.
|
|
10
|
+
*
|
|
11
|
+
* @param qb 조건을 추가할 대상(WhereExpressionBuilder / SelectQueryBuilder)
|
|
12
|
+
* @param alias 엔티티 alias
|
|
13
|
+
* @param domain 현재 도메인 (id, parentId)
|
|
14
|
+
* @param inherited 상속 방식: None(자기 도메인) / Include(자기+부모) / Only(부모만)
|
|
15
|
+
*/
|
|
16
|
+
function applyDomainInheritanceFilter(qb, alias, domain, inherited = list_param_js_1.InheritedValueType.None) {
|
|
17
|
+
if (!inherited || inherited === list_param_js_1.InheritedValueType.None) {
|
|
18
|
+
qb.andWhere(`${alias}.domain = :domain`, { domain: domain.id });
|
|
19
|
+
}
|
|
20
|
+
else if (inherited === list_param_js_1.InheritedValueType.Include) {
|
|
21
|
+
qb.andWhere(`${alias}.domain IN (:...domains)`, { domains: [domain.id, domain.parentId].filter(Boolean) });
|
|
22
|
+
}
|
|
23
|
+
else if (inherited === list_param_js_1.InheritedValueType.Only) {
|
|
24
|
+
qb.andWhere(`${alias}.domain = :domain`, { domain: domain.parentId || 'Impossible' });
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
qb.andWhere(`${alias}.domain = :domain`, { domain: 'Impossible' });
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=apply-domain-filter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"apply-domain-filter.js","sourceRoot":"","sources":["../../server/utils/apply-domain-filter.ts"],"names":[],"mappings":";;AAeA,oEAeC;AA5BD,yEAA0E;AAE1E;;;;;;;;;;GAUG;AACH,SAAgB,4BAA4B,CAC1C,EAA0B,EAC1B,KAAa,EACb,MAAgD,EAChD,YAAgC,kCAAkB,CAAC,IAAI;IAEvD,IAAI,CAAC,SAAS,IAAI,SAAS,KAAK,kCAAkB,CAAC,IAAI,EAAE,CAAC;QACxD,EAAE,CAAC,QAAQ,CAAC,GAAG,KAAK,mBAAmB,EAAE,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,EAAE,CAAC,CAAA;IACjE,CAAC;SAAM,IAAI,SAAS,KAAK,kCAAkB,CAAC,OAAO,EAAE,CAAC;QACpD,EAAE,CAAC,QAAQ,CAAC,GAAG,KAAK,0BAA0B,EAAE,EAAE,OAAO,EAAE,CAAC,MAAM,CAAC,EAAE,EAAE,MAAM,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC,CAAA;IAC5G,CAAC;SAAM,IAAI,SAAS,KAAK,kCAAkB,CAAC,IAAI,EAAE,CAAC;QACjD,EAAE,CAAC,QAAQ,CAAC,GAAG,KAAK,mBAAmB,EAAE,EAAE,MAAM,EAAE,MAAM,CAAC,QAAQ,IAAI,YAAY,EAAE,CAAC,CAAA;IACvF,CAAC;SAAM,CAAC;QACN,EAAE,CAAC,QAAQ,CAAC,GAAG,KAAK,mBAAmB,EAAE,EAAE,MAAM,EAAE,YAAY,EAAE,CAAC,CAAA;IACpE,CAAC;AACH,CAAC","sourcesContent":["import { WhereExpressionBuilder } from 'typeorm'\n\nimport { InheritedValueType } from '../service/common-types/list-param.js'\n\n/**\n * 멀티테넌트 도메인 상속 필터를 쿼리에 적용한다.\n *\n * `getQueryBuilderFromListParams` 와 (deprecated) `buildQuery` 양쪽에 동일하게 중복돼 있던\n * 도메인 필터 로직의 단일 소스. 도메인 필터는 테넌트 격리(보안)에 직결되므로 한 곳에서만 정의한다.\n *\n * @param qb 조건을 추가할 대상(WhereExpressionBuilder / SelectQueryBuilder)\n * @param alias 엔티티 alias\n * @param domain 현재 도메인 (id, parentId)\n * @param inherited 상속 방식: None(자기 도메인) / Include(자기+부모) / Only(부모만)\n */\nexport function applyDomainInheritanceFilter(\n qb: WhereExpressionBuilder,\n alias: string,\n domain: { id: string; parentId?: string | null },\n inherited: InheritedValueType = InheritedValueType.None\n): void {\n if (!inherited || inherited === InheritedValueType.None) {\n qb.andWhere(`${alias}.domain = :domain`, { domain: domain.id })\n } else if (inherited === InheritedValueType.Include) {\n qb.andWhere(`${alias}.domain IN (:...domains)`, { domains: [domain.id, domain.parentId].filter(Boolean) })\n } else if (inherited === InheritedValueType.Only) {\n qb.andWhere(`${alias}.domain = :domain`, { domain: domain.parentId || 'Impossible' })\n } else {\n qb.andWhere(`${alias}.domain = :domain`, { domain: 'Impossible' })\n }\n}\n"]}
|
|
@@ -95,33 +95,41 @@ const buildCondition = function (alias, fieldName, operator, value, relation, se
|
|
|
95
95
|
return {
|
|
96
96
|
clause: `${alias}.${fieldName} IS NOT NULL`
|
|
97
97
|
};
|
|
98
|
+
/* boolean 비교는 파라미터 바인딩으로 (IS TRUE/FALSE 는 MSSQL/Oracle 미지원 → 이식성 확보) */
|
|
98
99
|
case 'is_false':
|
|
99
100
|
return {
|
|
100
|
-
clause: `${alias}.${fieldName}
|
|
101
|
+
clause: `${alias}.${fieldName} = :args${seq}`,
|
|
102
|
+
parameters: { [`args${seq}`]: false }
|
|
101
103
|
};
|
|
102
104
|
case 'is_true':
|
|
103
105
|
return {
|
|
104
|
-
clause: `${alias}.${fieldName}
|
|
106
|
+
clause: `${alias}.${fieldName} = :args${seq}`,
|
|
107
|
+
parameters: { [`args${seq}`]: true }
|
|
105
108
|
};
|
|
106
109
|
case 'is_not_false':
|
|
110
|
+
/* NOT FALSE = true 또는 NULL */
|
|
107
111
|
return {
|
|
108
|
-
clause:
|
|
112
|
+
clause: `(${alias}.${fieldName} IS NULL OR ${alias}.${fieldName} = :args${seq})`,
|
|
113
|
+
parameters: { [`args${seq}`]: true }
|
|
109
114
|
};
|
|
110
115
|
case 'is_not_true':
|
|
116
|
+
/* NOT TRUE = false 또는 NULL */
|
|
111
117
|
return {
|
|
112
|
-
clause:
|
|
118
|
+
clause: `(${alias}.${fieldName} IS NULL OR ${alias}.${fieldName} = :args${seq})`,
|
|
119
|
+
parameters: { [`args${seq}`]: false }
|
|
113
120
|
};
|
|
114
121
|
case 'is_present':
|
|
115
122
|
return {
|
|
116
|
-
clause: `${alias}.${fieldName} IS
|
|
123
|
+
clause: `${alias}.${fieldName} IS NOT NULL`
|
|
117
124
|
};
|
|
118
125
|
case 'is_blank':
|
|
119
126
|
return {
|
|
120
|
-
clause: `${alias}.${fieldName} IS
|
|
127
|
+
clause: `${alias}.${fieldName} IS NULL`
|
|
121
128
|
};
|
|
122
129
|
case 'is_empty_num_id':
|
|
123
130
|
return {
|
|
124
|
-
clause:
|
|
131
|
+
clause: `(${alias}.${fieldName} IS NULL OR ${alias}.${fieldName} = :args${seq})`,
|
|
132
|
+
parameters: { [`args${seq}`]: 0 }
|
|
125
133
|
};
|
|
126
134
|
case 'between':
|
|
127
135
|
return {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"condition-builder.js","sourceRoot":"","sources":["../../server/utils/condition-builder.ts"],"names":[],"mappings":"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
|
|
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 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"]}
|
|
@@ -2,16 +2,45 @@ import { Repository, SelectQueryBuilder } from 'typeorm';
|
|
|
2
2
|
import { ListParam } from '../service/common-types/list-param.js';
|
|
3
3
|
import { Domain } from '../service/domain/domain.js';
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
5
|
+
* `search` 연산자의 매칭 방식.
|
|
6
|
+
* - contains(기본): `%v%` — 어디든 포함(유연하나 인덱스 미활용).
|
|
7
|
+
* - startsWith: `v%` — 접두 일치(인덱스 활용 가능 → 대용량에서 빠름).
|
|
8
|
+
* - exact: `v` — 정확 일치(대소문자 무시).
|
|
9
|
+
*/
|
|
10
|
+
export type SearchMode = 'contains' | 'startsWith' | 'exact';
|
|
11
|
+
/**
|
|
12
|
+
* Builds a TypeORM SelectQueryBuilder from a generic ListParam (filters, search, sortings,
|
|
13
|
+
* pagination) plus optional multi-tenant domain scoping. Returns the builder so callers can
|
|
14
|
+
* further chain joins/select/where before executing (e.g. getMany / getManyAndCount).
|
|
15
|
+
*
|
|
16
|
+
* Supported filter operators (`Filter.operator`):
|
|
17
|
+
* eq, noteq, like, nlike, i_like, i_nlike, search, lt, gt, lte, gte,
|
|
18
|
+
* in, notin, notin_with_null, between,
|
|
19
|
+
* is_null, is_not_null, is_true, is_false, is_not_true, is_not_false,
|
|
20
|
+
* is_present, is_blank, is_empty_num_id.
|
|
21
|
+
* An unknown operator throws a clear, actionable error.
|
|
22
|
+
*
|
|
23
|
+
* Behavior notes:
|
|
24
|
+
* - `search` uses case-insensitive substring match (`LOWER(col) LIKE %v%`) and requires the column
|
|
25
|
+
* to be listed in `searchables`.
|
|
26
|
+
* - `like`/`i_like` filters are IGNORED (with a console.warn) unless the column is in `searchables`,
|
|
27
|
+
* to avoid unindexed full-table scans.
|
|
28
|
+
* - Boolean operators are emitted as parameter-bound comparisons (portable across all supported DBs).
|
|
29
|
+
* - Domain scoping is applied only when `domain` is provided, honoring `params.inherited`
|
|
30
|
+
* (None = own domain, Include = own + parent, Only = parent).
|
|
31
|
+
* - Pagination uses skip/take; with joins this triggers TypeORM's two-query strategy for correctness.
|
|
32
|
+
* - Unknown filter columns / operators / relations throw a clear, actionable error (fail-fast).
|
|
6
33
|
*
|
|
7
|
-
* @param options
|
|
8
|
-
* @param options.
|
|
9
|
-
* @param options.
|
|
10
|
-
* @param [options.
|
|
11
|
-
* @param [options.
|
|
12
|
-
* @param [options.
|
|
13
|
-
*
|
|
14
|
-
* @
|
|
34
|
+
* @param options.repository TypeORM repository (or a tx-scoped repository) to build from.
|
|
35
|
+
* @param options.params ListParam: `{ filters, sortings, pagination, inherited }`.
|
|
36
|
+
* @param [options.domain] Optional current domain `{ id, parentId }` for tenant scoping.
|
|
37
|
+
* @param [options.alias] Optional entity alias (defaults to the entity name).
|
|
38
|
+
* @param [options.searchables] Columns eligible for `search`/`like` filtering.
|
|
39
|
+
* @param [options.filtersMap] Maps a filter/sort name to a column or relation column, e.g.
|
|
40
|
+
* `{ ownerName: { columnName: 'name', relationColumn: 'owner' } }`.
|
|
41
|
+
* @param [options.searchMode] `search` matching mode: 'contains'(default) / 'startsWith' / 'exact'.
|
|
42
|
+
* 'startsWith' emits `v%` (prefix) which can use an index on large tables.
|
|
43
|
+
* @returns {SelectQueryBuilder<Type>} The constructed SelectQueryBuilder (not yet executed).
|
|
15
44
|
*/
|
|
16
45
|
export declare function getQueryBuilderFromListParams<Type>(options: {
|
|
17
46
|
repository: Repository<Type>;
|
|
@@ -25,4 +54,5 @@ export declare function getQueryBuilderFromListParams<Type>(options: {
|
|
|
25
54
|
relationColumn?: string;
|
|
26
55
|
};
|
|
27
56
|
};
|
|
57
|
+
searchMode?: SearchMode;
|
|
28
58
|
}): SelectQueryBuilder<Type>;
|
|
@@ -3,20 +3,43 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.getQueryBuilderFromListParams = getQueryBuilderFromListParams;
|
|
4
4
|
const typeorm_1 = require("typeorm");
|
|
5
5
|
const list_param_js_1 = require("../service/common-types/list-param.js");
|
|
6
|
+
const apply_domain_filter_js_1 = require("./apply-domain-filter.js");
|
|
6
7
|
/**
|
|
7
|
-
*
|
|
8
|
+
* Builds a TypeORM SelectQueryBuilder from a generic ListParam (filters, search, sortings,
|
|
9
|
+
* pagination) plus optional multi-tenant domain scoping. Returns the builder so callers can
|
|
10
|
+
* further chain joins/select/where before executing (e.g. getMany / getManyAndCount).
|
|
8
11
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
12
|
+
* Supported filter operators (`Filter.operator`):
|
|
13
|
+
* eq, noteq, like, nlike, i_like, i_nlike, search, lt, gt, lte, gte,
|
|
14
|
+
* in, notin, notin_with_null, between,
|
|
15
|
+
* is_null, is_not_null, is_true, is_false, is_not_true, is_not_false,
|
|
16
|
+
* is_present, is_blank, is_empty_num_id.
|
|
17
|
+
* An unknown operator throws a clear, actionable error.
|
|
18
|
+
*
|
|
19
|
+
* Behavior notes:
|
|
20
|
+
* - `search` uses case-insensitive substring match (`LOWER(col) LIKE %v%`) and requires the column
|
|
21
|
+
* to be listed in `searchables`.
|
|
22
|
+
* - `like`/`i_like` filters are IGNORED (with a console.warn) unless the column is in `searchables`,
|
|
23
|
+
* to avoid unindexed full-table scans.
|
|
24
|
+
* - Boolean operators are emitted as parameter-bound comparisons (portable across all supported DBs).
|
|
25
|
+
* - Domain scoping is applied only when `domain` is provided, honoring `params.inherited`
|
|
26
|
+
* (None = own domain, Include = own + parent, Only = parent).
|
|
27
|
+
* - Pagination uses skip/take; with joins this triggers TypeORM's two-query strategy for correctness.
|
|
28
|
+
* - Unknown filter columns / operators / relations throw a clear, actionable error (fail-fast).
|
|
29
|
+
*
|
|
30
|
+
* @param options.repository TypeORM repository (or a tx-scoped repository) to build from.
|
|
31
|
+
* @param options.params ListParam: `{ filters, sortings, pagination, inherited }`.
|
|
32
|
+
* @param [options.domain] Optional current domain `{ id, parentId }` for tenant scoping.
|
|
33
|
+
* @param [options.alias] Optional entity alias (defaults to the entity name).
|
|
34
|
+
* @param [options.searchables] Columns eligible for `search`/`like` filtering.
|
|
35
|
+
* @param [options.filtersMap] Maps a filter/sort name to a column or relation column, e.g.
|
|
36
|
+
* `{ ownerName: { columnName: 'name', relationColumn: 'owner' } }`.
|
|
37
|
+
* @param [options.searchMode] `search` matching mode: 'contains'(default) / 'startsWith' / 'exact'.
|
|
38
|
+
* 'startsWith' emits `v%` (prefix) which can use an index on large tables.
|
|
39
|
+
* @returns {SelectQueryBuilder<Type>} The constructed SelectQueryBuilder (not yet executed).
|
|
17
40
|
*/
|
|
18
41
|
function getQueryBuilderFromListParams(options) {
|
|
19
|
-
const { repository, params, domain, alias, searchables, filtersMap = {} } = options;
|
|
42
|
+
const { repository, params, domain, alias, searchables, filtersMap = {}, searchMode = 'contains' } = options;
|
|
20
43
|
const { inherited = list_param_js_1.InheritedValueType.None } = params || {};
|
|
21
44
|
const selectQueryBuilder = repository.createQueryBuilder(alias);
|
|
22
45
|
const entityAlias = selectQueryBuilder.alias;
|
|
@@ -49,33 +72,20 @@ function getQueryBuilderFromListParams(options) {
|
|
|
49
72
|
// Apply column filters
|
|
50
73
|
if (columnFilters.length > 0) {
|
|
51
74
|
columnFilters.forEach(filter => {
|
|
52
|
-
addCondition(metadata, selectQueryBuilder, selectQueryBuilder, filter, filtersMap, true);
|
|
75
|
+
addCondition(metadata, selectQueryBuilder, selectQueryBuilder, filter, filtersMap, true, searchMode);
|
|
53
76
|
});
|
|
54
77
|
}
|
|
55
78
|
// Apply search filters
|
|
56
79
|
if (searchFilters.length > 0) {
|
|
57
80
|
selectQueryBuilder.andWhere(new typeorm_1.Brackets(qb => {
|
|
58
81
|
searchFilters.forEach(filter => {
|
|
59
|
-
addCondition(metadata, selectQueryBuilder, qb, filter, filtersMap, false);
|
|
82
|
+
addCondition(metadata, selectQueryBuilder, qb, filter, filtersMap, false, searchMode);
|
|
60
83
|
});
|
|
61
84
|
}));
|
|
62
85
|
}
|
|
63
86
|
// Apply domain filters
|
|
64
87
|
if (domain) {
|
|
65
|
-
|
|
66
|
-
selectQueryBuilder.andWhere(`${entityAlias}.domain = :domain`, { domain: domain.id });
|
|
67
|
-
}
|
|
68
|
-
else if (inherited === list_param_js_1.InheritedValueType.Include) {
|
|
69
|
-
selectQueryBuilder.andWhere(`${entityAlias}.domain IN (:...domains)`, {
|
|
70
|
-
domains: [domain.id, domain.parentId].filter(Boolean)
|
|
71
|
-
});
|
|
72
|
-
}
|
|
73
|
-
else if (inherited === list_param_js_1.InheritedValueType.Only) {
|
|
74
|
-
selectQueryBuilder.andWhere(`${entityAlias}.domain = :domain`, { domain: domain.parentId || 'Impossible' });
|
|
75
|
-
}
|
|
76
|
-
else {
|
|
77
|
-
selectQueryBuilder.andWhere(`${entityAlias}.domain = :domain`, { domain: 'Impossible' });
|
|
78
|
-
}
|
|
88
|
+
(0, apply_domain_filter_js_1.applyDomainInheritanceFilter)(selectQueryBuilder, entityAlias, domain, inherited);
|
|
79
89
|
}
|
|
80
90
|
// Apply pagination
|
|
81
91
|
addPagination(selectQueryBuilder, pagination);
|
|
@@ -112,7 +122,7 @@ function addPagination(selectQueryBuilder, pagination) {
|
|
|
112
122
|
* @param filtersMap - A mapping of filter names to column names and relation column names.
|
|
113
123
|
* @param andCondition - A flag indicating whether to use "AND" or "OR" for combining conditions.
|
|
114
124
|
*/
|
|
115
|
-
function addCondition(metadata, selectQueryBuilder, whereExpressionBuilder, filter, filtersMap = {}, andCondition) {
|
|
125
|
+
function addCondition(metadata, selectQueryBuilder, whereExpressionBuilder, filter, filtersMap = {}, andCondition, searchMode = 'contains') {
|
|
116
126
|
const { name, operator, value } = filter;
|
|
117
127
|
var entityAlias = selectQueryBuilder.alias;
|
|
118
128
|
var { relationColumn, columnName } = filtersMap[name] || {};
|
|
@@ -166,9 +176,12 @@ function addCondition(metadata, selectQueryBuilder, whereExpressionBuilder, filt
|
|
|
166
176
|
columnMeta = relationIdMeta.relation.joinColumns[0];
|
|
167
177
|
}
|
|
168
178
|
else {
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
179
|
+
/* 미지의 컬럼: 이전엔 warn 후 흘러가 columnMeta.databaseName 에서 불투명 TypeError 로 크래시했다.
|
|
180
|
+
원인을 바로 알 수 있도록 명확·실행가능한 오류를 던진다(fail-fast). */
|
|
181
|
+
const target = columnName || name;
|
|
182
|
+
const available = metadata.columns.map(column => column.propertyName).join(', ');
|
|
183
|
+
throw new Error(`[getQueryBuilderFromListParams] unknown filter column "${target}" on entity "${metadata.name}". ` +
|
|
184
|
+
`Add a filtersMap entry for "${name}", or use one of: ${available}`);
|
|
172
185
|
}
|
|
173
186
|
}
|
|
174
187
|
else {
|
|
@@ -191,7 +204,7 @@ function addCondition(metadata, selectQueryBuilder, whereExpressionBuilder, filt
|
|
|
191
204
|
const alias = relationColumnMeta ? `${name}-filter` : entityAlias;
|
|
192
205
|
/* relation columne인 경우 name을 alias로 사용한다. */
|
|
193
206
|
const field = `${alias}.${dbNameForColumn}`;
|
|
194
|
-
var { clause, parameters } = getClauseAndParameters(field, name, operator, value);
|
|
207
|
+
var { clause, parameters } = getClauseAndParameters(field, name, operator, value, searchMode);
|
|
195
208
|
if (relationColumnMeta) {
|
|
196
209
|
const { propertyName } = relationColumnMeta;
|
|
197
210
|
const property = `${entityAlias}.${propertyName}`;
|
|
@@ -272,7 +285,9 @@ function applyJoins(selectQueryBuilder, entityAlias, relationColumn, metadata, j
|
|
|
272
285
|
for (const column of columns) {
|
|
273
286
|
const relation = currentMetadata.relations.find(rel => rel.propertyName === column);
|
|
274
287
|
if (!relation) {
|
|
275
|
-
|
|
288
|
+
const available = currentMetadata.relations.map(rel => rel.propertyName).join(', ');
|
|
289
|
+
throw new Error(`[getQueryBuilderFromListParams] relation "${column}" not found on entity "${currentMetadata.name}"` +
|
|
290
|
+
` (relationColumn "${relationColumn}"). Available relations: ${available}`);
|
|
276
291
|
}
|
|
277
292
|
const nextAlias = `${currentAlias}_${relation.inverseEntityMetadata.tableName}_for_${columnName}`;
|
|
278
293
|
if (!selectQueryBuilder.expressionMap.aliases.some(alias => alias.name === nextAlias)) {
|
|
@@ -286,6 +301,18 @@ function applyJoins(selectQueryBuilder, entityAlias, relationColumn, metadata, j
|
|
|
286
301
|
}
|
|
287
302
|
return currentAlias;
|
|
288
303
|
}
|
|
304
|
+
/** search 값에 searchMode 에 맞는 와일드카드를 씌운다. */
|
|
305
|
+
function wrapSearch(value, mode) {
|
|
306
|
+
switch (mode) {
|
|
307
|
+
case 'startsWith':
|
|
308
|
+
return `${value}%`;
|
|
309
|
+
case 'exact':
|
|
310
|
+
return value;
|
|
311
|
+
case 'contains':
|
|
312
|
+
default:
|
|
313
|
+
return `%${value}%`;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
289
316
|
/**
|
|
290
317
|
* Generates the SQL clause and parameters based on the provided filter.
|
|
291
318
|
*
|
|
@@ -295,7 +322,7 @@ function applyJoins(selectQueryBuilder, entityAlias, relationColumn, metadata, j
|
|
|
295
322
|
* @param value - The value to filter with.
|
|
296
323
|
* @returns An object containing the SQL clause and the parameters.
|
|
297
324
|
*/
|
|
298
|
-
function getClauseAndParameters(field, name, operator, value) {
|
|
325
|
+
function getClauseAndParameters(field, name, operator, value, searchMode = 'contains') {
|
|
299
326
|
const values = value instanceof Array ? value : [value];
|
|
300
327
|
let clause = '';
|
|
301
328
|
let parameters = {};
|
|
@@ -309,6 +336,10 @@ function getClauseAndParameters(field, name, operator, value) {
|
|
|
309
336
|
parameters = { [name]: `%${value}%` };
|
|
310
337
|
break;
|
|
311
338
|
case 'search':
|
|
339
|
+
/* searchMode 로 매칭 방식 선택(기본 contains). startsWith 는 접두 인덱스 활용 가능. */
|
|
340
|
+
clause = `LOWER(${field}) LIKE :${name}`;
|
|
341
|
+
parameters = { [name]: wrapSearch(String(value).toLowerCase(), searchMode) };
|
|
342
|
+
break;
|
|
312
343
|
case 'i_like':
|
|
313
344
|
clause = `LOWER(${field}) LIKE :${name}`;
|
|
314
345
|
parameters = { [name]: `%${String(value).toLowerCase()}%` };
|
|
@@ -359,31 +390,43 @@ function getClauseAndParameters(field, name, operator, value) {
|
|
|
359
390
|
case 'is_not_null':
|
|
360
391
|
clause = `${field} IS NOT NULL`;
|
|
361
392
|
break;
|
|
393
|
+
/* boolean 비교는 파라미터 바인딩으로 (IS TRUE/FALSE 는 MSSQL/Oracle 미지원 → 이식성 확보) */
|
|
362
394
|
case 'is_false':
|
|
363
|
-
clause = `${field}
|
|
395
|
+
clause = `${field} = :${name}`;
|
|
396
|
+
parameters = { [name]: false };
|
|
364
397
|
break;
|
|
365
398
|
case 'is_true':
|
|
366
|
-
clause = `${field}
|
|
399
|
+
clause = `${field} = :${name}`;
|
|
400
|
+
parameters = { [name]: true };
|
|
367
401
|
break;
|
|
368
402
|
case 'is_not_false':
|
|
369
|
-
|
|
403
|
+
/* NOT FALSE = true 또는 NULL */
|
|
404
|
+
clause = `(${field} IS NULL OR ${field} = :${name})`;
|
|
405
|
+
parameters = { [name]: true };
|
|
370
406
|
break;
|
|
371
407
|
case 'is_not_true':
|
|
372
|
-
|
|
408
|
+
/* NOT TRUE = false 또는 NULL */
|
|
409
|
+
clause = `(${field} IS NULL OR ${field} = :${name})`;
|
|
410
|
+
parameters = { [name]: false };
|
|
373
411
|
break;
|
|
374
412
|
case 'is_present':
|
|
375
|
-
clause = `${field} IS
|
|
413
|
+
clause = `${field} IS NOT NULL`;
|
|
376
414
|
break;
|
|
377
415
|
case 'is_blank':
|
|
378
|
-
clause = `${field} IS
|
|
416
|
+
clause = `${field} IS NULL`;
|
|
379
417
|
break;
|
|
380
418
|
case 'is_empty_num_id':
|
|
381
|
-
clause =
|
|
419
|
+
clause = `(${field} IS NULL OR ${field} = :${name})`;
|
|
420
|
+
parameters = { [name]: 0 };
|
|
382
421
|
break;
|
|
383
422
|
case 'between':
|
|
384
423
|
clause = `${field} BETWEEN :${name}_1 AND :${name}_2`;
|
|
385
424
|
parameters = { [`${name}_1`]: values[0], [`${name}_2`]: values[1] };
|
|
386
425
|
break;
|
|
426
|
+
default:
|
|
427
|
+
/* 지원하지 않는 연산자: 이전엔 clause='' 로 andWhere('') 가 되어 malformed/무음이었다.
|
|
428
|
+
명확한 오류로 원인을 알린다(fail-fast). */
|
|
429
|
+
throw new Error(`[getQueryBuilderFromListParams] unsupported filter operator "${operator}" for field "${field}".`);
|
|
387
430
|
}
|
|
388
431
|
return { clause, parameters };
|
|
389
432
|
}
|