Skip to content

Commit

Permalink
feat: project timezone setting (#136)
Browse files Browse the repository at this point in the history
* feat: add project timezone

* fix: project timezone as json (#135)

* feat: select timezone

---------

Co-authored-by: Carson <[email protected]>
  • Loading branch information
chiol and h4l-yup authored Jan 17, 2024
1 parent 035240a commit 8f2d53c
Show file tree
Hide file tree
Showing 62 changed files with 972 additions and 192 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/**
* Copyright 2023 LINE Corporation
*
* LINE Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
import type { MigrationInterface, QueryRunner } from 'typeorm';

export class ProjectTimezoneJson1705398750913 implements MigrationInterface {
name = 'ProjectTimezoneJson1705398750913';

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE \`projects\` CHANGE \`timezone_offset\` \`timezone\` varchar(255) NULL DEFAULT '+00:00'`,
);
await queryRunner.query(
`ALTER TABLE \`projects\` DROP COLUMN \`timezone\``,
);
await queryRunner.query(
`ALTER TABLE \`projects\` ADD \`timezone\` varchar(255) NOT NULL DEFAULT '{"countryCode":"KR","name":"Asia/Seoul","offset":"+09:00"}'`,
);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE \`projects\` DROP COLUMN \`timezone\``,
);
await queryRunner.query(
`ALTER TABLE \`projects\` ADD \`timezone\` varchar(255) NULL DEFAULT '+00:00'`,
);
await queryRunner.query(
`ALTER TABLE \`projects\` CHANGE \`timezone\` \`timezone_offset\` varchar(255) NULL DEFAULT '+00:00'`,
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export class FindIssueByIdResponseDto {
@ApiProperty({
description: 'Issue status',
example: IssueStatusEnum.IN_PROGRESS,
enum: IssueStatusEnum,
})
status: IssueStatusEnum;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,14 @@
* License for the specific language governing permissions and limitations
* under the License.
*/
import type { TimezoneOffset } from '@ufb/shared';

import type { CreateRoleDto } from '../../role/dtos';
import type { Timezone } from '../project.entity';

export class CreateProjectDto {
name: string;
description: string;
timezoneOffset: TimezoneOffset;
timezone: Timezone;
roles?: Omit<CreateRoleDto, 'projectId'>[];
members?: {
roleName: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { ApiProperty } from '@nestjs/swagger';
import {
IsArray,
IsNumber,
IsObject,
IsOptional,
IsString,
Length,
Expand Down Expand Up @@ -47,6 +48,20 @@ class CreateApiKeyByValueDto {
value: string;
}

class TimezoneDto {
@ApiProperty()
@IsString()
countryCode: string;

@ApiProperty()
@IsString()
name: string;

@ApiProperty()
@IsString()
offset: TimezoneOffset;
}

export class CreateProjectRequestDto {
@ApiProperty()
@IsString()
Expand All @@ -60,9 +75,9 @@ export class CreateProjectRequestDto {
@MaxLength(50)
description: string | null;

@ApiProperty()
@IsString()
timezoneOffset: TimezoneOffset;
@ApiProperty({ type: TimezoneDto })
@IsObject()
timezone: TimezoneDto;

@ApiProperty({ type: [CreateRoleRequestDto], required: false })
@IsArray()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,20 @@ import { Expose, plainToInstance } from 'class-transformer';

import { TimezoneOffset } from '@ufb/shared';

export class TimezoneDto {
@Expose()
@ApiProperty()
countryCode: string;

@Expose()
@ApiProperty()
name: string;

@Expose()
@ApiProperty()
offset: TimezoneOffset;
}

export class FindProjectByIdResponseDto {
@Expose()
@ApiProperty()
Expand All @@ -32,8 +46,8 @@ export class FindProjectByIdResponseDto {
description: string;

@Expose()
@ApiProperty()
timezoneOffset: TimezoneOffset;
@ApiProperty({ type: TimezoneDto })
timezone: TimezoneDto;

@Expose()
@ApiProperty()
Expand Down
30 changes: 24 additions & 6 deletions apps/api/src/domains/project/project/project.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import {
Relation,
} from 'typeorm';

import { TimezoneOffset } from '@ufb/shared';
import type { TimezoneOffset } from '@ufb/shared';

import { CommonEntity } from '@/common/entities';
import type { ApiKeyEntity } from '@/domains/project/api-key/api-key.entity';
Expand All @@ -33,6 +33,12 @@ import { ChannelEntity } from '../../channel/channel/channel.entity';
import { IssueEntity } from '../issue/issue.entity';
import { RoleEntity } from '../role/role.entity';

export interface Timezone {
countryCode: string;
name: string;
offset: TimezoneOffset;
}

@Entity('projects')
export class ProjectEntity extends CommonEntity {
@Column('varchar', { unique: true })
Expand All @@ -41,8 +47,20 @@ export class ProjectEntity extends CommonEntity {
@Column('varchar', { nullable: true })
description: string;

@Column('varchar', { default: '+00:00' })
timezoneOffset: TimezoneOffset;
@Column({
type: 'varchar',
default: JSON.stringify({
countryCode: 'KR',
name: 'Asia/Seoul',
offset: '+09:00',
}),
transformer: {
from: (value: string) =>
typeof value === 'object' ? value : JSON.parse(value),
to: (value: Timezone) => JSON.stringify(value),
},
})
timezone: Timezone;

@OneToMany(() => ChannelEntity, (channel) => channel.project, {
cascade: true,
Expand Down Expand Up @@ -83,19 +101,19 @@ export class ProjectEntity extends CommonEntity {
tenantId,
name,
description,
timezoneOffset,
timezone,
}: {
tenantId: number;
name: string;
description: string;
timezoneOffset: TimezoneOffset;
timezone: Timezone;
}) {
const project = new ProjectEntity();
project.tenant = new TenantEntity();
project.tenant.id = tenantId;
project.name = name;
project.description = description;
project.timezoneOffset = timezoneOffset;
project.timezone = timezone;

return project;
}
Expand Down
72 changes: 57 additions & 15 deletions apps/api/src/domains/project/project/project.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,11 @@ describe('ProjectService Test suite', () => {
.spyOn(projectRepo, 'save')
.mockResolvedValue({ id: projectId } as any);
jest.spyOn(projectRepo, 'findOne').mockResolvedValue({
timezoneOffset: '+09:00',
timezone: {
countryCode: 'KR',
name: 'Asia/Seoul',
offset: '+09:00',
},
} as ProjectEntity);

const { id } = await projectService.create(dto);
Expand All @@ -117,7 +121,11 @@ describe('ProjectService Test suite', () => {
.mockResolvedValue({ id: projectId } as any);
jest.spyOn(roleRepo, 'findOneBy').mockResolvedValue(null);
jest.spyOn(projectRepo, 'findOne').mockResolvedValue({
timezoneOffset: '+09:00',
timezone: {
countryCode: 'KR',
name: 'Asia/Seoul',
offset: '+09:00',
},
} as ProjectEntity);

const { id } = await projectService.create(dto);
Expand Down Expand Up @@ -165,7 +173,11 @@ describe('ProjectService Test suite', () => {
})) as any,
);
jest.spyOn(projectRepo, 'findOne').mockResolvedValue({
timezoneOffset: '+09:00',
timezone: {
countryCode: 'KR',
name: 'Asia/Seoul',
offset: '+09:00',
},
} as ProjectEntity);

const { id } = await projectService.create(dto);
Expand Down Expand Up @@ -197,9 +209,14 @@ describe('ProjectService Test suite', () => {
];
jest.spyOn(projectRepo, 'findOneBy').mockResolvedValueOnce(null);
jest.spyOn(tenantRepo, 'find').mockResolvedValue([{}] as TenantEntity[]);
jest
.spyOn(projectRepo, 'save')
.mockResolvedValue({ id: projectId, timezoneOffset: '+09:00' } as any);
jest.spyOn(projectRepo, 'save').mockResolvedValue({
id: projectId,
timezone: {
countryCode: 'KR',
name: 'Asia/Seoul',
offset: '+09:00',
},
} as any);
jest.spyOn(roleRepo, 'findOneBy').mockResolvedValue(null);
jest.spyOn(roleRepo, 'save').mockResolvedValue(
dto.roles.map((role) => ({
Expand All @@ -222,7 +239,11 @@ describe('ProjectService Test suite', () => {
.spyOn(projectRepo, 'findOneBy')
.mockResolvedValueOnce({} as ProjectEntity);
jest.spyOn(projectRepo, 'findOne').mockResolvedValue({
timezoneOffset: '+09:00',
timezone: {
countryCode: 'KR',
name: 'Asia/Seoul',
offset: '+09:00',
},
} as ProjectEntity);

const { id } = await projectService.create(dto);
Expand Down Expand Up @@ -260,9 +281,14 @@ describe('ProjectService Test suite', () => {
};
jest.spyOn(projectRepo, 'findOneBy').mockResolvedValueOnce(null);
jest.spyOn(tenantRepo, 'find').mockResolvedValue([{}] as TenantEntity[]);
jest
.spyOn(projectRepo, 'save')
.mockResolvedValue({ id: projectId, timezoneOffset: '+09:00' } as any);
jest.spyOn(projectRepo, 'save').mockResolvedValue({
id: projectId,
timezone: {
countryCode: 'KR',
name: 'Asia/Seoul',
offset: '+09:00',
},
} as any);
jest.spyOn(roleRepo, 'findOneBy').mockResolvedValue(null);
jest.spyOn(roleRepo, 'save').mockResolvedValue(
dto.roles.map((role) => ({
Expand All @@ -272,7 +298,14 @@ describe('ProjectService Test suite', () => {
})) as any,
);
jest.spyOn(roleRepo, 'findOne').mockResolvedValue({
project: { id: projectId, timezoneOffset: '+09:00' },
project: {
id: projectId,
timezone: {
countryCode: 'KR',
name: 'Asia/Seoul',
offset: '+09:00',
},
},
} as RoleEntity);
jest.spyOn(userRepo, 'findOne').mockResolvedValue({} as UserEntity);
jest.spyOn(memberRepo, 'findOne').mockResolvedValue(null);
Expand All @@ -285,7 +318,11 @@ describe('ProjectService Test suite', () => {
.spyOn(projectRepo, 'findOneBy')
.mockResolvedValueOnce({} as ProjectEntity);
jest.spyOn(projectRepo, 'findOne').mockResolvedValue({
timezoneOffset: '+09:00',
timezone: {
countryCode: 'KR',
name: 'Asia/Seoul',
offset: '+09:00',
},
} as ProjectEntity);

const { id } = await projectService.create(dto);
Expand All @@ -304,9 +341,14 @@ describe('ProjectService Test suite', () => {
.spyOn(projectRepo, 'findOneBy')
.mockResolvedValue({ name: dto.name } as ProjectEntity);
jest.spyOn(tenantRepo, 'find').mockResolvedValue([{}] as TenantEntity[]);
jest
.spyOn(projectRepo, 'save')
.mockResolvedValue({ id: projectId, timezoneOffset: '+09:00' } as any);
jest.spyOn(projectRepo, 'save').mockResolvedValue({
id: projectId,
timezone: {
countryCode: 'KR',
name: 'Asia/Seoul',
offset: '+09:00',
},
} as any);

await expect(projectService.create(dto)).rejects.toThrowError(
ProjectAlreadyExistsException,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,11 @@ describe('FeedbackIssueStatisticsService suite', () => {
it('adding a cron job succeeds with valid input', async () => {
const projectId = faker.number.int();
jest.spyOn(projectRepo, 'findOne').mockResolvedValue({
timezoneOffset: '+09:00',
timezone: {
countryCode: 'KR',
name: 'Asia/Seoul',
offset: '+09:00',
},
} as ProjectEntity);
jest.spyOn(schedulerRegistry, 'addCronJob');

Expand All @@ -255,7 +259,11 @@ describe('FeedbackIssueStatisticsService suite', () => {
id: faker.number.int(),
}));
jest.spyOn(projectRepo, 'findOne').mockResolvedValue({
timezoneOffset: '+09:00',
timezone: {
countryCode: 'KR',
name: 'Asia/Seoul',
offset: '+09:00',
},
} as ProjectEntity);
jest.spyOn(issueRepo, 'find').mockResolvedValue(issues as IssueEntity[]);
jest.spyOn(feedbackRepo, 'count').mockResolvedValueOnce(0);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,12 +109,11 @@ export class FeedbackIssueStatisticsService {
}

async addCronJobByProjectId(projectId: number) {
const { timezoneOffset } = await this.projectRepository.findOne({
const { timezone } = await this.projectRepository.findOne({
where: { id: projectId },
});

const timezoneOffset = timezone.offset;
const cronHour = (24 - Number(timezoneOffset.split(':')[0])) % 24;

const job = new CronJob(`0 ${cronHour} * * *`, async () => {
await this.createFeedbackIssueStatistics(projectId);
});
Expand All @@ -132,9 +131,10 @@ export class FeedbackIssueStatisticsService {
projectId: number,
dayToCreate: number = 1,
) {
const { timezoneOffset } = await this.projectRepository.findOne({
const { timezone } = await this.projectRepository.findOne({
where: { id: projectId },
});
const timezoneOffset = timezone.offset;
const [hours, minutes] = timezoneOffset.split(':');
const offset = Number(hours) + Number(minutes) / 60;

Expand Down
Loading

0 comments on commit 8f2d53c

Please sign in to comment.