Skip to content

Commit 31fdc4a

Browse files
authored
Merge pull request #894 from line/dev
release: 6.2443.70
2 parents 5600f9f + 366957f commit 31fdc4a

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

53 files changed

+2578
-2109
lines changed

README.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -154,9 +154,9 @@ Please follow the [contributing guidelines](./CONTRIBUTING.md) to contribute to
154154
## License
155155

156156
```
157-
Copyright 2023 LINE Corporation
157+
Copyright 2024 LY Corporation
158158
159-
LINE Corporation licenses this file to you under the Apache License,
159+
LY Corporation licenses this file to you under the Apache License,
160160
version 2.0 (the "License"); you may not use this file except in compliance
161161
with the License. You may obtain a copy of the License at:
162162

apps/api/.env.example

+4-1
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,7 @@ REFESH_TOKEN_EXPIRED_TIME=1h # default: 1h
3030

3131
# AUTO_MIGRATION=true # default: true
3232

33-
# MASTER_API_KEY= # default: none
33+
# MASTER_API_KEY= # default: none
34+
35+
# ENABLE_AUTO_FEEDBACK_DELETION=false # default: false
36+
# AUTO_FEEDBACK_DELETION_PERIOD_DAYS=365*5

apps/api/README.md

+20-18
Original file line numberDiff line numberDiff line change
@@ -82,24 +82,26 @@ The following is a list of environment variables used by the application, along
8282

8383
### Optional Environment Variables
8484

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

104106
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.
105107

apps/api/integration-test/global.setup.ts

+2
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ process.env.NODE_ENV = 'test';
2323
process.env.MYSQL_PRIMARY_URL =
2424
'mysql://root:userfeedback@localhost:13307/integration';
2525
process.env.MASTER_API_KEY = 'master-api-key';
26+
process.env.ENABLE_AUTO_FEEDBACK_DELETION = 'true';
27+
process.env.AUTO_FEEDBACK_DELETION_PERIOD_DAYS = '30';
2628

2729
async function createTestDatabase() {
2830
const connection = await connect();

apps/api/integration-test/test-specs/channel.integration-spec.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -206,13 +206,13 @@ describe('ChannelController (integration)', () => {
206206
});
207207
});
208208

209-
it('should return 400 error when update channel fields with special character', async () => {
209+
it('should return 400 error when update channel field key with special character', async () => {
210210
const dto = new UpdateChannelFieldsRequestDto();
211211
const fieldDto = new UpdateChannelRequestFieldDto();
212212
fieldDto.id = 5;
213213
fieldDto.format = FieldFormatEnum.text;
214-
fieldDto.key = 'testField';
215-
fieldDto.name = '!';
214+
fieldDto.key = 'testField!';
215+
fieldDto.name = 'testField!';
216216
dto.fields = [fieldDto];
217217

218218
await request(app.getHttpServer() as Server)

apps/api/integration-test/test-specs/feedback.integration-spec.ts

+56
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import type { TestingModule } from '@nestjs/testing';
2020
import { Test } from '@nestjs/testing';
2121
import { getDataSourceToken, getRepositoryToken } from '@nestjs/typeorm';
2222
import type { Client } from '@opensearch-project/opensearch';
23+
import { DateTime } from 'luxon';
2324
import request from 'supertest';
2425
import type { DataSource, Repository } from 'typeorm';
2526
import { initializeTransactionalContext } from 'typeorm-transactional';
@@ -344,6 +345,61 @@ describe('FeedbackController (integration)', () => {
344345
});
345346
});
346347

348+
describe('old feedback deletion test', () => {
349+
it('should create feedbacks and delete feedbacks within specific date range', async () => {
350+
const dto: Record<string, string | number | string[] | number[]> = {};
351+
fields
352+
.filter(
353+
({ key }) =>
354+
key !== 'id' &&
355+
key !== 'issues' &&
356+
key !== 'createdAt' &&
357+
key !== 'updatedAt',
358+
)
359+
.forEach(({ key, format, options }) => {
360+
dto[key] = getRandomValue(format, options);
361+
});
362+
363+
dto.createdAt = DateTime.now().minus({ month: 7 }).toFormat('yyyy-MM-dd');
364+
await request(app.getHttpServer() as Server)
365+
.post(`/admin/projects/${project.id}/channels/${channel.id}/feedbacks`)
366+
.set('x-api-key', `${process.env.MASTER_API_KEY}`)
367+
.send(dto)
368+
.expect(201);
369+
370+
dto.createdAt = DateTime.now().minus({ days: 1 }).toFormat('yyyy-MM-dd');
371+
await request(app.getHttpServer() as Server)
372+
.post(`/admin/projects/${project.id}/channels/${channel.id}/feedbacks`)
373+
.set('x-api-key', `${process.env.MASTER_API_KEY}`)
374+
.send(dto)
375+
.expect(201);
376+
377+
await tenantService.deleteOldFeedbacks();
378+
379+
const findFeedbackDto = {
380+
query: {
381+
createdAt: {
382+
gte: DateTime.fromJSDate(new Date(0)).toFormat('yyyy-MM-dd'),
383+
lt: DateTime.now().toFormat('yyyy-MM-dd'),
384+
},
385+
},
386+
limit: 10,
387+
page: 1,
388+
};
389+
390+
return request(app.getHttpServer() as Server)
391+
.post(
392+
`/admin/projects/${project.id}/channels/${channel.id}/feedbacks/search`,
393+
)
394+
.set('Authorization', `Bearer ${accessToken}`)
395+
.send(findFeedbackDto)
396+
.expect(201)
397+
.then(({ body }: { body: FindFeedbacksByChannelIdResponseDto }) => {
398+
expect(body.meta.itemCount).toBe(1);
399+
});
400+
});
401+
});
402+
347403
afterAll(async () => {
348404
await clearEntities([tenantRepo, projectRepo, channelRepo, fieldRepo]);
349405
const delay = (ms: number) =>

apps/api/package.json

+6-6
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
"test:integration": "jest --config ./integration-test/jest-integration.json --runInBand --detectOpenHandles",
2323
"test:watch": "jest --watch --detectOpenHandles",
2424
"typecheck": "tsc --noEmit",
25-
"typeorm": "ts-node --project ./tsconfig.json -r tsconfig-paths/register ./node_modules/typeorm/cli -d src/configs/modules/typeorm-config/typeorm-config.datasource.ts"
25+
"typeorm": "ts-node --project ./tsconfig.json -r tsconfig-paths/register ../../node_modules/typeorm/cli -d src/configs/modules/typeorm-config/typeorm-config.datasource.ts"
2626
},
2727
"prettier": "@ufb/prettier-config",
2828
"dependencies": {
@@ -60,7 +60,7 @@
6060
"dotenv": "^16.4.5",
6161
"exceljs": "^4.4.0",
6262
"fast-csv": "^5.0.1",
63-
"fastify": "^4.26.2",
63+
"fastify": "^5.0.0",
6464
"joi": "^17.12.3",
6565
"luxon": "^3.4.4",
6666
"magic-bytes.js": "^1.10.0",
@@ -92,15 +92,15 @@
9292
"@swc-node/jest": "^1.8.0",
9393
"@swc/core": "^1.4.16",
9494
"@types/bcrypt": "^5.0.2",
95-
"@types/express": "^4.17.21",
95+
"@types/express": "^5.0.0",
9696
"@types/jest": "^29.5.12",
9797
"@types/luxon": "^3.4.2",
98-
"@types/node": "20.16.11",
98+
"@types/node": "20.16.14",
9999
"@types/nodemailer": "^6.4.15",
100100
"@types/passport-jwt": "*",
101101
"@types/supertest": "^6.0.2",
102-
"@typescript-eslint/eslint-plugin": "^7.7.1",
103-
"@typescript-eslint/parser": "^7.7.1",
102+
"@typescript-eslint/eslint-plugin": "^8.0.0",
103+
"@typescript-eslint/parser": "^8.0.0",
104104
"@ufb/eslint-config": "workspace:*",
105105
"@ufb/prettier-config": "workspace:*",
106106
"@ufb/tsconfig": "workspace:*",

apps/api/src/configs/app.config.ts

+13
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,24 @@ export const appConfigSchema = Joi.object({
2222
APP_PORT: Joi.number().default(4000),
2323
APP_ADDRESS: Joi.string().default('0.0.0.0'),
2424
BASE_URL: Joi.string().required(),
25+
ENABLE_AUTO_FEEDBACK_DELETION: Joi.boolean().default(false),
26+
AUTO_FEEDBACK_DELETION_PERIOD_DAYS: Joi.number().when(
27+
'ENABLE_AUTO_FEEDBACK_DELETION',
28+
{
29+
is: true,
30+
then: Joi.required(),
31+
otherwise: Joi.optional(),
32+
},
33+
),
2534
});
2635

2736
export const appConfig = registerAs('app', () => ({
2837
port: process.env.APP_PORT,
2938
address: process.env.APP_ADDRESS,
3039
baseUrl: process.env.APP_BASE_URL,
40+
enableAutoFeedbackDeletion:
41+
process.env.ENABLE_AUTO_FEEDBACK_DELETION === 'true',
42+
autoFeedbackDeletionPeriodDays:
43+
process.env.AUTO_FEEDBACK_DELETION_PERIOD_DAYS,
3144
serverId: uuidv4(),
3245
}));
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/**
2+
* Copyright 2023 LINE Corporation
3+
*
4+
* LINE Corporation licenses this file to you under the Apache License,
5+
* version 2.0 (the "License"); you may not use this file except in compliance
6+
* with the License. You may obtain a copy of the License at:
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
* License for the specific language governing permissions and limitations
14+
* under the License.
15+
*/
16+
import type { MigrationInterface, QueryRunner } from 'typeorm';
17+
18+
export class ModifySchedulerLockEnum1728522901760
19+
implements MigrationInterface
20+
{
21+
name = 'ModifySchedulerLockEnum1728522901760';
22+
23+
public async up(queryRunner: QueryRunner): Promise<void> {
24+
await queryRunner.query(
25+
`ALTER TABLE \`scheduler_locks\` CHANGE \`lock_type\` \`lock_type\` enum ('FEEDBACK_STATISTICS', 'ISSUE_STATISTICS', 'FEEDBACK_ISSUE_STATISTICS', 'FEEDBACK_COUNT', 'FEEDBACK_DELETE') NOT NULL`,
26+
);
27+
}
28+
29+
public async down(queryRunner: QueryRunner): Promise<void> {
30+
await queryRunner.query(
31+
`ALTER TABLE \`scheduler_locks\` CHANGE \`lock_type\` \`lock_type\` enum ('FEEDBACK_STATISTICS', 'ISSUE_STATISTICS', 'FEEDBACK_ISSUE_STATISTICS', 'FEEDBACK_COUNT') NOT NULL`,
32+
);
33+
}
34+
}

apps/api/src/domains/admin/auth/auth.service.spec.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { faker } from '@faker-js/faker';
1717
import { BadRequestException } from '@nestjs/common';
1818
import { Test } from '@nestjs/testing';
1919
import { getRepositoryToken } from '@nestjs/typeorm';
20+
import { ClsModule } from 'nestjs-cls';
2021
import type { Repository } from 'typeorm';
2122

2223
import { CodeEntity } from '@/shared/code/code.entity';
@@ -62,7 +63,7 @@ describe('auth service ', () => {
6263
let apiKeyRepo: Repository<ApiKeyEntity>;
6364
beforeEach(async () => {
6465
const module = await Test.createTestingModule({
65-
imports: [TestConfig],
66+
imports: [TestConfig, ClsModule.forRoot()],
6667
providers: AuthServiceProviders,
6768
}).compile();
6869
authService = module.get(AuthService);

apps/api/src/domains/admin/auth/auth.service.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ export class AuthService {
9090
SignUpMethodEnum.EMAIL,
9191
);
9292
if (user) throw new UserAlreadyExistsException();
93-
await this.userService.validateEmail(email);
93+
await this.memberService.validateEmail(email);
9494

9595
const code = await this.codeService.setCode({
9696
type: CodeTypeEnum.EMAIL_VEIRIFICATION,
@@ -133,7 +133,7 @@ export class AuthService {
133133
CodeTypeEnum.EMAIL_VEIRIFICATION,
134134
dto.email,
135135
);
136-
} catch (error) {
136+
} catch {
137137
throw new BadRequestException('must request email verification');
138138
}
139139
if (!isVerified) throw new NotVerifiedEmailException();

apps/api/src/domains/admin/channel/field/field.mysql.service.ts

+5-3
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,11 @@ export class FieldMySQLService {
5050
if (!validateUnique(fields, 'key')) {
5151
throw new FieldKeyDuplicatedException();
5252
}
53-
fields.forEach(({ name }) => {
54-
if (/^[a-z0-9_-]+$/i.test(name) === false) {
55-
throw new BadRequestException('field name should be alphanumeric');
53+
fields.forEach(({ key }) => {
54+
if (/^[a-z0-9_]+$/i.test(key) === false) {
55+
throw new BadRequestException(
56+
'field key only should contain alphanumeric and underscore',
57+
);
5658
}
5759
});
5860
fields.forEach(({ format, options }) => {

apps/api/src/domains/admin/feedback/dtos/generate-excel.dto.ts

+1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import type { TimeRange } from '@/common/dtos';
1717
import type { SortMethodEnum } from '@/common/enums';
1818

1919
export class GenerateExcelDto {
20+
projectId: number;
2021
channelId: number;
2122
query?: {
2223
searchText?: string;

apps/api/src/domains/admin/feedback/feedback.controller.spec.ts

+8-1
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ describe('FeedbackController', () => {
9494
expect(MockFeedbackService.findByChannelId).toBeCalledTimes(1);
9595
});
9696
it('exportFeedbacks', async () => {
97+
const projectId = faker.number.int();
9798
const channelId = faker.number.int();
9899
const response = {
99100
type: jest.fn(),
@@ -113,7 +114,13 @@ describe('FeedbackController', () => {
113114
project: { name: faker.string.sample() },
114115
} as ChannelEntity);
115116

116-
await feedbackController.exportFeedbacks(channelId, dto, response, userDto);
117+
await feedbackController.exportFeedbacks(
118+
projectId,
119+
channelId,
120+
dto,
121+
response,
122+
userDto,
123+
);
117124

118125
expect(MockFeedbackService.generateFile).toBeCalledTimes(1);
119126
});

apps/api/src/domains/admin/feedback/feedback.controller.ts

+2
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ export class FeedbackController {
133133
@ApiBearerAuth()
134134
@Post('export')
135135
async exportFeedbacks(
136+
@Param('projectId', ParseIntPipe) projectId: number,
136137
@Param('channelId', ParseIntPipe) channelId: number,
137138
@Body() body: ExportFeedbacksRequestDto,
138139
@Res() res: FastifyReply,
@@ -145,6 +146,7 @@ export class FeedbackController {
145146

146147
const { streamableFile, feedbackIds } =
147148
await this.feedbackService.generateFile({
149+
projectId,
148150
channelId,
149151
query,
150152
sort,

0 commit comments

Comments
 (0)