From a44fa3b578d78fa19a32c2bb19fb18200e108457 Mon Sep 17 00:00:00 2001 From: jaham Date: Wed, 8 Nov 2023 17:27:51 +0900 Subject: [PATCH] =?UTF-8?q?feat=20:sparkles:=20daily=20materialized=20view?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit feat: :sparkles: mv_daily_scale_team_counts 추가 feat: :sparkles: mv_daily_team_close_counts 추가 feat: :sparkles: mv_daily_score_values 추가 chore: :arrow_up: mongodb client version update, TZ 시간 형태로 변경 - #54 --- app/package.json | 2 +- app/pnpm-lock.yaml | 86 +++++++++++------------ app/src/index.ts | 3 + app/src/location/location.ts | 7 +- app/src/mongodb/mongodb.ts | 2 + app/src/scaleTeam/scaleTeam.ts | 110 +++++++++++++++++++++++++++--- app/src/score/score.ts | 99 ++++++++++++++++++++++++++- app/src/team/team.ts | 121 +++++++++++++++++++++++++++------ env | 2 +- 9 files changed, 346 insertions(+), 86 deletions(-) diff --git a/app/package.json b/app/package.json index 6c4f0c6..a1561b0 100644 --- a/app/package.json +++ b/app/package.json @@ -40,7 +40,7 @@ "camelcase-keys": "^8.0.2", "dotenv": "^16.0.3", "la-seine": "^0.0.7", - "mongodb": "^5.3.0", + "mongodb": "^6.2.0", "redis": "^4.6.6", "zod": "^3.21.4" } diff --git a/app/pnpm-lock.yaml b/app/pnpm-lock.yaml index f0a383e..beb7fc6 100644 --- a/app/pnpm-lock.yaml +++ b/app/pnpm-lock.yaml @@ -18,8 +18,8 @@ dependencies: specifier: ^0.0.7 version: 0.0.7 mongodb: - specifier: ^5.3.0 - version: 5.3.0 + specifier: ^6.2.0 + version: 6.2.0 redis: specifier: ^4.6.6 version: 4.6.6 @@ -1189,6 +1189,12 @@ packages: '@jridgewell/sourcemap-codec': 1.4.15 dev: true + /@mongodb-js/saslprep@1.1.1: + resolution: {integrity: sha512-t7c5K033joZZMspnHg/gWPE4kandgc2OxE74aYOtGKfgB9VPuVJPix0H6fhmm2erj5PBJ21mqcx34lpIGtUCsQ==} + dependencies: + sparse-bitfield: 3.0.3 + dev: false + /@nodelib/fs.scandir@2.1.5: resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -1721,15 +1727,15 @@ packages: resolution: {integrity: sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==} dev: true - /@types/webidl-conversions@7.0.0: - resolution: {integrity: sha512-xTE1E+YF4aWPJJeUzaZI5DRntlkY3+BCVJi0axFptnjGmAoWxkyREIh/XMrfxVLejwQxMCfDXdICo0VLxThrog==} + /@types/webidl-conversions@7.0.3: + resolution: {integrity: sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==} dev: false /@types/whatwg-url@8.2.2: resolution: {integrity: sha512-FtQu10RWgn3D9U4aazdwIE2yzphmTJREDqNdODHrbrZmmMqI0vMheC/6NE/J1Yveaj8H+ela+YwWTjq5PGmuhA==} dependencies: '@types/node': 18.18.8 - '@types/webidl-conversions': 7.0.0 + '@types/webidl-conversions': 7.0.3 dev: false /@types/yargs-parser@21.0.0: @@ -2094,9 +2100,9 @@ packages: node-int64: 0.4.0 dev: true - /bson@5.2.0: - resolution: {integrity: sha512-HevkSpDbpUfsrHWmWiAsNavANKYIErV2ePXllp1bwq5CDreAaFVj6RVlZpJnxK4WWDCJ/5jMUpaY6G526q3Hjg==} - engines: {node: '>=14.20.1'} + /bson@6.2.0: + resolution: {integrity: sha512-ID1cI+7bazPDyL9wYy9GaQ8gEEohWvcUl/Yf0dIdutJxnmInEEyCsb4awy/OiBfall7zBA179Pahi3vCdFze3Q==} + engines: {node: '>=16.20.1'} dev: false /buffer-from@1.1.2: @@ -2806,10 +2812,6 @@ packages: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} dev: true - /ip@2.0.0: - resolution: {integrity: sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ==} - dev: false - /is-arrayish@0.2.1: resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} dev: true @@ -3483,7 +3485,6 @@ packages: resolution: {integrity: sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==} requiresBuild: true dev: false - optional: true /merge-stream@2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} @@ -3525,26 +3526,36 @@ packages: whatwg-url: 11.0.0 dev: false - /mongodb@5.3.0: - resolution: {integrity: sha512-Wy/sbahguL8c3TXQWXmuBabiLD+iVmz+tOgQf+FwkCjhUIorqbAxRbbz00g4ZoN4sXIPwpAlTANMaGRjGGTikQ==} - engines: {node: '>=14.20.1'} + /mongodb@6.2.0: + resolution: {integrity: sha512-d7OSuGjGWDZ5usZPqfvb36laQ9CPhnWkAGHT61x5P95p/8nMVeH8asloMwW6GcYFeB0Vj4CB/1wOTDG2RA9BFA==} + engines: {node: '>=16.20.1'} peerDependencies: - '@aws-sdk/credential-providers': ^3.201.0 - mongodb-client-encryption: '>=2.3.0 <3' + '@aws-sdk/credential-providers': ^3.188.0 + '@mongodb-js/zstd': ^1.1.0 + gcp-metadata: ^5.2.0 + kerberos: ^2.0.1 + mongodb-client-encryption: '>=6.0.0 <7' snappy: ^7.2.2 + socks: ^2.7.1 peerDependenciesMeta: '@aws-sdk/credential-providers': optional: true + '@mongodb-js/zstd': + optional: true + gcp-metadata: + optional: true + kerberos: + optional: true mongodb-client-encryption: optional: true snappy: optional: true + socks: + optional: true dependencies: - bson: 5.2.0 + '@mongodb-js/saslprep': 1.1.1 + bson: 6.2.0 mongodb-connection-string-url: 2.6.0 - socks: 2.7.1 - optionalDependencies: - saslprep: 1.0.3 dev: false /ms@2.1.2: @@ -3762,6 +3773,12 @@ packages: /punycode@2.3.0: resolution: {integrity: sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==} engines: {node: '>=6'} + dev: true + + /punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + dev: false /pure-rand@6.0.2: resolution: {integrity: sha512-6Yg0ekpKICSjPswYOuC5sku/TSWaRYlA0qsXqJgM/d/4pLPHPuTxK7Nbf7jFKzAeedUhR8C7K9Uv63FBsSo8xQ==} @@ -3852,15 +3869,6 @@ packages: queue-microtask: 1.2.3 dev: true - /saslprep@1.0.3: - resolution: {integrity: sha512-/MY/PEMbk2SuY5sScONwhUDsV2p77Znkb/q3nSVstq/yQzYJOH/Azh29p9oJLsl3LnQwSvZDKagDGBsBwSooag==} - engines: {node: '>=6'} - requiresBuild: true - dependencies: - sparse-bitfield: 3.0.3 - dev: false - optional: true - /semver@6.3.1: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true @@ -3907,19 +3915,6 @@ packages: engines: {node: '>=8'} dev: true - /smart-buffer@4.2.0: - resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==} - engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} - dev: false - - /socks@2.7.1: - resolution: {integrity: sha512-7maUZy1N7uo6+WVEX6psASxtNlKaNVMlGQKkG/63nEDdLOWNbiUMoLK7X4uYoLhQstau72mLgfEWcXcwsaHbYQ==} - engines: {node: '>= 10.13.0', npm: '>= 3.0.0'} - dependencies: - ip: 2.0.0 - smart-buffer: 4.2.0 - dev: false - /source-map-support@0.5.13: resolution: {integrity: sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==} dependencies: @@ -3938,7 +3933,6 @@ packages: dependencies: memory-pager: 1.5.0 dev: false - optional: true /sprintf-js@1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} @@ -4071,7 +4065,7 @@ packages: resolution: {integrity: sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==} engines: {node: '>=12'} dependencies: - punycode: 2.3.0 + punycode: 2.3.1 dev: false /ts-api-utils@1.0.3(typescript@5.2.2): diff --git a/app/src/index.ts b/app/src/index.ts index 5801891..b46e5e8 100644 --- a/app/src/index.ts +++ b/app/src/index.ts @@ -38,6 +38,9 @@ const execUpdators = async ( } }; +export const TIMEZONE = process.env.TIMEZONE; +assertEnvExist(TIMEZONE); + const main = async (): Promise => { await initSeine(); diff --git a/app/src/location/location.ts b/app/src/location/location.ts index 84de857..e656fcd 100644 --- a/app/src/location/location.ts +++ b/app/src/location/location.ts @@ -50,12 +50,14 @@ export class LocationUpdator { * E 의 경우, 접속을 종료하는 사람들이 100명을 넘을 때 마다 한번씩 요청을 더 보내야함. * 평소엔 1 ~ 2번으로 충분함. */ + @UpdateAction static async update(mongo: LambdaMongo, end: Date): Promise { + await LocationUpdator.updatePrevOngoing(mongo); + await mongo.withCollectionUpdatedAt({ end, collection: LOCATION_COLLECTION, callback: async (start, end) => { - await LocationUpdator.updatePrevOngoing(mongo); await LocationUpdator.updateOngoing(mongo, start, end); await LocationUpdator.updateEnded(mongo, start, end); await LocationUpdator.updateDailyLogtime(mongo, end); @@ -63,7 +65,6 @@ export class LocationUpdator { }); } - @UpdateAction @LogAsyncEstimatedTime private static async updateOngoing( mongo: LambdaMongo, @@ -93,7 +94,6 @@ export class LocationUpdator { return parseLocations(locationDtos); } - @UpdateAction @LogAsyncEstimatedTime private static async updateEnded( mongo: LambdaMongo, @@ -128,7 +128,6 @@ export class LocationUpdator { * location api 에 버그가 생기면 end_at 이 null 인 상태로 존재하기 때문에, * 기존에 있던 location 들의 점검이 필요함. */ - @UpdateAction @LogAsyncEstimatedTime private static async updatePrevOngoing(mongo: LambdaMongo): Promise { const prevOngoingIds = await mongo diff --git a/app/src/mongodb/mongodb.ts b/app/src/mongodb/mongodb.ts index 0b7a4fd..575b4c9 100644 --- a/app/src/mongodb/mongodb.ts +++ b/app/src/mongodb/mongodb.ts @@ -11,6 +11,7 @@ import type { PROJECT_SESSIONS_SKILL_COLLECTION } from '#lambda/projectSessionsS import type { PROJECTS_USER_COLLECTION } from '#lambda/projectsUser/projectsUser.js'; import type { QUESTS_USER_COLLECTION } from '#lambda/questsUser/questsUser.js'; import type { SCALE_TEAM_COLLECTION } from '#lambda/scaleTeam/scaleTeam.js'; +import type { SCORE_COLLECTION } from '#lambda/score/score.js'; import type { SKILL_COLLECTION } from '#lambda/skill/skill.js'; import type { TEAM_COLLECTION } from '#lambda/team/team.js'; import type { USER_COLLECTION } from '#lambda/user/user.js'; @@ -44,6 +45,7 @@ type LogUpdatedAt = | typeof COALITIONS_USER_COLLECTION | typeof PROJECT_SESSION_COLLECTION | typeof SKILL_COLLECTION + | typeof SCORE_COLLECTION | typeof PROJECT_SESSIONS_SKILL_COLLECTION | typeof USER_COLLECTION; diff --git a/app/src/scaleTeam/scaleTeam.ts b/app/src/scaleTeam/scaleTeam.ts index 5176b52..11dd1ea 100644 --- a/app/src/scaleTeam/scaleTeam.ts +++ b/app/src/scaleTeam/scaleTeam.ts @@ -1,3 +1,4 @@ +import { TIMEZONE } from '#lambda/index.js'; import { LambdaMongo } from '#lambda/mongodb/mongodb.js'; import { fetchAllPages } from '#lambda/request/fetchAllPages.js'; import { @@ -12,6 +13,7 @@ import { } from '#lambda/util/decorator.js'; export const SCALE_TEAM_COLLECTION = 'scale_teams'; +const DAILY_USER_SCALE_TEAM_COUNTS_VIEW = 'mv_daily_user_scale_team_counts'; // eslint-disable-next-line export class ScaleTeamUpdator { @@ -26,25 +28,31 @@ export class ScaleTeamUpdator { * * 끝난 평가가 100개를 넘을 때 마다 요청을 한번씩 더 보내야 함. */ + @UpdateAction static async update(mongo: LambdaMongo, end: Date): Promise { - await ScaleTeamUpdator.updateFilled(mongo, end); + await mongo.withCollectionUpdatedAt({ + end, + collection: SCALE_TEAM_COLLECTION, + callback: async (start, end) => { + await ScaleTeamUpdator.updateFilled(mongo, start, end); + await ScaleTeamUpdator.updateDailyScaleTeamCountsView( + mongo, + start, + end, + ); + }, + }); } - @UpdateAction @LogAsyncEstimatedTime private static async updateFilled( mongo: LambdaMongo, + start: Date, end: Date, ): Promise { - await mongo.withCollectionUpdatedAt({ - end, - collection: SCALE_TEAM_COLLECTION, - callback: async (start, end) => { - const filled = await ScaleTeamUpdator.fetchFilled(start, end); + const filled = await ScaleTeamUpdator.fetchFilled(start, end); - await mongo.upsertManyById(SCALE_TEAM_COLLECTION, filled); - }, - }); + await mongo.upsertManyById(SCALE_TEAM_COLLECTION, filled); } @FetchApiAction @@ -58,4 +66,86 @@ export class ScaleTeamUpdator { return parseScaleTeams(scaleTeamDtos); } + + @LogAsyncEstimatedTime + private static async updateDailyScaleTeamCountsView( + mongo: LambdaMongo, + start: Date, + end: Date, + ): Promise { + await mongo + .db() + .collection(SCALE_TEAM_COLLECTION) + .aggregate([ + { + $match: { + filledAt: { $gte: start, $lt: end }, + }, + }, + { + $group: { + _id: { + date: { + $dateFromParts: { + year: { + $year: { + date: '$filledAt', + timezone: TIMEZONE, + }, + }, + month: { + $month: { + date: '$filledAt', + timezone: TIMEZONE, + }, + }, + day: { + $dayOfMonth: { + date: '$filledAt', + timezone: TIMEZONE, + }, + }, + timezone: TIMEZONE, + }, + }, + userId: '$corrector.id', + }, + count: { + $count: {}, + }, + }, + }, + { + $sort: { + _id: 1, + userId: 1, + }, + }, + { + $project: { + _id: 0, + date: '$_id.date', + userId: '$_id.userId', + count: 1, + }, + }, + { + $merge: { + into: DAILY_USER_SCALE_TEAM_COUNTS_VIEW, + on: ['date', 'userId'], + whenMatched: [ + { + $set: { + count: { + $sum: ['$count', '$$new.count'], + }, + }, + }, + ], + whenNotMatched: 'insert', + }, + }, + ]) + .toArray(); + } } diff --git a/app/src/score/score.ts b/app/src/score/score.ts index 3b40f0a..bb9fd4f 100644 --- a/app/src/score/score.ts +++ b/app/src/score/score.ts @@ -1,9 +1,10 @@ import { SEOUL_COALITION_IDS } from '#lambda/coalition/api/coalition.api.js'; +import { TIMEZONE } from '#lambda/index.js'; import { LambdaMongo } from '#lambda/mongodb/mongodb.js'; import { fetchAllPages } from '#lambda/request/fetchAllPages.js'; import { - SCORE_EDGE_CASE, SCORE_API, + SCORE_EDGE_CASE, Score, parseScores, } from '#lambda/score/api/score.api.js'; @@ -14,6 +15,7 @@ import { } from '#lambda/util/decorator.js'; export const SCORE_COLLECTION = 'scores'; +const DAILY_SCORE_VALUES_VIEW = 'mv_daily_score_values'; type CountByCoalitionId = { coalitionId: number; @@ -34,11 +36,18 @@ export class ScoreUpdator { * coalition 별로 한번씩 요청을 보내보아야 하지만, 체육대회 등의 행사가 있어 모든 coalition user * 에게 score 가 지급되는게 아니면, 각 한번으로 충분함. */ + @UpdateAction static async update(mongo: LambdaMongo, end: Date): Promise { - await ScoreUpdator.updateByCoalition(mongo, end); + await mongo.withCollectionUpdatedAt({ + end, + collection: SCORE_COLLECTION, + callback: async (start, end) => { + await ScoreUpdator.updateByCoalition(mongo, end); + await ScoreUpdator.updateDailyScoreValuesView(mongo, start, end); + }, + }); } - @UpdateAction @LogAsyncEstimatedTime private static async updateByCoalition( mongo: LambdaMongo, @@ -110,4 +119,88 @@ export class ScoreUpdator { score.createdAt.getTime() < end.getTime(), ); } + + @LogAsyncEstimatedTime + private static async updateDailyScoreValuesView( + mongo: LambdaMongo, + start: Date, + end: Date, + ): Promise { + await mongo + .db() + .collection(SCORE_COLLECTION) + .aggregate([ + { + $match: { + createdAt: { + $gte: start, + $lt: end, + }, + coalitionsUserId: { $ne: null }, + }, + }, + { + $group: { + _id: { + coalitionId: '$coalitionId', + date: { + $dateFromParts: { + year: { + $year: { + date: '$createdAt', + timezone: TIMEZONE, + }, + }, + month: { + $month: { + date: '$createdAt', + timezone: TIMEZONE, + }, + }, + day: { + $dayOfMonth: { + date: '$createdAt', + timezone: TIMEZONE, + }, + }, + timezone: TIMEZONE, + }, + }, + }, + value: { $sum: '$value' }, + }, + }, + { + $project: { + _id: 0, + coalitionId: '$_id.coalitionId', + date: '$_id.date', + value: 1, + }, + }, + { + $sort: { + date: 1, + coalitionId: 1, + }, + }, + { + $merge: { + into: DAILY_SCORE_VALUES_VIEW, + on: ['date', 'coalitionId'], + whenMatched: [ + { + $set: { + value: { + $sum: ['$value', '$$new.value'], + }, + }, + }, + ], + whenNotMatched: 'insert', + }, + }, + ]) + .toArray(); + } } diff --git a/app/src/team/team.ts b/app/src/team/team.ts index e2aa30f..916e193 100644 --- a/app/src/team/team.ts +++ b/app/src/team/team.ts @@ -1,6 +1,7 @@ import { CampusUserUpdator } from '#lambda/campusUser/campusUser.js'; import { HYULIM } from '#lambda/cursusUser/api/cursusUser.api.js'; import { getStudentIds } from '#lambda/cursusUser/cursusUser.js'; +import { TIMEZONE } from '#lambda/index.js'; import { LambdaMongo } from '#lambda/mongodb/mongodb.js'; import { ProjectsUserUpdator } from '#lambda/projectsUser/projectsUser.js'; import { fetchAllPages } from '#lambda/request/fetchAllPages.js'; @@ -21,6 +22,7 @@ import { import { hasId } from '#lambda/util/hasId.js'; export const TEAM_COLLECTION = 'teams'; +const DAILY_TEAM_CLOSE_COUNTS_VIEW = 'mv_daily_team_close_counts'; // eslint-disable-next-line export class TeamUpdator { @@ -43,38 +45,39 @@ export class TeamUpdator { * * G: waiting_for_correction 인 team 이 증가할수록 선형적으로 증가함. */ + @UpdateAction static async update(mongo: LambdaMongo, end: Date): Promise { - await TeamUpdator.updateUpdated(mongo, end); + await mongo.withCollectionUpdatedAt({ + end, + collection: TEAM_COLLECTION, + callback: async (start, end) => { + await TeamUpdator.updateUpdated(mongo, start, end); + await TeamUpdator.updateDailyTeamCloseCountsView(mongo, start, end); + }, + }); await TeamUpdator.deleteUnregistered(mongo); await TeamUpdator.updateGiveUps(mongo); } - @UpdateAction @LogAsyncEstimatedTime private static async updateUpdated( mongo: LambdaMongo, + start: Date, end: Date, ): Promise { - await mongo.withCollectionUpdatedAt({ - end, - collection: TEAM_COLLECTION, - callback: async (start, end) => { - const studentIds = await getStudentIds(mongo); - const transferIds = CampusUserUpdator.getTransferIds(); - - const updated = await TeamUpdator.fetchUpdated(start, end).then( - (teams) => - teams.filter( - ({ users }) => - users.find((user) => hasId([...studentIds, HYULIM], user.id)) && - users.find((user) => hasId(transferIds, user.id)) === undefined, - ), - ); - - await mongo.upsertManyById(TEAM_COLLECTION, updated); - }, - }); + const studentIds = await getStudentIds(mongo); + const transferIds = CampusUserUpdator.getTransferIds(); + + const updated = await TeamUpdator.fetchUpdated(start, end).then((teams) => + teams.filter( + ({ users }) => + users.find((user) => hasId([...studentIds, HYULIM], user.id)) && + users.find((user) => hasId(transferIds, user.id)) === undefined, + ), + ); + + await mongo.upsertManyById(TEAM_COLLECTION, updated); } @FetchApiAction @@ -139,4 +142,80 @@ export class TeamUpdator { return parseTeams(teamDtos); } + + @LogAsyncEstimatedTime + private static async updateDailyTeamCloseCountsView( + mongo: LambdaMongo, + start: Date, + end: Date, + ): Promise { + await mongo + .db() + .collection(TEAM_COLLECTION) + .aggregate([ + { + $match: { + closedAt: { $gte: start, $lt: end }, + projectId: { $nin: [1320, 1321, 1322, 1323, 1324] }, + }, + }, + { + $group: { + _id: { + $dateFromParts: { + year: { + $year: { + date: '$closedAt', + timezone: TIMEZONE, + }, + }, + month: { + $month: { + date: '$closedAt', + timezone: TIMEZONE, + }, + }, + day: { + $dayOfMonth: { + date: '$closedAt', + timezone: TIMEZONE, + }, + }, + timezone: TIMEZONE, + }, + }, + count: { + $count: {}, + }, + }, + }, + { + $sort: { _id: 1 }, + }, + { + $project: { + _id: 0, + date: '$_id', + count: 1, + }, + }, + { + $merge: { + into: DAILY_TEAM_CLOSE_COUNTS_VIEW, + on: 'date', + whenMatched: [ + { + $set: { + count: { + $sum: ['$count', '$$new.count'], + }, + }, + }, + ], + whenNotMatched: 'insert', + }, + }, + ]) + .toArray(); + } } diff --git a/env b/env index 535cfef..53c6e58 160000 --- a/env +++ b/env @@ -1 +1 @@ -Subproject commit 535cfef7d5c9f721fdae9e0d45ef4cdd40c3b9d6 +Subproject commit 53c6e5826d93e7b70380df9f3cf2c39844040cf8