Skip to content

Commit

Permalink
Merge pull request #894 from line/dev
Browse files Browse the repository at this point in the history
release: 6.2443.70
  • Loading branch information
jihun authored Oct 23, 2024
2 parents 5600f9f + 366957f commit 31fdc4a
Show file tree
Hide file tree
Showing 53 changed files with 2,578 additions and 2,109 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,9 +154,9 @@ Please follow the [contributing guidelines](./CONTRIBUTING.md) to contribute to
## License

```
Copyright 2023 LINE Corporation
Copyright 2024 LY Corporation
LINE Corporation licenses this file to you under the Apache License,
LY 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:
Expand Down
5 changes: 4 additions & 1 deletion apps/api/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,7 @@ REFESH_TOKEN_EXPIRED_TIME=1h # default: 1h

# AUTO_MIGRATION=true # default: true

# MASTER_API_KEY= # default: none
# MASTER_API_KEY= # default: none

# ENABLE_AUTO_FEEDBACK_DELETION=false # default: false
# AUTO_FEEDBACK_DELETION_PERIOD_DAYS=365*5
38 changes: 20 additions & 18 deletions apps/api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,24 +82,26 @@ The following is a list of environment variables used by the application, along

### Optional Environment Variables

| Environment | Description | Default Value |
| ---------------------- | -------------------------------------------------------------- | ----------------------------------- |
| `APP_PORT` | The port that the server runs on | `4000` |
| `APP_ADDRESS` | The address that the server binds to | `0.0.0.0` |
| `MYSQL_SECONDARY_URLS` | Secondary MySQL connection URLs (must be in JSON array format) | _optional_ |
| `SMTP_USE` | Flag to enable SMTP server usage (for email verification) | `false` |
| `SMTP_HOST` | SMTP server host | _required if `SMTP_USE=true`_ |
| `SMTP_PORT` | SMTP server port | _required if `SMTP_USE=true`_ |
| `SMTP_USERNAME` | SMTP server authentication username | _optional_ |
| `SMTP_PASSWORD` | SMTP server authentication password | _optional_ |
| `SMTP_SENDER` | Email address used as sender in emails | _required if `SMTP_USE=true`_ |
| `SMTP_BASE_URL` | Base URL for emails to link back to the application | _required if `SMTP_USE=true`_ |
| `OPENSEARCH_USE` | Flag to enable OpenSearch integration | `false` |
| `OPENSEARCH_NODE` | OpenSearch node URL | _required if `OPENSEARCH_USE=true`_ |
| `OPENSEARCH_USERNAME` | OpenSearch username (if authentication is enabled) | _required if `OPENSEARCH_USE=true`_ |
| `OPENSEARCH_PASSWORD` | OpenSearch password (if authentication is enabled) | _required if `OPENSEARCH_USE=true`_ |
| `AUTO_MIGRATION` | Automatically perform database migration on application start | `true` |
| `MASTER_API_KEY` | Master API key for privileged operations | _none_ |
| Environment | Description | Default Value |
| ------------------------------------ | -------------------------------------------------------------- | --------------------------------------------- |
| `APP_PORT` | The port that the server runs on | `4000` |
| `APP_ADDRESS` | The address that the server binds to | `0.0.0.0` |
| `MYSQL_SECONDARY_URLS` | Secondary MySQL connection URLs (must be in JSON array format) | _optional_ |
| `SMTP_USE` | Flag to enable SMTP server usage (for email verification) | `false` |
| `SMTP_HOST` | SMTP server host | _required if `SMTP_USE=true`_ |
| `SMTP_PORT` | SMTP server port | _required if `SMTP_USE=true`_ |
| `SMTP_USERNAME` | SMTP server authentication username | _optional_ |
| `SMTP_PASSWORD` | SMTP server authentication password | _optional_ |
| `SMTP_SENDER` | Email address used as sender in emails | _required if `SMTP_USE=true`_ |
| `SMTP_BASE_URL` | Base URL for emails to link back to the application | _required if `SMTP_USE=true`_ |
| `OPENSEARCH_USE` | Flag to enable OpenSearch integration | `false` |
| `OPENSEARCH_NODE` | OpenSearch node URL | _required if `OPENSEARCH_USE=true`_ |
| `OPENSEARCH_USERNAME` | OpenSearch username (if authentication is enabled) | _required if `OPENSEARCH_USE=true`_ |
| `OPENSEARCH_PASSWORD` | OpenSearch password (if authentication is enabled) | _required if `OPENSEARCH_USE=true`_ |
| `AUTO_MIGRATION` | Automatically perform database migration on application start | `true` |
| `MASTER_API_KEY` | Master API key for privileged operations | _none_ |
| `ENABLE_AUTO_FEEDBACK_DELETION` | Enable auto old feedback deletion cron on application start | `false` |
| `AUTO_FEEDBACK_DELETION_PERIOD_DAYS` | Auto old feedback deletion period (in days) | _required if `ENABLE_AUTO_FEEDBACK_DELETION`_ |

Please ensure that you set the required environment variables before starting the application. Optional variables can be set as needed based on your specific configuration and requirements.

Expand Down
2 changes: 2 additions & 0 deletions apps/api/integration-test/global.setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ process.env.NODE_ENV = 'test';
process.env.MYSQL_PRIMARY_URL =
'mysql://root:userfeedback@localhost:13307/integration';
process.env.MASTER_API_KEY = 'master-api-key';
process.env.ENABLE_AUTO_FEEDBACK_DELETION = 'true';
process.env.AUTO_FEEDBACK_DELETION_PERIOD_DAYS = '30';

async function createTestDatabase() {
const connection = await connect();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -206,13 +206,13 @@ describe('ChannelController (integration)', () => {
});
});

it('should return 400 error when update channel fields with special character', async () => {
it('should return 400 error when update channel field key with special character', async () => {
const dto = new UpdateChannelFieldsRequestDto();
const fieldDto = new UpdateChannelRequestFieldDto();
fieldDto.id = 5;
fieldDto.format = FieldFormatEnum.text;
fieldDto.key = 'testField';
fieldDto.name = '!';
fieldDto.key = 'testField!';
fieldDto.name = 'testField!';
dto.fields = [fieldDto];

await request(app.getHttpServer() as Server)
Expand Down
56 changes: 56 additions & 0 deletions apps/api/integration-test/test-specs/feedback.integration-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import type { TestingModule } from '@nestjs/testing';
import { Test } from '@nestjs/testing';
import { getDataSourceToken, getRepositoryToken } from '@nestjs/typeorm';
import type { Client } from '@opensearch-project/opensearch';
import { DateTime } from 'luxon';
import request from 'supertest';
import type { DataSource, Repository } from 'typeorm';
import { initializeTransactionalContext } from 'typeorm-transactional';
Expand Down Expand Up @@ -344,6 +345,61 @@ describe('FeedbackController (integration)', () => {
});
});

describe('old feedback deletion test', () => {
it('should create feedbacks and delete feedbacks within specific date range', async () => {
const dto: Record<string, string | number | string[] | number[]> = {};
fields
.filter(
({ key }) =>
key !== 'id' &&
key !== 'issues' &&
key !== 'createdAt' &&
key !== 'updatedAt',
)
.forEach(({ key, format, options }) => {
dto[key] = getRandomValue(format, options);
});

dto.createdAt = DateTime.now().minus({ month: 7 }).toFormat('yyyy-MM-dd');
await request(app.getHttpServer() as Server)
.post(`/admin/projects/${project.id}/channels/${channel.id}/feedbacks`)
.set('x-api-key', `${process.env.MASTER_API_KEY}`)
.send(dto)
.expect(201);

dto.createdAt = DateTime.now().minus({ days: 1 }).toFormat('yyyy-MM-dd');
await request(app.getHttpServer() as Server)
.post(`/admin/projects/${project.id}/channels/${channel.id}/feedbacks`)
.set('x-api-key', `${process.env.MASTER_API_KEY}`)
.send(dto)
.expect(201);

await tenantService.deleteOldFeedbacks();

const findFeedbackDto = {
query: {
createdAt: {
gte: DateTime.fromJSDate(new Date(0)).toFormat('yyyy-MM-dd'),
lt: DateTime.now().toFormat('yyyy-MM-dd'),
},
},
limit: 10,
page: 1,
};

return request(app.getHttpServer() as Server)
.post(
`/admin/projects/${project.id}/channels/${channel.id}/feedbacks/search`,
)
.set('Authorization', `Bearer ${accessToken}`)
.send(findFeedbackDto)
.expect(201)
.then(({ body }: { body: FindFeedbacksByChannelIdResponseDto }) => {
expect(body.meta.itemCount).toBe(1);
});
});
});

afterAll(async () => {
await clearEntities([tenantRepo, projectRepo, channelRepo, fieldRepo]);
const delay = (ms: number) =>
Expand Down
12 changes: 6 additions & 6 deletions apps/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
"test:integration": "jest --config ./integration-test/jest-integration.json --runInBand --detectOpenHandles",
"test:watch": "jest --watch --detectOpenHandles",
"typecheck": "tsc --noEmit",
"typeorm": "ts-node --project ./tsconfig.json -r tsconfig-paths/register ./node_modules/typeorm/cli -d src/configs/modules/typeorm-config/typeorm-config.datasource.ts"
"typeorm": "ts-node --project ./tsconfig.json -r tsconfig-paths/register ../../node_modules/typeorm/cli -d src/configs/modules/typeorm-config/typeorm-config.datasource.ts"
},
"prettier": "@ufb/prettier-config",
"dependencies": {
Expand Down Expand Up @@ -60,7 +60,7 @@
"dotenv": "^16.4.5",
"exceljs": "^4.4.0",
"fast-csv": "^5.0.1",
"fastify": "^4.26.2",
"fastify": "^5.0.0",
"joi": "^17.12.3",
"luxon": "^3.4.4",
"magic-bytes.js": "^1.10.0",
Expand Down Expand Up @@ -92,15 +92,15 @@
"@swc-node/jest": "^1.8.0",
"@swc/core": "^1.4.16",
"@types/bcrypt": "^5.0.2",
"@types/express": "^4.17.21",
"@types/express": "^5.0.0",
"@types/jest": "^29.5.12",
"@types/luxon": "^3.4.2",
"@types/node": "20.16.11",
"@types/node": "20.16.14",
"@types/nodemailer": "^6.4.15",
"@types/passport-jwt": "*",
"@types/supertest": "^6.0.2",
"@typescript-eslint/eslint-plugin": "^7.7.1",
"@typescript-eslint/parser": "^7.7.1",
"@typescript-eslint/eslint-plugin": "^8.0.0",
"@typescript-eslint/parser": "^8.0.0",
"@ufb/eslint-config": "workspace:*",
"@ufb/prettier-config": "workspace:*",
"@ufb/tsconfig": "workspace:*",
Expand Down
13 changes: 13 additions & 0 deletions apps/api/src/configs/app.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,24 @@ export const appConfigSchema = Joi.object({
APP_PORT: Joi.number().default(4000),
APP_ADDRESS: Joi.string().default('0.0.0.0'),
BASE_URL: Joi.string().required(),
ENABLE_AUTO_FEEDBACK_DELETION: Joi.boolean().default(false),
AUTO_FEEDBACK_DELETION_PERIOD_DAYS: Joi.number().when(
'ENABLE_AUTO_FEEDBACK_DELETION',
{
is: true,
then: Joi.required(),
otherwise: Joi.optional(),
},
),
});

export const appConfig = registerAs('app', () => ({
port: process.env.APP_PORT,
address: process.env.APP_ADDRESS,
baseUrl: process.env.APP_BASE_URL,
enableAutoFeedbackDeletion:
process.env.ENABLE_AUTO_FEEDBACK_DELETION === 'true',
autoFeedbackDeletionPeriodDays:
process.env.AUTO_FEEDBACK_DELETION_PERIOD_DAYS,
serverId: uuidv4(),
}));
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/**
* 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 ModifySchedulerLockEnum1728522901760
implements MigrationInterface
{
name = 'ModifySchedulerLockEnum1728522901760';

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE \`scheduler_locks\` CHANGE \`lock_type\` \`lock_type\` enum ('FEEDBACK_STATISTICS', 'ISSUE_STATISTICS', 'FEEDBACK_ISSUE_STATISTICS', 'FEEDBACK_COUNT', 'FEEDBACK_DELETE') NOT NULL`,
);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE \`scheduler_locks\` CHANGE \`lock_type\` \`lock_type\` enum ('FEEDBACK_STATISTICS', 'ISSUE_STATISTICS', 'FEEDBACK_ISSUE_STATISTICS', 'FEEDBACK_COUNT') NOT NULL`,
);
}
}
3 changes: 2 additions & 1 deletion apps/api/src/domains/admin/auth/auth.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { faker } from '@faker-js/faker';
import { BadRequestException } from '@nestjs/common';
import { Test } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { ClsModule } from 'nestjs-cls';
import type { Repository } from 'typeorm';

import { CodeEntity } from '@/shared/code/code.entity';
Expand Down Expand Up @@ -62,7 +63,7 @@ describe('auth service ', () => {
let apiKeyRepo: Repository<ApiKeyEntity>;
beforeEach(async () => {
const module = await Test.createTestingModule({
imports: [TestConfig],
imports: [TestConfig, ClsModule.forRoot()],
providers: AuthServiceProviders,
}).compile();
authService = module.get(AuthService);
Expand Down
4 changes: 2 additions & 2 deletions apps/api/src/domains/admin/auth/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ export class AuthService {
SignUpMethodEnum.EMAIL,
);
if (user) throw new UserAlreadyExistsException();
await this.userService.validateEmail(email);
await this.memberService.validateEmail(email);

const code = await this.codeService.setCode({
type: CodeTypeEnum.EMAIL_VEIRIFICATION,
Expand Down Expand Up @@ -133,7 +133,7 @@ export class AuthService {
CodeTypeEnum.EMAIL_VEIRIFICATION,
dto.email,
);
} catch (error) {
} catch {
throw new BadRequestException('must request email verification');
}
if (!isVerified) throw new NotVerifiedEmailException();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,11 @@ export class FieldMySQLService {
if (!validateUnique(fields, 'key')) {
throw new FieldKeyDuplicatedException();
}
fields.forEach(({ name }) => {
if (/^[a-z0-9_-]+$/i.test(name) === false) {
throw new BadRequestException('field name should be alphanumeric');
fields.forEach(({ key }) => {
if (/^[a-z0-9_]+$/i.test(key) === false) {
throw new BadRequestException(
'field key only should contain alphanumeric and underscore',
);
}
});
fields.forEach(({ format, options }) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import type { TimeRange } from '@/common/dtos';
import type { SortMethodEnum } from '@/common/enums';

export class GenerateExcelDto {
projectId: number;
channelId: number;
query?: {
searchText?: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ describe('FeedbackController', () => {
expect(MockFeedbackService.findByChannelId).toBeCalledTimes(1);
});
it('exportFeedbacks', async () => {
const projectId = faker.number.int();
const channelId = faker.number.int();
const response = {
type: jest.fn(),
Expand All @@ -113,7 +114,13 @@ describe('FeedbackController', () => {
project: { name: faker.string.sample() },
} as ChannelEntity);

await feedbackController.exportFeedbacks(channelId, dto, response, userDto);
await feedbackController.exportFeedbacks(
projectId,
channelId,
dto,
response,
userDto,
);

expect(MockFeedbackService.generateFile).toBeCalledTimes(1);
});
Expand Down
2 changes: 2 additions & 0 deletions apps/api/src/domains/admin/feedback/feedback.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ export class FeedbackController {
@ApiBearerAuth()
@Post('export')
async exportFeedbacks(
@Param('projectId', ParseIntPipe) projectId: number,
@Param('channelId', ParseIntPipe) channelId: number,
@Body() body: ExportFeedbacksRequestDto,
@Res() res: FastifyReply,
Expand All @@ -145,6 +146,7 @@ export class FeedbackController {

const { streamableFile, feedbackIds } =
await this.feedbackService.generateFile({
projectId,
channelId,
query,
sort,
Expand Down
Loading

0 comments on commit 31fdc4a

Please sign in to comment.