mirror of
https://github.com/samsonjs/immich.git
synced 2026-04-27 15:07:45 +00:00
fix: null validation (#25891)
This commit is contained in:
parent
440b3b4c6f
commit
9dddccd831
18 changed files with 343 additions and 45 deletions
BIN
mobile/openapi/lib/model/asset_bulk_update_dto.dart
generated
BIN
mobile/openapi/lib/model/asset_bulk_update_dto.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/model/user_admin_create_dto.dart
generated
BIN
mobile/openapi/lib/model/user_admin_create_dto.dart
generated
Binary file not shown.
|
|
@ -15760,7 +15760,7 @@
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"duplicateId": {
|
"duplicateId": {
|
||||||
"description": "Duplicate asset ID",
|
"description": "Duplicate ID",
|
||||||
"nullable": true,
|
"nullable": true,
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
|
@ -19038,6 +19038,7 @@
|
||||||
"format": "uuid",
|
"format": "uuid",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"minItems": 1,
|
||||||
"type": "array"
|
"type": "array"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -19128,6 +19129,7 @@
|
||||||
"format": "uuid",
|
"format": "uuid",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"minItems": 1,
|
||||||
"type": "array"
|
"type": "array"
|
||||||
},
|
},
|
||||||
"readAt": {
|
"readAt": {
|
||||||
|
|
@ -25069,6 +25071,12 @@
|
||||||
"description": "User password",
|
"description": "User password",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"pinCode": {
|
||||||
|
"description": "PIN code",
|
||||||
|
"example": "123456",
|
||||||
|
"nullable": true,
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"quotaSizeInBytes": {
|
"quotaSizeInBytes": {
|
||||||
"description": "Storage quota in bytes",
|
"description": "Storage quota in bytes",
|
||||||
"format": "int64",
|
"format": "int64",
|
||||||
|
|
|
||||||
|
|
@ -233,6 +233,8 @@ export type UserAdminCreateDto = {
|
||||||
notify?: boolean;
|
notify?: boolean;
|
||||||
/** User password */
|
/** User password */
|
||||||
password: string;
|
password: string;
|
||||||
|
/** PIN code */
|
||||||
|
pinCode?: string | null;
|
||||||
/** Storage quota in bytes */
|
/** Storage quota in bytes */
|
||||||
quotaSizeInBytes?: number | null;
|
quotaSizeInBytes?: number | null;
|
||||||
/** Require password change on next login */
|
/** Require password change on next login */
|
||||||
|
|
@ -822,7 +824,7 @@ export type AssetBulkUpdateDto = {
|
||||||
dateTimeRelative?: number;
|
dateTimeRelative?: number;
|
||||||
/** Asset description */
|
/** Asset description */
|
||||||
description?: string;
|
description?: string;
|
||||||
/** Duplicate asset ID */
|
/** Duplicate ID */
|
||||||
duplicateId?: string | null;
|
duplicateId?: string | null;
|
||||||
/** Asset IDs to update */
|
/** Asset IDs to update */
|
||||||
ids: string[];
|
ids: string[];
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,34 @@ describe(AssetController.name, () => {
|
||||||
await request(ctx.getHttpServer()).put(`/assets`);
|
await request(ctx.getHttpServer()).put(`/assets`);
|
||||||
expect(ctx.authenticate).toHaveBeenCalled();
|
expect(ctx.authenticate).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should require a valid uuid', async () => {
|
||||||
|
const { status, body } = await request(ctx.getHttpServer())
|
||||||
|
.put(`/assets`)
|
||||||
|
.send({ ids: ['123'] });
|
||||||
|
|
||||||
|
expect(status).toBe(400);
|
||||||
|
expect(body).toEqual(factory.responses.badRequest(['each value in ids must be a UUID']));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should require duplicateId to be a string', async () => {
|
||||||
|
const id = factory.uuid();
|
||||||
|
const { status, body } = await request(ctx.getHttpServer())
|
||||||
|
.put(`/assets`)
|
||||||
|
.send({ ids: [id], duplicateId: true });
|
||||||
|
|
||||||
|
expect(status).toBe(400);
|
||||||
|
expect(body).toEqual(factory.responses.badRequest(['duplicateId must be a string']));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should accept a null duplicateId', async () => {
|
||||||
|
const id = factory.uuid();
|
||||||
|
await request(ctx.getHttpServer())
|
||||||
|
.put(`/assets`)
|
||||||
|
.send({ ids: [id], duplicateId: null });
|
||||||
|
|
||||||
|
expect(service.updateAll).toHaveBeenCalledWith(undefined, expect.objectContaining({ duplicateId: null }));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('DELETE /assets', () => {
|
describe('DELETE /assets', () => {
|
||||||
|
|
|
||||||
36
server/src/controllers/notification-admin.controller.spec.ts
Normal file
36
server/src/controllers/notification-admin.controller.spec.ts
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
import { NotificationAdminController } from 'src/controllers/notification-admin.controller';
|
||||||
|
import { NotificationAdminService } from 'src/services/notification-admin.service';
|
||||||
|
import request from 'supertest';
|
||||||
|
import { factory } from 'test/small.factory';
|
||||||
|
import { ControllerContext, controllerSetup, mockBaseService } from 'test/utils';
|
||||||
|
|
||||||
|
describe(NotificationAdminController.name, () => {
|
||||||
|
let ctx: ControllerContext;
|
||||||
|
const service = mockBaseService(NotificationAdminService);
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
ctx = await controllerSetup(NotificationAdminController, [
|
||||||
|
{ provide: NotificationAdminService, useValue: service },
|
||||||
|
]);
|
||||||
|
return () => ctx.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
service.resetAllMocks();
|
||||||
|
ctx.reset();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('POST /admin/notifications', () => {
|
||||||
|
it('should be an authenticated route', async () => {
|
||||||
|
await request(ctx.getHttpServer()).post('/admin/notifications');
|
||||||
|
expect(ctx.authenticate).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should accept a null readAt', async () => {
|
||||||
|
await request(ctx.getHttpServer())
|
||||||
|
.post(`/admin/notifications`)
|
||||||
|
.send({ title: 'Test', userId: factory.uuid(), readAt: null });
|
||||||
|
expect(service.create).toHaveBeenCalledWith(undefined, expect.objectContaining({ readAt: null }));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -37,9 +37,33 @@ describe(NotificationController.name, () => {
|
||||||
|
|
||||||
describe('PUT /notifications', () => {
|
describe('PUT /notifications', () => {
|
||||||
it('should be an authenticated route', async () => {
|
it('should be an authenticated route', async () => {
|
||||||
await request(ctx.getHttpServer()).get('/notifications');
|
await request(ctx.getHttpServer()).put('/notifications');
|
||||||
expect(ctx.authenticate).toHaveBeenCalled();
|
expect(ctx.authenticate).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('ids', () => {
|
||||||
|
it('should require a list', async () => {
|
||||||
|
const { status, body } = await request(ctx.getHttpServer()).put(`/notifications`).send({ ids: true });
|
||||||
|
expect(status).toBe(400);
|
||||||
|
expect(body).toEqual(errorDto.badRequest(expect.arrayContaining(['ids must be an array'])));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should require uuids', async () => {
|
||||||
|
const { status, body } = await request(ctx.getHttpServer())
|
||||||
|
.put(`/notifications`)
|
||||||
|
.send({ ids: [true] });
|
||||||
|
expect(status).toBe(400);
|
||||||
|
expect(body).toEqual(errorDto.badRequest(['each value in ids must be a UUID']));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should accept valid uuids', async () => {
|
||||||
|
const id = factory.uuid();
|
||||||
|
await request(ctx.getHttpServer())
|
||||||
|
.put(`/notifications`)
|
||||||
|
.send({ ids: [id] });
|
||||||
|
expect(service.updateAll).toHaveBeenCalledWith(undefined, expect.objectContaining({ ids: [id] }));
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('GET /notifications/:id', () => {
|
describe('GET /notifications/:id', () => {
|
||||||
|
|
@ -60,5 +84,11 @@ describe(NotificationController.name, () => {
|
||||||
await request(ctx.getHttpServer()).put(`/notifications/${factory.uuid()}`).send({ readAt: factory.date() });
|
await request(ctx.getHttpServer()).put(`/notifications/${factory.uuid()}`).send({ readAt: factory.date() });
|
||||||
expect(ctx.authenticate).toHaveBeenCalled();
|
expect(ctx.authenticate).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should accept a null readAt', async () => {
|
||||||
|
const id = factory.uuid();
|
||||||
|
await request(ctx.getHttpServer()).put(`/notifications/${id}`).send({ readAt: null });
|
||||||
|
expect(service.update).toHaveBeenCalledWith(undefined, id, expect.objectContaining({ readAt: null }));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,11 @@ describe(PersonController.name, () => {
|
||||||
await request(ctx.getHttpServer()).post('/people').send({ birthDate: '' });
|
await request(ctx.getHttpServer()).post('/people').send({ birthDate: '' });
|
||||||
expect(service.create).toHaveBeenCalledWith(undefined, { birthDate: null });
|
expect(service.create).toHaveBeenCalledWith(undefined, { birthDate: null });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should map an empty color to null', async () => {
|
||||||
|
await request(ctx.getHttpServer()).post('/people').send({ color: '' });
|
||||||
|
expect(service.create).toHaveBeenCalledWith(undefined, { color: null });
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('DELETE /people', () => {
|
describe('DELETE /people', () => {
|
||||||
|
|
|
||||||
34
server/src/controllers/shared-link.controller.spec.ts
Normal file
34
server/src/controllers/shared-link.controller.spec.ts
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
import { SharedLinkController } from 'src/controllers/shared-link.controller';
|
||||||
|
import { SharedLinkType } from 'src/enum';
|
||||||
|
import { SharedLinkService } from 'src/services/shared-link.service';
|
||||||
|
import request from 'supertest';
|
||||||
|
import { ControllerContext, controllerSetup, mockBaseService } from 'test/utils';
|
||||||
|
|
||||||
|
describe(SharedLinkController.name, () => {
|
||||||
|
let ctx: ControllerContext;
|
||||||
|
const service = mockBaseService(SharedLinkService);
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
ctx = await controllerSetup(SharedLinkController, [{ provide: SharedLinkService, useValue: service }]);
|
||||||
|
return () => ctx.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
service.resetAllMocks();
|
||||||
|
ctx.reset();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('POST /shared-links', () => {
|
||||||
|
it('should be an authenticated route', async () => {
|
||||||
|
await request(ctx.getHttpServer()).post('/shared-links');
|
||||||
|
expect(ctx.authenticate).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should allow an null expiresAt', async () => {
|
||||||
|
await request(ctx.getHttpServer())
|
||||||
|
.post('/shared-links')
|
||||||
|
.send({ expiresAt: null, type: SharedLinkType.Individual });
|
||||||
|
expect(service.create).toHaveBeenCalledWith(undefined, expect.objectContaining({ expiresAt: null }));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
73
server/src/controllers/tag.controller.spec.ts
Normal file
73
server/src/controllers/tag.controller.spec.ts
Normal file
|
|
@ -0,0 +1,73 @@
|
||||||
|
import { TagController } from 'src/controllers/tag.controller';
|
||||||
|
import { TagService } from 'src/services/tag.service';
|
||||||
|
import request from 'supertest';
|
||||||
|
import { errorDto } from 'test/medium/responses';
|
||||||
|
import { factory } from 'test/small.factory';
|
||||||
|
import { ControllerContext, controllerSetup, mockBaseService } from 'test/utils';
|
||||||
|
|
||||||
|
describe(TagController.name, () => {
|
||||||
|
let ctx: ControllerContext;
|
||||||
|
const service = mockBaseService(TagService);
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
ctx = await controllerSetup(TagController, [{ provide: TagService, useValue: service }]);
|
||||||
|
return () => ctx.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
service.resetAllMocks();
|
||||||
|
ctx.reset();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('GET /tags', () => {
|
||||||
|
it('should be an authenticated route', async () => {
|
||||||
|
await request(ctx.getHttpServer()).get('/tags');
|
||||||
|
expect(ctx.authenticate).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('POST /tags', () => {
|
||||||
|
it('should be an authenticated route', async () => {
|
||||||
|
await request(ctx.getHttpServer()).post('/tags');
|
||||||
|
expect(ctx.authenticate).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should a null parentId', async () => {
|
||||||
|
await request(ctx.getHttpServer()).post(`/tags`).send({ name: 'tag', parentId: null });
|
||||||
|
expect(service.create).toHaveBeenCalledWith(undefined, expect.objectContaining({ parentId: null }));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('PUT /tags', () => {
|
||||||
|
it('should be an authenticated route', async () => {
|
||||||
|
await request(ctx.getHttpServer()).put('/tags');
|
||||||
|
expect(ctx.authenticate).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('GET /tags/:id', () => {
|
||||||
|
it('should be an authenticated route', async () => {
|
||||||
|
await request(ctx.getHttpServer()).get(`/tags/${factory.uuid()}`);
|
||||||
|
expect(ctx.authenticate).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should require a valid uuid', async () => {
|
||||||
|
const { status, body } = await request(ctx.getHttpServer()).get(`/tags/123`);
|
||||||
|
expect(status).toBe(400);
|
||||||
|
expect(body).toEqual(errorDto.badRequest([expect.stringContaining('id must be a UUID')]));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('PUT /tags/:id', () => {
|
||||||
|
it('should be an authenticated route', async () => {
|
||||||
|
await request(ctx.getHttpServer()).put(`/tags/${factory.uuid()}`);
|
||||||
|
expect(ctx.authenticate).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should allow setting a null color via an empty string', async () => {
|
||||||
|
const id = factory.uuid();
|
||||||
|
await request(ctx.getHttpServer()).put(`/tags/${id}`).send({ color: '' });
|
||||||
|
expect(service.update).toHaveBeenCalledWith(undefined, id, expect.objectContaining({ color: null }));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -31,12 +31,55 @@ describe(UserAdminController.name, () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('PUT /admin/users/:id', () => {
|
||||||
|
it('should be an authenticated route', async () => {
|
||||||
|
await request(ctx.getHttpServer()).put(`/admin/users/${factory.uuid()}`);
|
||||||
|
expect(ctx.authenticate).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('POST /admin/users', () => {
|
describe('POST /admin/users', () => {
|
||||||
it('should be an authenticated route', async () => {
|
it('should be an authenticated route', async () => {
|
||||||
await request(ctx.getHttpServer()).post('/admin/users');
|
await request(ctx.getHttpServer()).post('/admin/users');
|
||||||
expect(ctx.authenticate).toHaveBeenCalled();
|
expect(ctx.authenticate).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should allow a null pinCode', async () => {
|
||||||
|
await request(ctx.getHttpServer()).post(`/admin/users`).send({
|
||||||
|
name: 'Test user',
|
||||||
|
email: 'test@immich.cloud',
|
||||||
|
password: 'password',
|
||||||
|
pinCode: null,
|
||||||
|
});
|
||||||
|
expect(service.create).toHaveBeenCalledWith(expect.objectContaining({ pinCode: null }));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should allow a null avatarColor', async () => {
|
||||||
|
await request(ctx.getHttpServer()).post(`/admin/users`).send({
|
||||||
|
name: 'Test user',
|
||||||
|
email: 'test@immich.cloud',
|
||||||
|
password: 'password',
|
||||||
|
avatarColor: null,
|
||||||
|
});
|
||||||
|
expect(service.create).toHaveBeenCalledWith(expect.objectContaining({ avatarColor: null }));
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`should `, async () => {
|
||||||
|
const dto: UserAdminCreateDto = {
|
||||||
|
email: 'user@immich.app',
|
||||||
|
password: 'test',
|
||||||
|
name: 'Test User',
|
||||||
|
quotaSizeInBytes: 1.2,
|
||||||
|
};
|
||||||
|
|
||||||
|
const { status, body } = await request(ctx.getHttpServer())
|
||||||
|
.post(`/admin/users`)
|
||||||
|
.set('Authorization', `Bearer token`)
|
||||||
|
.send(dto);
|
||||||
|
expect(status).toBe(400);
|
||||||
|
expect(body).toEqual(errorDto.badRequest(expect.arrayContaining(['quotaSizeInBytes must be an integer number'])));
|
||||||
|
});
|
||||||
|
|
||||||
it(`should not allow decimal quota`, async () => {
|
it(`should not allow decimal quota`, async () => {
|
||||||
const dto: UserAdminCreateDto = {
|
const dto: UserAdminCreateDto = {
|
||||||
email: 'user@immich.app',
|
email: 'user@immich.app',
|
||||||
|
|
@ -75,5 +118,17 @@ describe(UserAdminController.name, () => {
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(errorDto.badRequest(expect.arrayContaining(['quotaSizeInBytes must be an integer number'])));
|
expect(body).toEqual(errorDto.badRequest(expect.arrayContaining(['quotaSizeInBytes must be an integer number'])));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should allow a null pinCode', async () => {
|
||||||
|
const id = factory.uuid();
|
||||||
|
await request(ctx.getHttpServer()).put(`/admin/users/${id}`).send({ pinCode: null });
|
||||||
|
expect(service.update).toHaveBeenCalledWith(undefined, id, expect.objectContaining({ pinCode: null }));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should allow a null avatarColor', async () => {
|
||||||
|
const id = factory.uuid();
|
||||||
|
await request(ctx.getHttpServer()).put(`/admin/users/${id}`).send({ avatarColor: null });
|
||||||
|
expect(service.update).toHaveBeenCalledWith(undefined, id, expect.objectContaining({ avatarColor: null }));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,14 @@ describe(UserController.name, () => {
|
||||||
expect(body).toEqual(errorDto.badRequest());
|
expect(body).toEqual(errorDto.badRequest());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
it('should allow an empty avatarColor', async () => {
|
||||||
|
await request(ctx.getHttpServer())
|
||||||
|
.put(`/users/me`)
|
||||||
|
.set('Authorization', `Bearer token`)
|
||||||
|
.send({ avatarColor: null });
|
||||||
|
expect(service.updateMe).toHaveBeenCalledWith(undefined, expect.objectContaining({ avatarColor: null }));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('GET /users/:id', () => {
|
describe('GET /users/:id', () => {
|
||||||
|
|
|
||||||
|
|
@ -73,8 +73,7 @@ export class AssetBulkUpdateDto extends UpdateAssetBase {
|
||||||
@ValidateUUID({ each: true, description: 'Asset IDs to update' })
|
@ValidateUUID({ each: true, description: 'Asset IDs to update' })
|
||||||
ids!: string[];
|
ids!: string[];
|
||||||
|
|
||||||
@ApiProperty({ description: 'Duplicate asset ID' })
|
@ValidateString({ optional: true, nullable: true, description: 'Duplicate ID' })
|
||||||
@Optional()
|
|
||||||
duplicateId?: string | null;
|
duplicateId?: string | null;
|
||||||
|
|
||||||
@ApiProperty({ description: 'Relative time offset in seconds' })
|
@ApiProperty({ description: 'Relative time offset in seconds' })
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
import { IsString } from 'class-validator';
|
import { ArrayMinSize, IsString } from 'class-validator';
|
||||||
import { NotificationLevel, NotificationType } from 'src/enum';
|
import { NotificationLevel, NotificationType } from 'src/enum';
|
||||||
import { Optional, ValidateBoolean, ValidateDate, ValidateEnum, ValidateUUID } from 'src/validation';
|
import { Optional, ValidateBoolean, ValidateDate, ValidateEnum, ValidateString, ValidateUUID } from 'src/validation';
|
||||||
|
|
||||||
export class TestEmailResponseDto {
|
export class TestEmailResponseDto {
|
||||||
@ApiProperty({ description: 'Email message ID' })
|
@ApiProperty({ description: 'Email message ID' })
|
||||||
|
|
@ -75,20 +75,17 @@ export class NotificationCreateDto {
|
||||||
@ValidateEnum({ enum: NotificationType, name: 'NotificationType', optional: true, description: 'Notification type' })
|
@ValidateEnum({ enum: NotificationType, name: 'NotificationType', optional: true, description: 'Notification type' })
|
||||||
type?: NotificationType;
|
type?: NotificationType;
|
||||||
|
|
||||||
@ApiProperty({ description: 'Notification title' })
|
@ValidateString({ description: 'Notification title' })
|
||||||
@IsString()
|
|
||||||
title!: string;
|
title!: string;
|
||||||
|
|
||||||
@ApiPropertyOptional({ description: 'Notification description' })
|
@ValidateString({ optional: true, nullable: true, description: 'Notification description' })
|
||||||
@IsString()
|
|
||||||
@Optional({ nullable: true })
|
|
||||||
description?: string | null;
|
description?: string | null;
|
||||||
|
|
||||||
@ApiPropertyOptional({ description: 'Additional notification data' })
|
@ApiPropertyOptional({ description: 'Additional notification data' })
|
||||||
@Optional({ nullable: true })
|
@Optional({ nullable: true })
|
||||||
data?: any;
|
data?: any;
|
||||||
|
|
||||||
@ValidateDate({ optional: true, description: 'Date when notification was read' })
|
@ValidateDate({ optional: true, nullable: true, description: 'Date when notification was read' })
|
||||||
readAt?: Date | null;
|
readAt?: Date | null;
|
||||||
|
|
||||||
@ValidateUUID({ description: 'User ID to send notification to' })
|
@ValidateUUID({ description: 'User ID to send notification to' })
|
||||||
|
|
@ -96,20 +93,22 @@ export class NotificationCreateDto {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class NotificationUpdateDto {
|
export class NotificationUpdateDto {
|
||||||
@ValidateDate({ optional: true, description: 'Date when notification was read' })
|
@ValidateDate({ optional: true, nullable: true, description: 'Date when notification was read' })
|
||||||
readAt?: Date | null;
|
readAt?: Date | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class NotificationUpdateAllDto {
|
export class NotificationUpdateAllDto {
|
||||||
@ValidateUUID({ each: true, optional: true, description: 'Notification IDs to update' })
|
@ValidateUUID({ each: true, description: 'Notification IDs to update' })
|
||||||
|
@ArrayMinSize(1)
|
||||||
ids!: string[];
|
ids!: string[];
|
||||||
|
|
||||||
@ValidateDate({ optional: true, description: 'Date when notifications were read' })
|
@ValidateDate({ optional: true, nullable: true, description: 'Date when notifications were read' })
|
||||||
readAt?: Date | null;
|
readAt?: Date | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class NotificationDeleteAllDto {
|
export class NotificationDeleteAllDto {
|
||||||
@ValidateUUID({ each: true, description: 'Notification IDs to delete' })
|
@ValidateUUID({ each: true, description: 'Notification IDs to delete' })
|
||||||
|
@ArrayMinSize(1)
|
||||||
ids!: string[];
|
ids!: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,7 @@ export class SharedLinkCreateDto {
|
||||||
@IsString()
|
@IsString()
|
||||||
slug?: string | null;
|
slug?: string | null;
|
||||||
|
|
||||||
@ValidateDate({ optional: true, description: 'Expiration date' })
|
@ValidateDate({ optional: true, nullable: true, description: 'Expiration date' })
|
||||||
expiresAt?: Date | null = null;
|
expiresAt?: Date | null = null;
|
||||||
|
|
||||||
@ValidateBoolean({ optional: true, description: 'Allow uploads' })
|
@ValidateBoolean({ optional: true, description: 'Allow uploads' })
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ export class TagCreateDto {
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
name!: string;
|
name!: string;
|
||||||
|
|
||||||
@ValidateUUID({ optional: true, description: 'Parent tag ID' })
|
@ValidateUUID({ nullable: true, optional: true, description: 'Parent tag ID' })
|
||||||
parentId?: string | null;
|
parentId?: string | null;
|
||||||
|
|
||||||
@ApiPropertyOptional({ description: 'Tag color (hex)' })
|
@ApiPropertyOptional({ description: 'Tag color (hex)' })
|
||||||
|
|
@ -20,7 +20,7 @@ export class TagCreateDto {
|
||||||
|
|
||||||
export class TagUpdateDto {
|
export class TagUpdateDto {
|
||||||
@ApiPropertyOptional({ description: 'Tag color (hex)' })
|
@ApiPropertyOptional({ description: 'Tag color (hex)' })
|
||||||
@Optional({ emptyToNull: true })
|
@Optional({ nullable: true, emptyToNull: true })
|
||||||
@ValidateHexColor()
|
@ValidateHexColor()
|
||||||
color?: string | null;
|
color?: string | null;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,13 @@ export class UserUpdateMeDto {
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
name?: string;
|
name?: string;
|
||||||
|
|
||||||
@ValidateEnum({ enum: UserAvatarColor, name: 'UserAvatarColor', optional: true, description: 'Avatar color' })
|
@ValidateEnum({
|
||||||
|
enum: UserAvatarColor,
|
||||||
|
name: 'UserAvatarColor',
|
||||||
|
optional: true,
|
||||||
|
nullable: true,
|
||||||
|
description: 'Avatar color',
|
||||||
|
})
|
||||||
avatarColor?: UserAvatarColor | null;
|
avatarColor?: UserAvatarColor | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -96,9 +102,19 @@ export class UserAdminCreateDto {
|
||||||
@IsString()
|
@IsString()
|
||||||
name!: string;
|
name!: string;
|
||||||
|
|
||||||
@ValidateEnum({ enum: UserAvatarColor, name: 'UserAvatarColor', optional: true, description: 'Avatar color' })
|
@ValidateEnum({
|
||||||
|
enum: UserAvatarColor,
|
||||||
|
name: 'UserAvatarColor',
|
||||||
|
optional: true,
|
||||||
|
nullable: true,
|
||||||
|
description: 'Avatar color',
|
||||||
|
})
|
||||||
avatarColor?: UserAvatarColor | null;
|
avatarColor?: UserAvatarColor | null;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: 'PIN code' })
|
||||||
|
@PinCode({ optional: true, nullable: true, emptyToNull: true })
|
||||||
|
pinCode?: string | null;
|
||||||
|
|
||||||
@ApiPropertyOptional({ description: 'Storage label' })
|
@ApiPropertyOptional({ description: 'Storage label' })
|
||||||
@Optional({ nullable: true })
|
@Optional({ nullable: true })
|
||||||
@IsString()
|
@IsString()
|
||||||
|
|
@ -135,7 +151,7 @@ export class UserAdminUpdateDto {
|
||||||
password?: string;
|
password?: string;
|
||||||
|
|
||||||
@ApiPropertyOptional({ description: 'PIN code' })
|
@ApiPropertyOptional({ description: 'PIN code' })
|
||||||
@PinCode({ optional: true, emptyToNull: true })
|
@PinCode({ optional: true, nullable: true, emptyToNull: true })
|
||||||
pinCode?: string | null;
|
pinCode?: string | null;
|
||||||
|
|
||||||
@ApiPropertyOptional({ description: 'User name' })
|
@ApiPropertyOptional({ description: 'User name' })
|
||||||
|
|
@ -144,7 +160,13 @@ export class UserAdminUpdateDto {
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
name?: string;
|
name?: string;
|
||||||
|
|
||||||
@ValidateEnum({ enum: UserAvatarColor, name: 'UserAvatarColor', optional: true, description: 'Avatar color' })
|
@ValidateEnum({
|
||||||
|
enum: UserAvatarColor,
|
||||||
|
name: 'UserAvatarColor',
|
||||||
|
optional: true,
|
||||||
|
nullable: true,
|
||||||
|
description: 'Avatar color',
|
||||||
|
})
|
||||||
avatarColor?: UserAvatarColor | null;
|
avatarColor?: UserAvatarColor | null;
|
||||||
|
|
||||||
@ApiPropertyOptional({ description: 'Storage label' })
|
@ApiPropertyOptional({ description: 'Storage label' })
|
||||||
|
|
|
||||||
|
|
@ -232,19 +232,20 @@ export const ValidateHexColor = () => {
|
||||||
return applyDecorators(...decorators);
|
return applyDecorators(...decorators);
|
||||||
};
|
};
|
||||||
|
|
||||||
type DateOptions = { optional?: boolean; nullable?: boolean; format?: 'date' | 'date-time' };
|
type DateOptions = OptionalOptions & { optional?: boolean; format?: 'date' | 'date-time' };
|
||||||
export const ValidateDate = (options?: DateOptions & ApiPropertyOptions) => {
|
export const ValidateDate = (options?: DateOptions & ApiPropertyOptions) => {
|
||||||
const { optional, nullable, format, ...apiPropertyOptions } = {
|
const {
|
||||||
optional: false,
|
optional,
|
||||||
nullable: false,
|
nullable = false,
|
||||||
format: 'date-time',
|
emptyToNull = false,
|
||||||
...options,
|
format = 'date-time',
|
||||||
};
|
...apiPropertyOptions
|
||||||
|
} = options || {};
|
||||||
|
|
||||||
const decorators = [
|
return applyDecorators(
|
||||||
ApiProperty({ format, ...apiPropertyOptions }),
|
ApiProperty({ format, ...apiPropertyOptions }),
|
||||||
IsDate(),
|
IsDate(),
|
||||||
optional ? Optional({ nullable: true }) : IsNotEmpty(),
|
optional ? Optional({ nullable, emptyToNull }) : IsNotEmpty(),
|
||||||
Transform(({ key, value }) => {
|
Transform(({ key, value }) => {
|
||||||
if (value === null || value === undefined) {
|
if (value === null || value === undefined) {
|
||||||
return value;
|
return value;
|
||||||
|
|
@ -256,19 +257,17 @@ export const ValidateDate = (options?: DateOptions & ApiPropertyOptions) => {
|
||||||
|
|
||||||
return new Date(value as string);
|
return new Date(value as string);
|
||||||
}),
|
}),
|
||||||
];
|
);
|
||||||
|
|
||||||
if (optional) {
|
|
||||||
decorators.push(Optional({ nullable }));
|
|
||||||
}
|
|
||||||
|
|
||||||
return applyDecorators(...decorators);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
type StringOptions = { optional?: boolean; nullable?: boolean; trim?: boolean };
|
type StringOptions = OptionalOptions & { optional?: boolean; trim?: boolean };
|
||||||
export const ValidateString = (options?: StringOptions & ApiPropertyOptions) => {
|
export const ValidateString = (options?: StringOptions & ApiPropertyOptions) => {
|
||||||
const { optional, nullable, trim, ...apiPropertyOptions } = options || {};
|
const { optional, nullable, emptyToNull, trim, ...apiPropertyOptions } = options || {};
|
||||||
const decorators = [ApiProperty(apiPropertyOptions), IsString(), optional ? Optional({ nullable }) : IsNotEmpty()];
|
const decorators = [
|
||||||
|
ApiProperty(apiPropertyOptions),
|
||||||
|
IsString(),
|
||||||
|
optional ? Optional({ nullable, emptyToNull }) : IsNotEmpty(),
|
||||||
|
];
|
||||||
|
|
||||||
if (trim) {
|
if (trim) {
|
||||||
decorators.push(Transform(({ value }: { value: string }) => value?.trim()));
|
decorators.push(Transform(({ value }: { value: string }) => value?.trim()));
|
||||||
|
|
@ -277,9 +276,9 @@ export const ValidateString = (options?: StringOptions & ApiPropertyOptions) =>
|
||||||
return applyDecorators(...decorators);
|
return applyDecorators(...decorators);
|
||||||
};
|
};
|
||||||
|
|
||||||
type BooleanOptions = { optional?: boolean; nullable?: boolean };
|
type BooleanOptions = OptionalOptions & { optional?: boolean };
|
||||||
export const ValidateBoolean = (options?: BooleanOptions & PropertyOptions) => {
|
export const ValidateBoolean = (options?: BooleanOptions & PropertyOptions) => {
|
||||||
const { optional, nullable, ...apiPropertyOptions } = options || {};
|
const { optional, nullable, emptyToNull, ...apiPropertyOptions } = options || {};
|
||||||
const decorators = [
|
const decorators = [
|
||||||
Property(apiPropertyOptions),
|
Property(apiPropertyOptions),
|
||||||
IsBoolean(),
|
IsBoolean(),
|
||||||
|
|
@ -291,7 +290,7 @@ export const ValidateBoolean = (options?: BooleanOptions & PropertyOptions) => {
|
||||||
}
|
}
|
||||||
return value;
|
return value;
|
||||||
}),
|
}),
|
||||||
optional ? Optional({ nullable }) : IsNotEmpty(),
|
optional ? Optional({ nullable, emptyToNull }) : IsNotEmpty(),
|
||||||
];
|
];
|
||||||
|
|
||||||
return applyDecorators(...decorators);
|
return applyDecorators(...decorators);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue