@dssp/dkpi 1.0.0-alpha.85 → 1.0.0-alpha.86

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.
@@ -6,6 +6,18 @@ const type_graphql_1 = require("type-graphql");
6
6
  const shell_1 = require("@things-factory/shell");
7
7
  const kpi_1 = require("@things-factory/kpi");
8
8
  const project_1 = require("@dssp/project/dist-server/service/project/project");
9
+ /** valueDate(최신) → version → updatedAt/createdAt 순으로 a 가 b 보다 더 최신이면 true */
10
+ function isMoreRecentKpiValue(a, b) {
11
+ var _a, _b, _c, _d, _e, _f;
12
+ const dateCmp = (a.valueDate || '').localeCompare(b.valueDate || '');
13
+ if (dateCmp !== 0)
14
+ return dateCmp > 0;
15
+ if (((_a = a.version) !== null && _a !== void 0 ? _a : 0) !== ((_b = b.version) !== null && _b !== void 0 ? _b : 0))
16
+ return ((_c = a.version) !== null && _c !== void 0 ? _c : 0) > ((_d = b.version) !== null && _d !== void 0 ? _d : 0);
17
+ const ta = new Date((_e = (a.updatedAt || a.createdAt)) !== null && _e !== void 0 ? _e : 0).getTime();
18
+ const tb = new Date((_f = (b.updatedAt || b.createdAt)) !== null && _f !== void 0 ? _f : 0).getTime();
19
+ return ta > tb;
20
+ }
9
21
  let KpiValueQueryForProject = class KpiValueQueryForProject {
10
22
  async kpiValues(params) {
11
23
  const queryBuilder = (0, shell_1.getQueryBuilderFromListParams)({
@@ -17,11 +29,10 @@ let KpiValueQueryForProject = class KpiValueQueryForProject {
17
29
  return { items, total };
18
30
  }
19
31
  async projectXKpiValues(projectId) {
20
- var _a, _b, _c;
32
+ var _a;
21
33
  const repository = await (0, shell_1.getRepository)(kpi_1.KpiValue);
22
34
  // history 포함 전체 fetch — 한 프로젝트의 X KPI 들이라 row 수가 적어 JS group-by 가
23
- // SQL DISTINCT ON 보다 단순. 같은 (kpiId, valueDate, kpiOrgScope) 좌표는 version 이
24
- // 큰 것만 통과.
35
+ // SQL DISTINCT ON 보다 단순.
25
36
  const allKpiValues = await repository
26
37
  .createQueryBuilder('kpiValue')
27
38
  .leftJoinAndSelect('kpiValue.kpi', 'kpi')
@@ -29,12 +40,15 @@ let KpiValueQueryForProject = class KpiValueQueryForProject {
29
40
  .andWhere('kpi.name LIKE :namePrefix', { namePrefix: 'X%' })
30
41
  .orderBy('kpi.name', 'ASC')
31
42
  .getMany();
32
- // 좌표별 latest version 선택
43
+ // (kpiId, kpiOrgScope) 별로 '가장 최근 valueDate' 1건만 선택.
44
+ // 여러 날짜·여러 데이터가 입력돼 있어도 최신 날짜의 값이 대표값이 되도록 한다.
45
+ // valueDate 가 같으면 version, 그래도 같으면 updatedAt/createdAt 으로 최신 우선
46
+ // (같은 valueDate·version 에도 복수 row 가 존재할 수 있어 단일 기준으로는 부족).
33
47
  const latestByKey = new Map();
34
48
  for (const kv of allKpiValues) {
35
- const key = `${kv.kpiId || ((_a = kv.kpi) === null || _a === void 0 ? void 0 : _a.id)}:${kv.valueDate}:${kv.kpiOrgScopeId || ''}`;
49
+ const key = `${kv.kpiId || ((_a = kv.kpi) === null || _a === void 0 ? void 0 : _a.id)}:${kv.kpiOrgScopeId || ''}`;
36
50
  const existing = latestByKey.get(key);
37
- if (!existing || ((_b = kv.version) !== null && _b !== void 0 ? _b : 0) > ((_c = existing.version) !== null && _c !== void 0 ? _c : 0)) {
51
+ if (!existing || isMoreRecentKpiValue(kv, existing)) {
38
52
  latestByKey.set(key, kv);
39
53
  }
40
54
  }
@@ -57,7 +71,7 @@ tslib_1.__decorate([
57
71
  ], KpiValueQueryForProject.prototype, "kpiValues", null);
58
72
  tslib_1.__decorate([
59
73
  (0, type_graphql_1.Directive)('@privilege(category: "kpi", privilege: "read", domainOwnerGranted: true, superUserGranted: true)'),
60
- (0, type_graphql_1.Query)(returns => [kpi_1.KpiValue], { description: 'To fetch KpiValues by project group with X prefix kpi names (latest version only)' }),
74
+ (0, type_graphql_1.Query)(returns => [kpi_1.KpiValue], { description: 'To fetch KpiValues by project group with X prefix kpi names (latest valueDate only)' }),
61
75
  tslib_1.__param(0, (0, type_graphql_1.Arg)('projectId', type => String)),
62
76
  tslib_1.__metadata("design:type", Function),
63
77
  tslib_1.__metadata("design:paramtypes", [String]),
@@ -1 +1 @@
1
- {"version":3,"file":"kpi-value-query.js","sourceRoot":"","sources":["../../../server/service/kpi-value/kpi-value-query.ts"],"names":[],"mappings":"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","sourcesContent":["import { Resolver, Query, FieldResolver, Root, Args, Arg, Ctx, Directive, Float } from 'type-graphql'\nimport { Domain, getQueryBuilderFromListParams, getRepository, ListParam } from '@things-factory/shell'\nimport { User } from '@things-factory/auth-base'\nimport { Kpi, KpiValue, KpiValueList } from '@things-factory/kpi'\nimport { Project } from '@dssp/project/dist-server/service/project/project'\n\n@Resolver(KpiValue)\nexport class KpiValueQueryForProject {\n @Directive('@privilege(category: \"kpi\", privilege: \"read\", domainOwnerGranted: true, superUserGranted: true)')\n @Query(returns => KpiValueList, { description: 'To fetch multiple KpiValues' })\n async kpiValues(@Args(type => ListParam) params: ListParam): Promise<KpiValueList> {\n const queryBuilder = getQueryBuilderFromListParams({\n params,\n repository: await getRepository(KpiValue),\n searchables: ['kpi', 'group', 'valueDate']\n })\n\n const [items, total] = await queryBuilder.getManyAndCount()\n\n return { items, total }\n }\n\n @Directive('@privilege(category: \"kpi\", privilege: \"read\", domainOwnerGranted: true, superUserGranted: true)')\n @Query(returns => [KpiValue], { description: 'To fetch KpiValues by project group with X prefix kpi names (latest version only)' })\n async projectXKpiValues(@Arg('projectId', type => String) projectId: string): Promise<KpiValue[]> {\n const repository = await getRepository(KpiValue)\n\n // history 포함 전체 fetch — 한 프로젝트의 X KPI 들이라 row 수가 적어 JS group-by 가\n // SQL DISTINCT ON 보다 단순. 같은 (kpiId, valueDate, kpiOrgScope) 좌표는 version 이\n // 큰 것만 통과.\n const allKpiValues = await repository\n .createQueryBuilder('kpiValue')\n .leftJoinAndSelect('kpiValue.kpi', 'kpi')\n .where('kpiValue.group = :projectId', { projectId })\n .andWhere('kpi.name LIKE :namePrefix', { namePrefix: 'X%' })\n .orderBy('kpi.name', 'ASC')\n .getMany()\n\n // 좌표별 latest version 선택\n const latestByKey = new Map<string, KpiValue>()\n for (const kv of allKpiValues) {\n const key = `${(kv as any).kpiId || kv.kpi?.id}:${kv.valueDate}:${(kv as any).kpiOrgScopeId || ''}`\n const existing = latestByKey.get(key)\n if (!existing || (kv.version ?? 0) > (existing.version ?? 0)) {\n latestByKey.set(key, kv)\n }\n }\n return Array.from(latestByKey.values()).sort((a, b) => (a.kpi?.name || '').localeCompare(b.kpi?.name || ''))\n }\n\n @FieldResolver(type => Project)\n async project(@Root() kpiValue: KpiValue): Promise<Project | null> {\n if (!kpiValue.group) return null\n return await getRepository(Project).findOneBy({ id: kpiValue.group })\n }\n}\n"]}
1
+ {"version":3,"file":"kpi-value-query.js","sourceRoot":"","sources":["../../../server/service/kpi-value/kpi-value-query.ts"],"names":[],"mappings":"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","sourcesContent":["import { Resolver, Query, FieldResolver, Root, Args, Arg, Ctx, Directive, Float } from 'type-graphql'\nimport { Domain, getQueryBuilderFromListParams, getRepository, ListParam } from '@things-factory/shell'\nimport { User } from '@things-factory/auth-base'\nimport { Kpi, KpiValue, KpiValueList } from '@things-factory/kpi'\nimport { Project } from '@dssp/project/dist-server/service/project/project'\n\n/** valueDate(최신) → version → updatedAt/createdAt 순으로 a 가 b 보다 더 최신이면 true */\nfunction isMoreRecentKpiValue(a: KpiValue, b: KpiValue): boolean {\n const dateCmp = (a.valueDate || '').localeCompare(b.valueDate || '')\n if (dateCmp !== 0) return dateCmp > 0\n if ((a.version ?? 0) !== (b.version ?? 0)) return (a.version ?? 0) > (b.version ?? 0)\n const ta = new Date((a.updatedAt || a.createdAt) ?? 0).getTime()\n const tb = new Date((b.updatedAt || b.createdAt) ?? 0).getTime()\n return ta > tb\n}\n\n@Resolver(KpiValue)\nexport class KpiValueQueryForProject {\n @Directive('@privilege(category: \"kpi\", privilege: \"read\", domainOwnerGranted: true, superUserGranted: true)')\n @Query(returns => KpiValueList, { description: 'To fetch multiple KpiValues' })\n async kpiValues(@Args(type => ListParam) params: ListParam): Promise<KpiValueList> {\n const queryBuilder = getQueryBuilderFromListParams({\n params,\n repository: await getRepository(KpiValue),\n searchables: ['kpi', 'group', 'valueDate']\n })\n\n const [items, total] = await queryBuilder.getManyAndCount()\n\n return { items, total }\n }\n\n @Directive('@privilege(category: \"kpi\", privilege: \"read\", domainOwnerGranted: true, superUserGranted: true)')\n @Query(returns => [KpiValue], { description: 'To fetch KpiValues by project group with X prefix kpi names (latest valueDate only)' })\n async projectXKpiValues(@Arg('projectId', type => String) projectId: string): Promise<KpiValue[]> {\n const repository = await getRepository(KpiValue)\n\n // history 포함 전체 fetch — 한 프로젝트의 X KPI 들이라 row 수가 적어 JS group-by 가\n // SQL DISTINCT ON 보다 단순.\n const allKpiValues = await repository\n .createQueryBuilder('kpiValue')\n .leftJoinAndSelect('kpiValue.kpi', 'kpi')\n .where('kpiValue.group = :projectId', { projectId })\n .andWhere('kpi.name LIKE :namePrefix', { namePrefix: 'X%' })\n .orderBy('kpi.name', 'ASC')\n .getMany()\n\n // (kpiId, kpiOrgScope) 별로 '가장 최근 valueDate' 1건만 선택.\n // 여러 날짜·여러 데이터가 입력돼 있어도 최신 날짜의 값이 대표값이 되도록 한다.\n // valueDate 가 같으면 version, 그래도 같으면 updatedAt/createdAt 으로 최신 우선\n // (같은 valueDate·version 에도 복수 row 가 존재할 수 있어 단일 기준으로는 부족).\n const latestByKey = new Map<string, KpiValue>()\n for (const kv of allKpiValues) {\n const key = `${(kv as any).kpiId || kv.kpi?.id}:${(kv as any).kpiOrgScopeId || ''}`\n const existing = latestByKey.get(key)\n if (!existing || isMoreRecentKpiValue(kv, existing)) {\n latestByKey.set(key, kv)\n }\n }\n return Array.from(latestByKey.values()).sort((a, b) => (a.kpi?.name || '').localeCompare(b.kpi?.name || ''))\n }\n\n @FieldResolver(type => Project)\n async project(@Root() kpiValue: KpiValue): Promise<Project | null> {\n if (!kpiValue.group) return null\n return await getRepository(Project).findOneBy({ id: kpiValue.group })\n }\n}\n"]}