mirror of
https://github.com/immich-app/immich.git
synced 2025-01-01 08:31:59 +00:00
refactor(server): move timeline operations to their own controller/service (#8325)
* move timeline operations to their own controller/service * chore: open api * move e2e tests
This commit is contained in:
parent
b8b3c487d4
commit
b8c5363a15
20 changed files with 817 additions and 700 deletions
|
@ -5,7 +5,6 @@ import {
|
||||||
LibraryResponseDto,
|
LibraryResponseDto,
|
||||||
LoginResponseDto,
|
LoginResponseDto,
|
||||||
SharedLinkType,
|
SharedLinkType,
|
||||||
TimeBucketSize,
|
|
||||||
getAllLibraries,
|
getAllLibraries,
|
||||||
getAssetInfo,
|
getAssetInfo,
|
||||||
updateAssets,
|
updateAssets,
|
||||||
|
@ -942,146 +941,6 @@ describe('/asset', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('GET /asset/time-buckets', () => {
|
|
||||||
it('should require authentication', async () => {
|
|
||||||
const { status, body } = await request(app).get('/asset/time-buckets').query({ size: TimeBucketSize.Month });
|
|
||||||
expect(status).toBe(401);
|
|
||||||
expect(body).toEqual(errorDto.unauthorized);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should get time buckets by month', async () => {
|
|
||||||
const { status, body } = await request(app)
|
|
||||||
.get('/asset/time-buckets')
|
|
||||||
.set('Authorization', `Bearer ${timeBucketUser.accessToken}`)
|
|
||||||
.query({ size: TimeBucketSize.Month });
|
|
||||||
|
|
||||||
expect(status).toBe(200);
|
|
||||||
expect(body).toEqual(
|
|
||||||
expect.arrayContaining([
|
|
||||||
{ count: 3, timeBucket: '1970-02-01T00:00:00.000Z' },
|
|
||||||
{ count: 1, timeBucket: '1970-01-01T00:00:00.000Z' },
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not allow access for unrelated shared links', async () => {
|
|
||||||
const sharedLink = await utils.createSharedLink(user1.accessToken, {
|
|
||||||
type: SharedLinkType.Individual,
|
|
||||||
assetIds: user1Assets.map(({ id }) => id),
|
|
||||||
});
|
|
||||||
|
|
||||||
const { status, body } = await request(app)
|
|
||||||
.get('/asset/time-buckets')
|
|
||||||
.query({ key: sharedLink.key, size: TimeBucketSize.Month });
|
|
||||||
|
|
||||||
expect(status).toBe(400);
|
|
||||||
expect(body).toEqual(errorDto.noPermission);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should get time buckets by day', async () => {
|
|
||||||
const { status, body } = await request(app)
|
|
||||||
.get('/asset/time-buckets')
|
|
||||||
.set('Authorization', `Bearer ${timeBucketUser.accessToken}`)
|
|
||||||
.query({ size: TimeBucketSize.Day });
|
|
||||||
|
|
||||||
expect(status).toBe(200);
|
|
||||||
expect(body).toEqual([
|
|
||||||
{ count: 2, timeBucket: '1970-02-11T00:00:00.000Z' },
|
|
||||||
{ count: 1, timeBucket: '1970-02-10T00:00:00.000Z' },
|
|
||||||
{ count: 1, timeBucket: '1970-01-01T00:00:00.000Z' },
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('GET /asset/time-bucket', () => {
|
|
||||||
it('should require authentication', async () => {
|
|
||||||
const { status, body } = await request(app).get('/asset/time-bucket').query({
|
|
||||||
size: TimeBucketSize.Month,
|
|
||||||
timeBucket: '1900-01-01T00:00:00.000Z',
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(status).toBe(401);
|
|
||||||
expect(body).toEqual(errorDto.unauthorized);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle 5 digit years', async () => {
|
|
||||||
const { status, body } = await request(app)
|
|
||||||
.get('/asset/time-bucket')
|
|
||||||
.query({ size: TimeBucketSize.Month, timeBucket: '+012345-01-01T00:00:00.000Z' })
|
|
||||||
.set('Authorization', `Bearer ${timeBucketUser.accessToken}`);
|
|
||||||
|
|
||||||
expect(status).toBe(200);
|
|
||||||
expect(body).toEqual([]);
|
|
||||||
});
|
|
||||||
|
|
||||||
// TODO enable date string validation while still accepting 5 digit years
|
|
||||||
// it('should fail if time bucket is invalid', async () => {
|
|
||||||
// const { status, body } = await request(app)
|
|
||||||
// .get('/asset/time-bucket')
|
|
||||||
// .set('Authorization', `Bearer ${user1.accessToken}`)
|
|
||||||
// .query({ size: TimeBucketSize.Month, timeBucket: 'foo' });
|
|
||||||
|
|
||||||
// expect(status).toBe(400);
|
|
||||||
// expect(body).toEqual(errorDto.badRequest);
|
|
||||||
// });
|
|
||||||
|
|
||||||
it('should return time bucket', async () => {
|
|
||||||
const { status, body } = await request(app)
|
|
||||||
.get('/asset/time-bucket')
|
|
||||||
.set('Authorization', `Bearer ${timeBucketUser.accessToken}`)
|
|
||||||
.query({ size: TimeBucketSize.Month, timeBucket: '1970-02-10T00:00:00.000Z' });
|
|
||||||
|
|
||||||
expect(status).toBe(200);
|
|
||||||
expect(body).toEqual([]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return error if time bucket is requested with partners asset and archived', async () => {
|
|
||||||
const req1 = await request(app)
|
|
||||||
.get('/asset/time-buckets')
|
|
||||||
.set('Authorization', `Bearer ${timeBucketUser.accessToken}`)
|
|
||||||
.query({ size: TimeBucketSize.Month, withPartners: true, isArchived: true });
|
|
||||||
|
|
||||||
expect(req1.status).toBe(400);
|
|
||||||
expect(req1.body).toEqual(errorDto.badRequest());
|
|
||||||
|
|
||||||
const req2 = await request(app)
|
|
||||||
.get('/asset/time-buckets')
|
|
||||||
.set('Authorization', `Bearer ${user1.accessToken}`)
|
|
||||||
.query({ size: TimeBucketSize.Month, withPartners: true, isArchived: undefined });
|
|
||||||
|
|
||||||
expect(req2.status).toBe(400);
|
|
||||||
expect(req2.body).toEqual(errorDto.badRequest());
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return error if time bucket is requested with partners asset and favorite', async () => {
|
|
||||||
const req1 = await request(app)
|
|
||||||
.get('/asset/time-buckets')
|
|
||||||
.set('Authorization', `Bearer ${timeBucketUser.accessToken}`)
|
|
||||||
.query({ size: TimeBucketSize.Month, withPartners: true, isFavorite: true });
|
|
||||||
|
|
||||||
expect(req1.status).toBe(400);
|
|
||||||
expect(req1.body).toEqual(errorDto.badRequest());
|
|
||||||
|
|
||||||
const req2 = await request(app)
|
|
||||||
.get('/asset/time-buckets')
|
|
||||||
.set('Authorization', `Bearer ${timeBucketUser.accessToken}`)
|
|
||||||
.query({ size: TimeBucketSize.Month, withPartners: true, isFavorite: false });
|
|
||||||
|
|
||||||
expect(req2.status).toBe(400);
|
|
||||||
expect(req2.body).toEqual(errorDto.badRequest());
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return error if time bucket is requested with partners asset and trash', async () => {
|
|
||||||
const req = await request(app)
|
|
||||||
.get('/asset/time-buckets')
|
|
||||||
.set('Authorization', `Bearer ${user1.accessToken}`)
|
|
||||||
.query({ size: TimeBucketSize.Month, withPartners: true, isTrashed: true });
|
|
||||||
|
|
||||||
expect(req.status).toBe(400);
|
|
||||||
expect(req.body).toEqual(errorDto.badRequest());
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('GET /asset', () => {
|
describe('GET /asset', () => {
|
||||||
it('should return stack data', async () => {
|
it('should return stack data', async () => {
|
||||||
const { status, body } = await request(app).get('/asset').set('Authorization', `Bearer ${stackUser.accessToken}`);
|
const { status, body } = await request(app).get('/asset').set('Authorization', `Bearer ${stackUser.accessToken}`);
|
||||||
|
|
193
e2e/src/api/specs/timeline.e2e-spec.ts
Normal file
193
e2e/src/api/specs/timeline.e2e-spec.ts
Normal file
|
@ -0,0 +1,193 @@
|
||||||
|
import { AssetFileUploadResponseDto, LoginResponseDto, SharedLinkType, TimeBucketSize } from '@immich/sdk';
|
||||||
|
import { DateTime } from 'luxon';
|
||||||
|
import { createUserDto } from 'src/fixtures';
|
||||||
|
import { errorDto } from 'src/responses';
|
||||||
|
import { app, utils } from 'src/utils';
|
||||||
|
import request from 'supertest';
|
||||||
|
import { beforeAll, describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
// TODO this should probably be a test util function
|
||||||
|
const today = DateTime.fromObject({
|
||||||
|
year: 2023,
|
||||||
|
month: 11,
|
||||||
|
day: 3,
|
||||||
|
}) as DateTime<true>;
|
||||||
|
const yesterday = today.minus({ days: 1 });
|
||||||
|
|
||||||
|
describe('/timeline', () => {
|
||||||
|
let admin: LoginResponseDto;
|
||||||
|
let user: LoginResponseDto;
|
||||||
|
let timeBucketUser: LoginResponseDto;
|
||||||
|
|
||||||
|
let userAssets: AssetFileUploadResponseDto[];
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await utils.resetDatabase();
|
||||||
|
admin = await utils.adminSetup({ onboarding: false });
|
||||||
|
[user, timeBucketUser] = await Promise.all([
|
||||||
|
utils.userSetup(admin.accessToken, createUserDto.create('1')),
|
||||||
|
utils.userSetup(admin.accessToken, createUserDto.create('time-bucket')),
|
||||||
|
]);
|
||||||
|
|
||||||
|
userAssets = await Promise.all([
|
||||||
|
utils.createAsset(user.accessToken),
|
||||||
|
utils.createAsset(user.accessToken),
|
||||||
|
utils.createAsset(user.accessToken, {
|
||||||
|
isFavorite: true,
|
||||||
|
isReadOnly: true,
|
||||||
|
fileCreatedAt: yesterday.toISO(),
|
||||||
|
fileModifiedAt: yesterday.toISO(),
|
||||||
|
assetData: { filename: 'example.mp4' },
|
||||||
|
}),
|
||||||
|
utils.createAsset(user.accessToken),
|
||||||
|
utils.createAsset(user.accessToken),
|
||||||
|
]);
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
utils.createAsset(timeBucketUser.accessToken, { fileCreatedAt: new Date('1970-01-01').toISOString() }),
|
||||||
|
utils.createAsset(timeBucketUser.accessToken, { fileCreatedAt: new Date('1970-02-10').toISOString() }),
|
||||||
|
utils.createAsset(timeBucketUser.accessToken, { fileCreatedAt: new Date('1970-02-11').toISOString() }),
|
||||||
|
utils.createAsset(timeBucketUser.accessToken, { fileCreatedAt: new Date('1970-02-11').toISOString() }),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('GET /timeline/buckets', () => {
|
||||||
|
it('should require authentication', async () => {
|
||||||
|
const { status, body } = await request(app).get('/timeline/buckets').query({ size: TimeBucketSize.Month });
|
||||||
|
expect(status).toBe(401);
|
||||||
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should get time buckets by month', async () => {
|
||||||
|
const { status, body } = await request(app)
|
||||||
|
.get('/timeline/buckets')
|
||||||
|
.set('Authorization', `Bearer ${timeBucketUser.accessToken}`)
|
||||||
|
.query({ size: TimeBucketSize.Month });
|
||||||
|
|
||||||
|
expect(status).toBe(200);
|
||||||
|
expect(body).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
{ count: 3, timeBucket: '1970-02-01T00:00:00.000Z' },
|
||||||
|
{ count: 1, timeBucket: '1970-01-01T00:00:00.000Z' },
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not allow access for unrelated shared links', async () => {
|
||||||
|
const sharedLink = await utils.createSharedLink(user.accessToken, {
|
||||||
|
type: SharedLinkType.Individual,
|
||||||
|
assetIds: userAssets.map(({ id }) => id),
|
||||||
|
});
|
||||||
|
|
||||||
|
const { status, body } = await request(app)
|
||||||
|
.get('/timeline/buckets')
|
||||||
|
.query({ key: sharedLink.key, size: TimeBucketSize.Month });
|
||||||
|
|
||||||
|
expect(status).toBe(400);
|
||||||
|
expect(body).toEqual(errorDto.noPermission);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should get time buckets by day', async () => {
|
||||||
|
const { status, body } = await request(app)
|
||||||
|
.get('/timeline/buckets')
|
||||||
|
.set('Authorization', `Bearer ${timeBucketUser.accessToken}`)
|
||||||
|
.query({ size: TimeBucketSize.Day });
|
||||||
|
|
||||||
|
expect(status).toBe(200);
|
||||||
|
expect(body).toEqual([
|
||||||
|
{ count: 2, timeBucket: '1970-02-11T00:00:00.000Z' },
|
||||||
|
{ count: 1, timeBucket: '1970-02-10T00:00:00.000Z' },
|
||||||
|
{ count: 1, timeBucket: '1970-01-01T00:00:00.000Z' },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return error if time bucket is requested with partners asset and archived', async () => {
|
||||||
|
const req1 = await request(app)
|
||||||
|
.get('/timeline/buckets')
|
||||||
|
.set('Authorization', `Bearer ${timeBucketUser.accessToken}`)
|
||||||
|
.query({ size: TimeBucketSize.Month, withPartners: true, isArchived: true });
|
||||||
|
|
||||||
|
expect(req1.status).toBe(400);
|
||||||
|
expect(req1.body).toEqual(errorDto.badRequest());
|
||||||
|
|
||||||
|
const req2 = await request(app)
|
||||||
|
.get('/timeline/buckets')
|
||||||
|
.set('Authorization', `Bearer ${user.accessToken}`)
|
||||||
|
.query({ size: TimeBucketSize.Month, withPartners: true, isArchived: undefined });
|
||||||
|
|
||||||
|
expect(req2.status).toBe(400);
|
||||||
|
expect(req2.body).toEqual(errorDto.badRequest());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return error if time bucket is requested with partners asset and favorite', async () => {
|
||||||
|
const req1 = await request(app)
|
||||||
|
.get('/timeline/buckets')
|
||||||
|
.set('Authorization', `Bearer ${timeBucketUser.accessToken}`)
|
||||||
|
.query({ size: TimeBucketSize.Month, withPartners: true, isFavorite: true });
|
||||||
|
|
||||||
|
expect(req1.status).toBe(400);
|
||||||
|
expect(req1.body).toEqual(errorDto.badRequest());
|
||||||
|
|
||||||
|
const req2 = await request(app)
|
||||||
|
.get('/timeline/buckets')
|
||||||
|
.set('Authorization', `Bearer ${timeBucketUser.accessToken}`)
|
||||||
|
.query({ size: TimeBucketSize.Month, withPartners: true, isFavorite: false });
|
||||||
|
|
||||||
|
expect(req2.status).toBe(400);
|
||||||
|
expect(req2.body).toEqual(errorDto.badRequest());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return error if time bucket is requested with partners asset and trash', async () => {
|
||||||
|
const req = await request(app)
|
||||||
|
.get('/timeline/buckets')
|
||||||
|
.set('Authorization', `Bearer ${user.accessToken}`)
|
||||||
|
.query({ size: TimeBucketSize.Month, withPartners: true, isTrashed: true });
|
||||||
|
|
||||||
|
expect(req.status).toBe(400);
|
||||||
|
expect(req.body).toEqual(errorDto.badRequest());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('GET /timeline/bucket', () => {
|
||||||
|
it('should require authentication', async () => {
|
||||||
|
const { status, body } = await request(app).get('/timeline/bucket').query({
|
||||||
|
size: TimeBucketSize.Month,
|
||||||
|
timeBucket: '1900-01-01T00:00:00.000Z',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(status).toBe(401);
|
||||||
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle 5 digit years', async () => {
|
||||||
|
const { status, body } = await request(app)
|
||||||
|
.get('/timeline/bucket')
|
||||||
|
.query({ size: TimeBucketSize.Month, timeBucket: '+012345-01-01T00:00:00.000Z' })
|
||||||
|
.set('Authorization', `Bearer ${timeBucketUser.accessToken}`);
|
||||||
|
|
||||||
|
expect(status).toBe(200);
|
||||||
|
expect(body).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO enable date string validation while still accepting 5 digit years
|
||||||
|
// it('should fail if time bucket is invalid', async () => {
|
||||||
|
// const { status, body } = await request(app)
|
||||||
|
// .get('/timeline/bucket')
|
||||||
|
// .set('Authorization', `Bearer ${user.accessToken}`)
|
||||||
|
// .query({ size: TimeBucketSize.Month, timeBucket: 'foo' });
|
||||||
|
|
||||||
|
// expect(status).toBe(400);
|
||||||
|
// expect(body).toEqual(errorDto.badRequest);
|
||||||
|
// });
|
||||||
|
|
||||||
|
it('should return time bucket', async () => {
|
||||||
|
const { status, body } = await request(app)
|
||||||
|
.get('/timeline/bucket')
|
||||||
|
.set('Authorization', `Bearer ${timeBucketUser.accessToken}`)
|
||||||
|
.query({ size: TimeBucketSize.Month, timeBucket: '1970-02-10T00:00:00.000Z' });
|
||||||
|
|
||||||
|
expect(status).toBe(200);
|
||||||
|
expect(body).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
3
mobile/openapi/.openapi-generator/FILES
generated
3
mobile/openapi/.openapi-generator/FILES
generated
|
@ -169,6 +169,7 @@ doc/TagTypeEnum.md
|
||||||
doc/ThumbnailFormat.md
|
doc/ThumbnailFormat.md
|
||||||
doc/TimeBucketResponseDto.md
|
doc/TimeBucketResponseDto.md
|
||||||
doc/TimeBucketSize.md
|
doc/TimeBucketSize.md
|
||||||
|
doc/TimelineApi.md
|
||||||
doc/ToneMapping.md
|
doc/ToneMapping.md
|
||||||
doc/TranscodeHWAccel.md
|
doc/TranscodeHWAccel.md
|
||||||
doc/TranscodePolicy.md
|
doc/TranscodePolicy.md
|
||||||
|
@ -211,6 +212,7 @@ lib/api/server_info_api.dart
|
||||||
lib/api/shared_link_api.dart
|
lib/api/shared_link_api.dart
|
||||||
lib/api/system_config_api.dart
|
lib/api/system_config_api.dart
|
||||||
lib/api/tag_api.dart
|
lib/api/tag_api.dart
|
||||||
|
lib/api/timeline_api.dart
|
||||||
lib/api/trash_api.dart
|
lib/api/trash_api.dart
|
||||||
lib/api/user_api.dart
|
lib/api/user_api.dart
|
||||||
lib/api_client.dart
|
lib/api_client.dart
|
||||||
|
@ -556,6 +558,7 @@ test/tag_type_enum_test.dart
|
||||||
test/thumbnail_format_test.dart
|
test/thumbnail_format_test.dart
|
||||||
test/time_bucket_response_dto_test.dart
|
test/time_bucket_response_dto_test.dart
|
||||||
test/time_bucket_size_test.dart
|
test/time_bucket_size_test.dart
|
||||||
|
test/timeline_api_test.dart
|
||||||
test/tone_mapping_test.dart
|
test/tone_mapping_test.dart
|
||||||
test/transcode_hw_accel_test.dart
|
test/transcode_hw_accel_test.dart
|
||||||
test/transcode_policy_test.dart
|
test/transcode_policy_test.dart
|
||||||
|
|
BIN
mobile/openapi/README.md
generated
BIN
mobile/openapi/README.md
generated
Binary file not shown.
BIN
mobile/openapi/doc/AssetApi.md
generated
BIN
mobile/openapi/doc/AssetApi.md
generated
Binary file not shown.
BIN
mobile/openapi/doc/TimelineApi.md
generated
Normal file
BIN
mobile/openapi/doc/TimelineApi.md
generated
Normal file
Binary file not shown.
BIN
mobile/openapi/lib/api.dart
generated
BIN
mobile/openapi/lib/api.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/api/asset_api.dart
generated
BIN
mobile/openapi/lib/api/asset_api.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/api/timeline_api.dart
generated
Normal file
BIN
mobile/openapi/lib/api/timeline_api.dart
generated
Normal file
Binary file not shown.
BIN
mobile/openapi/test/asset_api_test.dart
generated
BIN
mobile/openapi/test/asset_api_test.dart
generated
Binary file not shown.
BIN
mobile/openapi/test/timeline_api_test.dart
generated
Normal file
BIN
mobile/openapi/test/timeline_api_test.dart
generated
Normal file
Binary file not shown.
|
@ -1720,286 +1720,6 @@
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/asset/time-bucket": {
|
|
||||||
"get": {
|
|
||||||
"operationId": "getTimeBucket",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"name": "albumId",
|
|
||||||
"required": false,
|
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
|
||||||
"format": "uuid",
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "isArchived",
|
|
||||||
"required": false,
|
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
|
||||||
"type": "boolean"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "isFavorite",
|
|
||||||
"required": false,
|
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
|
||||||
"type": "boolean"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "isTrashed",
|
|
||||||
"required": false,
|
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
|
||||||
"type": "boolean"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "key",
|
|
||||||
"required": false,
|
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "order",
|
|
||||||
"required": false,
|
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/AssetOrder"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "personId",
|
|
||||||
"required": false,
|
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
|
||||||
"format": "uuid",
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "size",
|
|
||||||
"required": true,
|
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/TimeBucketSize"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "timeBucket",
|
|
||||||
"required": true,
|
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "userId",
|
|
||||||
"required": false,
|
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
|
||||||
"format": "uuid",
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "withPartners",
|
|
||||||
"required": false,
|
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
|
||||||
"type": "boolean"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "withStacked",
|
|
||||||
"required": false,
|
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
|
||||||
"type": "boolean"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"items": {
|
|
||||||
"$ref": "#/components/schemas/AssetResponseDto"
|
|
||||||
},
|
|
||||||
"type": "array"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"description": ""
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"security": [
|
|
||||||
{
|
|
||||||
"bearer": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"cookie": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"api_key": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"bearer": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"cookie": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"api_key": []
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"tags": [
|
|
||||||
"Asset"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/asset/time-buckets": {
|
|
||||||
"get": {
|
|
||||||
"operationId": "getTimeBuckets",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"name": "albumId",
|
|
||||||
"required": false,
|
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
|
||||||
"format": "uuid",
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "isArchived",
|
|
||||||
"required": false,
|
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
|
||||||
"type": "boolean"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "isFavorite",
|
|
||||||
"required": false,
|
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
|
||||||
"type": "boolean"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "isTrashed",
|
|
||||||
"required": false,
|
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
|
||||||
"type": "boolean"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "key",
|
|
||||||
"required": false,
|
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "order",
|
|
||||||
"required": false,
|
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/AssetOrder"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "personId",
|
|
||||||
"required": false,
|
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
|
||||||
"format": "uuid",
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "size",
|
|
||||||
"required": true,
|
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/TimeBucketSize"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "userId",
|
|
||||||
"required": false,
|
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
|
||||||
"format": "uuid",
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "withPartners",
|
|
||||||
"required": false,
|
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
|
||||||
"type": "boolean"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "withStacked",
|
|
||||||
"required": false,
|
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
|
||||||
"type": "boolean"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"items": {
|
|
||||||
"$ref": "#/components/schemas/TimeBucketResponseDto"
|
|
||||||
},
|
|
||||||
"type": "array"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"description": ""
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"security": [
|
|
||||||
{
|
|
||||||
"bearer": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"cookie": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"api_key": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"bearer": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"cookie": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"api_key": []
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"tags": [
|
|
||||||
"Asset"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/asset/upload": {
|
"/asset/upload": {
|
||||||
"post": {
|
"post": {
|
||||||
"operationId": "uploadFile",
|
"operationId": "uploadFile",
|
||||||
|
@ -6048,6 +5768,286 @@
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/timeline/bucket": {
|
||||||
|
"get": {
|
||||||
|
"operationId": "getTimeBucket",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "albumId",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"format": "uuid",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "isArchived",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "isFavorite",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "isTrashed",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "key",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "order",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/AssetOrder"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "personId",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"format": "uuid",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "size",
|
||||||
|
"required": true,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/TimeBucketSize"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "timeBucket",
|
||||||
|
"required": true,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "userId",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"format": "uuid",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "withPartners",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "withStacked",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/AssetResponseDto"
|
||||||
|
},
|
||||||
|
"type": "array"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"bearer": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cookie": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"api_key": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bearer": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cookie": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"api_key": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Timeline"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/timeline/buckets": {
|
||||||
|
"get": {
|
||||||
|
"operationId": "getTimeBuckets",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "albumId",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"format": "uuid",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "isArchived",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "isFavorite",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "isTrashed",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "key",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "order",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/AssetOrder"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "personId",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"format": "uuid",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "size",
|
||||||
|
"required": true,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/TimeBucketSize"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "userId",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"format": "uuid",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "withPartners",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "withStacked",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/TimeBucketResponseDto"
|
||||||
|
},
|
||||||
|
"type": "array"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"bearer": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cookie": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"api_key": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bearer": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cookie": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"api_key": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Timeline"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
"/trash/empty": {
|
"/trash/empty": {
|
||||||
"post": {
|
"post": {
|
||||||
"operationId": "emptyTrash",
|
"operationId": "emptyTrash",
|
||||||
|
|
|
@ -284,10 +284,6 @@ export type AssetStatsResponseDto = {
|
||||||
total: number;
|
total: number;
|
||||||
videos: number;
|
videos: number;
|
||||||
};
|
};
|
||||||
export type TimeBucketResponseDto = {
|
|
||||||
count: number;
|
|
||||||
timeBucket: string;
|
|
||||||
};
|
|
||||||
export type CreateAssetDto = {
|
export type CreateAssetDto = {
|
||||||
assetData: Blob;
|
assetData: Blob;
|
||||||
deviceAssetId: string;
|
deviceAssetId: string;
|
||||||
|
@ -971,6 +967,10 @@ export type CreateTagDto = {
|
||||||
export type UpdateTagDto = {
|
export type UpdateTagDto = {
|
||||||
name?: string;
|
name?: string;
|
||||||
};
|
};
|
||||||
|
export type TimeBucketResponseDto = {
|
||||||
|
count: number;
|
||||||
|
timeBucket: string;
|
||||||
|
};
|
||||||
export type CreateUserDto = {
|
export type CreateUserDto = {
|
||||||
email: string;
|
email: string;
|
||||||
memoriesEnabled?: boolean;
|
memoriesEnabled?: boolean;
|
||||||
|
@ -1456,72 +1456,6 @@ export function getAssetThumbnail({ format, id, key }: {
|
||||||
...opts
|
...opts
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
export function getTimeBucket({ albumId, isArchived, isFavorite, isTrashed, key, order, personId, size, timeBucket, userId, withPartners, withStacked }: {
|
|
||||||
albumId?: string;
|
|
||||||
isArchived?: boolean;
|
|
||||||
isFavorite?: boolean;
|
|
||||||
isTrashed?: boolean;
|
|
||||||
key?: string;
|
|
||||||
order?: AssetOrder;
|
|
||||||
personId?: string;
|
|
||||||
size: TimeBucketSize;
|
|
||||||
timeBucket: string;
|
|
||||||
userId?: string;
|
|
||||||
withPartners?: boolean;
|
|
||||||
withStacked?: boolean;
|
|
||||||
}, opts?: Oazapfts.RequestOpts) {
|
|
||||||
return oazapfts.ok(oazapfts.fetchJson<{
|
|
||||||
status: 200;
|
|
||||||
data: AssetResponseDto[];
|
|
||||||
}>(`/asset/time-bucket${QS.query(QS.explode({
|
|
||||||
albumId,
|
|
||||||
isArchived,
|
|
||||||
isFavorite,
|
|
||||||
isTrashed,
|
|
||||||
key,
|
|
||||||
order,
|
|
||||||
personId,
|
|
||||||
size,
|
|
||||||
timeBucket,
|
|
||||||
userId,
|
|
||||||
withPartners,
|
|
||||||
withStacked
|
|
||||||
}))}`, {
|
|
||||||
...opts
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
export function getTimeBuckets({ albumId, isArchived, isFavorite, isTrashed, key, order, personId, size, userId, withPartners, withStacked }: {
|
|
||||||
albumId?: string;
|
|
||||||
isArchived?: boolean;
|
|
||||||
isFavorite?: boolean;
|
|
||||||
isTrashed?: boolean;
|
|
||||||
key?: string;
|
|
||||||
order?: AssetOrder;
|
|
||||||
personId?: string;
|
|
||||||
size: TimeBucketSize;
|
|
||||||
userId?: string;
|
|
||||||
withPartners?: boolean;
|
|
||||||
withStacked?: boolean;
|
|
||||||
}, opts?: Oazapfts.RequestOpts) {
|
|
||||||
return oazapfts.ok(oazapfts.fetchJson<{
|
|
||||||
status: 200;
|
|
||||||
data: TimeBucketResponseDto[];
|
|
||||||
}>(`/asset/time-buckets${QS.query(QS.explode({
|
|
||||||
albumId,
|
|
||||||
isArchived,
|
|
||||||
isFavorite,
|
|
||||||
isTrashed,
|
|
||||||
key,
|
|
||||||
order,
|
|
||||||
personId,
|
|
||||||
size,
|
|
||||||
userId,
|
|
||||||
withPartners,
|
|
||||||
withStacked
|
|
||||||
}))}`, {
|
|
||||||
...opts
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
export function uploadFile({ key, createAssetDto }: {
|
export function uploadFile({ key, createAssetDto }: {
|
||||||
key?: string;
|
key?: string;
|
||||||
createAssetDto: CreateAssetDto;
|
createAssetDto: CreateAssetDto;
|
||||||
|
@ -2595,6 +2529,72 @@ export function tagAssets({ id, assetIdsDto }: {
|
||||||
body: assetIdsDto
|
body: assetIdsDto
|
||||||
})));
|
})));
|
||||||
}
|
}
|
||||||
|
export function getTimeBucket({ albumId, isArchived, isFavorite, isTrashed, key, order, personId, size, timeBucket, userId, withPartners, withStacked }: {
|
||||||
|
albumId?: string;
|
||||||
|
isArchived?: boolean;
|
||||||
|
isFavorite?: boolean;
|
||||||
|
isTrashed?: boolean;
|
||||||
|
key?: string;
|
||||||
|
order?: AssetOrder;
|
||||||
|
personId?: string;
|
||||||
|
size: TimeBucketSize;
|
||||||
|
timeBucket: string;
|
||||||
|
userId?: string;
|
||||||
|
withPartners?: boolean;
|
||||||
|
withStacked?: boolean;
|
||||||
|
}, opts?: Oazapfts.RequestOpts) {
|
||||||
|
return oazapfts.ok(oazapfts.fetchJson<{
|
||||||
|
status: 200;
|
||||||
|
data: AssetResponseDto[];
|
||||||
|
}>(`/timeline/bucket${QS.query(QS.explode({
|
||||||
|
albumId,
|
||||||
|
isArchived,
|
||||||
|
isFavorite,
|
||||||
|
isTrashed,
|
||||||
|
key,
|
||||||
|
order,
|
||||||
|
personId,
|
||||||
|
size,
|
||||||
|
timeBucket,
|
||||||
|
userId,
|
||||||
|
withPartners,
|
||||||
|
withStacked
|
||||||
|
}))}`, {
|
||||||
|
...opts
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
export function getTimeBuckets({ albumId, isArchived, isFavorite, isTrashed, key, order, personId, size, userId, withPartners, withStacked }: {
|
||||||
|
albumId?: string;
|
||||||
|
isArchived?: boolean;
|
||||||
|
isFavorite?: boolean;
|
||||||
|
isTrashed?: boolean;
|
||||||
|
key?: string;
|
||||||
|
order?: AssetOrder;
|
||||||
|
personId?: string;
|
||||||
|
size: TimeBucketSize;
|
||||||
|
userId?: string;
|
||||||
|
withPartners?: boolean;
|
||||||
|
withStacked?: boolean;
|
||||||
|
}, opts?: Oazapfts.RequestOpts) {
|
||||||
|
return oazapfts.ok(oazapfts.fetchJson<{
|
||||||
|
status: 200;
|
||||||
|
data: TimeBucketResponseDto[];
|
||||||
|
}>(`/timeline/buckets${QS.query(QS.explode({
|
||||||
|
albumId,
|
||||||
|
isArchived,
|
||||||
|
isFavorite,
|
||||||
|
isTrashed,
|
||||||
|
key,
|
||||||
|
order,
|
||||||
|
personId,
|
||||||
|
size,
|
||||||
|
userId,
|
||||||
|
withPartners,
|
||||||
|
withStacked
|
||||||
|
}))}`, {
|
||||||
|
...opts
|
||||||
|
}));
|
||||||
|
}
|
||||||
export function emptyTrash(opts?: Oazapfts.RequestOpts) {
|
export function emptyTrash(opts?: Oazapfts.RequestOpts) {
|
||||||
return oazapfts.ok(oazapfts.fetchText("/trash/empty", {
|
return oazapfts.ok(oazapfts.fetchText("/trash/empty", {
|
||||||
...opts,
|
...opts,
|
||||||
|
@ -2789,10 +2789,6 @@ export enum ThumbnailFormat {
|
||||||
Jpeg = "JPEG",
|
Jpeg = "JPEG",
|
||||||
Webp = "WEBP"
|
Webp = "WEBP"
|
||||||
}
|
}
|
||||||
export enum TimeBucketSize {
|
|
||||||
Day = "DAY",
|
|
||||||
Month = "MONTH"
|
|
||||||
}
|
|
||||||
export enum EntityType {
|
export enum EntityType {
|
||||||
Asset = "ASSET",
|
Asset = "ASSET",
|
||||||
Album = "ALBUM"
|
Album = "ALBUM"
|
||||||
|
@ -2911,3 +2907,7 @@ export enum MapTheme {
|
||||||
Light = "light",
|
Light = "light",
|
||||||
Dark = "dark"
|
Dark = "dark"
|
||||||
}
|
}
|
||||||
|
export enum TimeBucketSize {
|
||||||
|
Day = "DAY",
|
||||||
|
Month = "MONTH"
|
||||||
|
}
|
||||||
|
|
|
@ -31,6 +31,7 @@ import { ServerInfoController } from 'src/controllers/server-info.controller';
|
||||||
import { SharedLinkController } from 'src/controllers/shared-link.controller';
|
import { SharedLinkController } from 'src/controllers/shared-link.controller';
|
||||||
import { SystemConfigController } from 'src/controllers/system-config.controller';
|
import { SystemConfigController } from 'src/controllers/system-config.controller';
|
||||||
import { TagController } from 'src/controllers/tag.controller';
|
import { TagController } from 'src/controllers/tag.controller';
|
||||||
|
import { TimelineController } from 'src/controllers/timeline.controller';
|
||||||
import { TrashController } from 'src/controllers/trash.controller';
|
import { TrashController } from 'src/controllers/trash.controller';
|
||||||
import { UserController } from 'src/controllers/user.controller';
|
import { UserController } from 'src/controllers/user.controller';
|
||||||
import { databaseConfig } from 'src/database.config';
|
import { databaseConfig } from 'src/database.config';
|
||||||
|
@ -121,6 +122,7 @@ import { StorageTemplateService } from 'src/services/storage-template.service';
|
||||||
import { StorageService } from 'src/services/storage.service';
|
import { StorageService } from 'src/services/storage.service';
|
||||||
import { SystemConfigService } from 'src/services/system-config.service';
|
import { SystemConfigService } from 'src/services/system-config.service';
|
||||||
import { TagService } from 'src/services/tag.service';
|
import { TagService } from 'src/services/tag.service';
|
||||||
|
import { TimelineService } from 'src/services/timeline.service';
|
||||||
import { TrashService } from 'src/services/trash.service';
|
import { TrashService } from 'src/services/trash.service';
|
||||||
import { UserService } from 'src/services/user.service';
|
import { UserService } from 'src/services/user.service';
|
||||||
import { otelConfig } from 'src/utils/instrumentation';
|
import { otelConfig } from 'src/utils/instrumentation';
|
||||||
|
@ -157,6 +159,7 @@ const controllers = [
|
||||||
SharedLinkController,
|
SharedLinkController,
|
||||||
SystemConfigController,
|
SystemConfigController,
|
||||||
TagController,
|
TagController,
|
||||||
|
TimelineController,
|
||||||
TrashController,
|
TrashController,
|
||||||
UserController,
|
UserController,
|
||||||
PersonController,
|
PersonController,
|
||||||
|
@ -189,6 +192,7 @@ const services: Provider[] = [
|
||||||
StorageTemplateService,
|
StorageTemplateService,
|
||||||
SystemConfigService,
|
SystemConfigService,
|
||||||
TagService,
|
TagService,
|
||||||
|
TimelineService,
|
||||||
TrashService,
|
TrashService,
|
||||||
UserService,
|
UserService,
|
||||||
];
|
];
|
||||||
|
|
|
@ -14,7 +14,6 @@ import {
|
||||||
import { AuthDto } from 'src/dtos/auth.dto';
|
import { AuthDto } from 'src/dtos/auth.dto';
|
||||||
import { MapMarkerDto, MapMarkerResponseDto, MemoryLaneDto, MetadataSearchDto } from 'src/dtos/search.dto';
|
import { MapMarkerDto, MapMarkerResponseDto, MemoryLaneDto, MetadataSearchDto } from 'src/dtos/search.dto';
|
||||||
import { UpdateStackParentDto } from 'src/dtos/stack.dto';
|
import { UpdateStackParentDto } from 'src/dtos/stack.dto';
|
||||||
import { TimeBucketAssetDto, TimeBucketDto, TimeBucketResponseDto } from 'src/dtos/time-bucket.dto';
|
|
||||||
import { Auth, Authenticated, SharedLinkRoute } from 'src/middleware/auth.guard';
|
import { Auth, Authenticated, SharedLinkRoute } from 'src/middleware/auth.guard';
|
||||||
import { Route } from 'src/middleware/file-upload.interceptor';
|
import { Route } from 'src/middleware/file-upload.interceptor';
|
||||||
import { AssetService } from 'src/services/asset.service';
|
import { AssetService } from 'src/services/asset.service';
|
||||||
|
@ -71,18 +70,6 @@ export class AssetController {
|
||||||
return this.service.getStatistics(auth, dto);
|
return this.service.getStatistics(auth, dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Authenticated({ isShared: true })
|
|
||||||
@Get('time-buckets')
|
|
||||||
getTimeBuckets(@Auth() auth: AuthDto, @Query() dto: TimeBucketDto): Promise<TimeBucketResponseDto[]> {
|
|
||||||
return this.service.getTimeBuckets(auth, dto);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Authenticated({ isShared: true })
|
|
||||||
@Get('time-bucket')
|
|
||||||
getTimeBucket(@Auth() auth: AuthDto, @Query() dto: TimeBucketAssetDto): Promise<AssetResponseDto[]> {
|
|
||||||
return this.service.getTimeBucket(auth, dto) as Promise<AssetResponseDto[]>;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Post('jobs')
|
@Post('jobs')
|
||||||
@HttpCode(HttpStatus.NO_CONTENT)
|
@HttpCode(HttpStatus.NO_CONTENT)
|
||||||
runAssetJobs(@Auth() auth: AuthDto, @Body() dto: AssetJobsDto): Promise<void> {
|
runAssetJobs(@Auth() auth: AuthDto, @Body() dto: AssetJobsDto): Promise<void> {
|
||||||
|
|
26
server/src/controllers/timeline.controller.ts
Normal file
26
server/src/controllers/timeline.controller.ts
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
import { Controller, Get, Query } from '@nestjs/common';
|
||||||
|
import { ApiTags } from '@nestjs/swagger';
|
||||||
|
import { AssetResponseDto } from 'src/dtos/asset-response.dto';
|
||||||
|
import { AuthDto } from 'src/dtos/auth.dto';
|
||||||
|
import { TimeBucketAssetDto, TimeBucketDto, TimeBucketResponseDto } from 'src/dtos/time-bucket.dto';
|
||||||
|
import { Auth, Authenticated } from 'src/middleware/auth.guard';
|
||||||
|
import { TimelineService } from 'src/services/timeline.service';
|
||||||
|
|
||||||
|
@ApiTags('Timeline')
|
||||||
|
@Controller('timeline')
|
||||||
|
@Authenticated()
|
||||||
|
export class TimelineController {
|
||||||
|
constructor(private service: TimelineService) {}
|
||||||
|
|
||||||
|
@Authenticated({ isShared: true })
|
||||||
|
@Get('buckets')
|
||||||
|
getTimeBuckets(@Auth() auth: AuthDto, @Query() dto: TimeBucketDto): Promise<TimeBucketResponseDto[]> {
|
||||||
|
return this.service.getTimeBuckets(auth, dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Authenticated({ isShared: true })
|
||||||
|
@Get('bucket')
|
||||||
|
getTimeBucket(@Auth() auth: AuthDto, @Query() dto: TimeBucketAssetDto): Promise<AssetResponseDto[]> {
|
||||||
|
return this.service.getTimeBucket(auth, dto) as Promise<AssetResponseDto[]>;
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,7 +4,7 @@ import { mapAsset } from 'src/dtos/asset-response.dto';
|
||||||
import { AssetJobName, AssetStatsResponseDto, UploadFieldName } from 'src/dtos/asset.dto';
|
import { AssetJobName, AssetStatsResponseDto, UploadFieldName } from 'src/dtos/asset.dto';
|
||||||
import { AssetEntity, AssetType } from 'src/entities/asset.entity';
|
import { AssetEntity, AssetType } from 'src/entities/asset.entity';
|
||||||
import { IAssetStackRepository } from 'src/interfaces/asset-stack.interface';
|
import { IAssetStackRepository } from 'src/interfaces/asset-stack.interface';
|
||||||
import { AssetStats, IAssetRepository, TimeBucketSize } from 'src/interfaces/asset.interface';
|
import { AssetStats, IAssetRepository } from 'src/interfaces/asset.interface';
|
||||||
import { ClientEvent, IEventRepository } from 'src/interfaces/event.interface';
|
import { ClientEvent, IEventRepository } from 'src/interfaces/event.interface';
|
||||||
import { IJobRepository, JobItem, JobName } from 'src/interfaces/job.interface';
|
import { IJobRepository, JobItem, JobName } from 'src/interfaces/job.interface';
|
||||||
import { IPartnerRepository } from 'src/interfaces/partner.interface';
|
import { IPartnerRepository } from 'src/interfaces/partner.interface';
|
||||||
|
@ -335,129 +335,6 @@ describe(AssetService.name, () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('getTimeBuckets', () => {
|
|
||||||
it("should return buckets if userId and albumId aren't set", async () => {
|
|
||||||
assetMock.getTimeBuckets.mockResolvedValue([{ timeBucket: 'bucket', count: 1 }]);
|
|
||||||
|
|
||||||
await expect(
|
|
||||||
sut.getTimeBuckets(authStub.admin, {
|
|
||||||
size: TimeBucketSize.DAY,
|
|
||||||
}),
|
|
||||||
).resolves.toEqual(expect.arrayContaining([{ timeBucket: 'bucket', count: 1 }]));
|
|
||||||
expect(assetMock.getTimeBuckets).toBeCalledWith({ size: TimeBucketSize.DAY, userIds: [authStub.admin.user.id] });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('getTimeBucket', () => {
|
|
||||||
it('should return the assets for a album time bucket if user has album.read', async () => {
|
|
||||||
accessMock.album.checkOwnerAccess.mockResolvedValue(new Set(['album-id']));
|
|
||||||
assetMock.getTimeBucket.mockResolvedValue([assetStub.image]);
|
|
||||||
|
|
||||||
await expect(
|
|
||||||
sut.getTimeBucket(authStub.admin, { size: TimeBucketSize.DAY, timeBucket: 'bucket', albumId: 'album-id' }),
|
|
||||||
).resolves.toEqual(expect.arrayContaining([expect.objectContaining({ id: 'asset-id' })]));
|
|
||||||
|
|
||||||
expect(accessMock.album.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['album-id']));
|
|
||||||
expect(assetMock.getTimeBucket).toBeCalledWith('bucket', {
|
|
||||||
size: TimeBucketSize.DAY,
|
|
||||||
timeBucket: 'bucket',
|
|
||||||
albumId: 'album-id',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return the assets for a archive time bucket if user has archive.read', async () => {
|
|
||||||
assetMock.getTimeBucket.mockResolvedValue([assetStub.image]);
|
|
||||||
|
|
||||||
await expect(
|
|
||||||
sut.getTimeBucket(authStub.admin, {
|
|
||||||
size: TimeBucketSize.DAY,
|
|
||||||
timeBucket: 'bucket',
|
|
||||||
isArchived: true,
|
|
||||||
userId: authStub.admin.user.id,
|
|
||||||
}),
|
|
||||||
).resolves.toEqual(expect.arrayContaining([expect.objectContaining({ id: 'asset-id' })]));
|
|
||||||
expect(assetMock.getTimeBucket).toBeCalledWith('bucket', {
|
|
||||||
size: TimeBucketSize.DAY,
|
|
||||||
timeBucket: 'bucket',
|
|
||||||
isArchived: true,
|
|
||||||
userIds: [authStub.admin.user.id],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return the assets for a library time bucket if user has library.read', async () => {
|
|
||||||
assetMock.getTimeBucket.mockResolvedValue([assetStub.image]);
|
|
||||||
|
|
||||||
await expect(
|
|
||||||
sut.getTimeBucket(authStub.admin, {
|
|
||||||
size: TimeBucketSize.DAY,
|
|
||||||
timeBucket: 'bucket',
|
|
||||||
userId: authStub.admin.user.id,
|
|
||||||
}),
|
|
||||||
).resolves.toEqual(expect.arrayContaining([expect.objectContaining({ id: 'asset-id' })]));
|
|
||||||
expect(assetMock.getTimeBucket).toBeCalledWith('bucket', {
|
|
||||||
size: TimeBucketSize.DAY,
|
|
||||||
timeBucket: 'bucket',
|
|
||||||
userIds: [authStub.admin.user.id],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should throw an error if withParners is true and isArchived true or undefined', async () => {
|
|
||||||
await expect(
|
|
||||||
sut.getTimeBucket(authStub.admin, {
|
|
||||||
size: TimeBucketSize.DAY,
|
|
||||||
timeBucket: 'bucket',
|
|
||||||
isArchived: true,
|
|
||||||
withPartners: true,
|
|
||||||
userId: authStub.admin.user.id,
|
|
||||||
}),
|
|
||||||
).rejects.toThrowError(BadRequestException);
|
|
||||||
|
|
||||||
await expect(
|
|
||||||
sut.getTimeBucket(authStub.admin, {
|
|
||||||
size: TimeBucketSize.DAY,
|
|
||||||
timeBucket: 'bucket',
|
|
||||||
isArchived: undefined,
|
|
||||||
withPartners: true,
|
|
||||||
userId: authStub.admin.user.id,
|
|
||||||
}),
|
|
||||||
).rejects.toThrowError(BadRequestException);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should throw an error if withParners is true and isFavorite is either true or false', async () => {
|
|
||||||
await expect(
|
|
||||||
sut.getTimeBucket(authStub.admin, {
|
|
||||||
size: TimeBucketSize.DAY,
|
|
||||||
timeBucket: 'bucket',
|
|
||||||
isFavorite: true,
|
|
||||||
withPartners: true,
|
|
||||||
userId: authStub.admin.user.id,
|
|
||||||
}),
|
|
||||||
).rejects.toThrowError(BadRequestException);
|
|
||||||
|
|
||||||
await expect(
|
|
||||||
sut.getTimeBucket(authStub.admin, {
|
|
||||||
size: TimeBucketSize.DAY,
|
|
||||||
timeBucket: 'bucket',
|
|
||||||
isFavorite: false,
|
|
||||||
withPartners: true,
|
|
||||||
userId: authStub.admin.user.id,
|
|
||||||
}),
|
|
||||||
).rejects.toThrowError(BadRequestException);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should throw an error if withParners is true and isTrash is true', async () => {
|
|
||||||
await expect(
|
|
||||||
sut.getTimeBucket(authStub.admin, {
|
|
||||||
size: TimeBucketSize.DAY,
|
|
||||||
timeBucket: 'bucket',
|
|
||||||
isTrashed: true,
|
|
||||||
withPartners: true,
|
|
||||||
userId: authStub.admin.user.id,
|
|
||||||
}),
|
|
||||||
).rejects.toThrowError(BadRequestException);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('getStatistics', () => {
|
describe('getStatistics', () => {
|
||||||
it('should get the statistics for a user, excluding archived assets', async () => {
|
it('should get the statistics for a user, excluding archived assets', async () => {
|
||||||
assetMock.getStatistics.mockResolvedValue(stats);
|
assetMock.getStatistics.mockResolvedValue(stats);
|
||||||
|
|
|
@ -25,12 +25,11 @@ import {
|
||||||
import { AuthDto } from 'src/dtos/auth.dto';
|
import { AuthDto } from 'src/dtos/auth.dto';
|
||||||
import { MapMarkerDto, MapMarkerResponseDto, MemoryLaneDto } from 'src/dtos/search.dto';
|
import { MapMarkerDto, MapMarkerResponseDto, MemoryLaneDto } from 'src/dtos/search.dto';
|
||||||
import { UpdateStackParentDto } from 'src/dtos/stack.dto';
|
import { UpdateStackParentDto } from 'src/dtos/stack.dto';
|
||||||
import { TimeBucketAssetDto, TimeBucketDto, TimeBucketResponseDto } from 'src/dtos/time-bucket.dto';
|
|
||||||
import { AssetEntity } from 'src/entities/asset.entity';
|
import { AssetEntity } from 'src/entities/asset.entity';
|
||||||
import { LibraryType } from 'src/entities/library.entity';
|
import { LibraryType } from 'src/entities/library.entity';
|
||||||
import { IAccessRepository } from 'src/interfaces/access.interface';
|
import { IAccessRepository } from 'src/interfaces/access.interface';
|
||||||
import { IAssetStackRepository } from 'src/interfaces/asset-stack.interface';
|
import { IAssetStackRepository } from 'src/interfaces/asset-stack.interface';
|
||||||
import { IAssetRepository, TimeBucketOptions } from 'src/interfaces/asset.interface';
|
import { IAssetRepository } from 'src/interfaces/asset.interface';
|
||||||
import { ClientEvent, IEventRepository } from 'src/interfaces/event.interface';
|
import { ClientEvent, IEventRepository } from 'src/interfaces/event.interface';
|
||||||
import {
|
import {
|
||||||
IAssetDeletionJob,
|
IAssetDeletionJob,
|
||||||
|
@ -195,72 +194,6 @@ export class AssetService {
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
private async timeBucketChecks(auth: AuthDto, dto: TimeBucketDto) {
|
|
||||||
if (dto.albumId) {
|
|
||||||
await this.access.requirePermission(auth, Permission.ALBUM_READ, [dto.albumId]);
|
|
||||||
} else {
|
|
||||||
dto.userId = dto.userId || auth.user.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dto.userId) {
|
|
||||||
await this.access.requirePermission(auth, Permission.TIMELINE_READ, [dto.userId]);
|
|
||||||
if (dto.isArchived !== false) {
|
|
||||||
await this.access.requirePermission(auth, Permission.ARCHIVE_READ, [dto.userId]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dto.withPartners) {
|
|
||||||
const requestedArchived = dto.isArchived === true || dto.isArchived === undefined;
|
|
||||||
const requestedFavorite = dto.isFavorite === true || dto.isFavorite === false;
|
|
||||||
const requestedTrash = dto.isTrashed === true;
|
|
||||||
|
|
||||||
if (requestedArchived || requestedFavorite || requestedTrash) {
|
|
||||||
throw new BadRequestException(
|
|
||||||
'withPartners is only supported for non-archived, non-trashed, non-favorited assets',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async getTimeBuckets(auth: AuthDto, dto: TimeBucketDto): Promise<TimeBucketResponseDto[]> {
|
|
||||||
await this.timeBucketChecks(auth, dto);
|
|
||||||
const timeBucketOptions = await this.buildTimeBucketOptions(auth, dto);
|
|
||||||
|
|
||||||
return this.assetRepository.getTimeBuckets(timeBucketOptions);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getTimeBucket(
|
|
||||||
auth: AuthDto,
|
|
||||||
dto: TimeBucketAssetDto,
|
|
||||||
): Promise<AssetResponseDto[] | SanitizedAssetResponseDto[]> {
|
|
||||||
await this.timeBucketChecks(auth, dto);
|
|
||||||
const timeBucketOptions = await this.buildTimeBucketOptions(auth, dto);
|
|
||||||
const assets = await this.assetRepository.getTimeBucket(dto.timeBucket, timeBucketOptions);
|
|
||||||
return !auth.sharedLink || auth.sharedLink?.showExif
|
|
||||||
? assets.map((asset) => mapAsset(asset, { withStack: true, auth }))
|
|
||||||
: assets.map((asset) => mapAsset(asset, { stripMetadata: true, auth }));
|
|
||||||
}
|
|
||||||
|
|
||||||
async buildTimeBucketOptions(auth: AuthDto, dto: TimeBucketDto): Promise<TimeBucketOptions> {
|
|
||||||
const { userId, ...options } = dto;
|
|
||||||
let userIds: string[] | undefined = undefined;
|
|
||||||
|
|
||||||
if (userId) {
|
|
||||||
userIds = [userId];
|
|
||||||
|
|
||||||
if (dto.withPartners) {
|
|
||||||
const partners = await this.partnerRepository.getAll(auth.user.id);
|
|
||||||
const partnersIds = partners
|
|
||||||
.filter((partner) => partner.sharedBy && partner.sharedWith && partner.inTimeline)
|
|
||||||
.map((partner) => partner.sharedById);
|
|
||||||
|
|
||||||
userIds.push(...partnersIds);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return { ...options, userIds };
|
|
||||||
}
|
|
||||||
|
|
||||||
async getStatistics(auth: AuthDto, dto: AssetStatsDto) {
|
async getStatistics(auth: AuthDto, dto: AssetStatsDto) {
|
||||||
const stats = await this.assetRepository.getStatistics(auth.user.id, dto);
|
const stats = await this.assetRepository.getStatistics(auth.user.id, dto);
|
||||||
return mapStats(stats);
|
return mapStats(stats);
|
||||||
|
|
149
server/src/services/timeline.service.spec.ts
Normal file
149
server/src/services/timeline.service.spec.ts
Normal file
|
@ -0,0 +1,149 @@
|
||||||
|
import { BadRequestException } from '@nestjs/common';
|
||||||
|
import { IAssetRepository, TimeBucketSize } from 'src/interfaces/asset.interface';
|
||||||
|
import { IPartnerRepository } from 'src/interfaces/partner.interface';
|
||||||
|
import { TimelineService } from 'src/services/timeline.service';
|
||||||
|
import { assetStub } from 'test/fixtures/asset.stub';
|
||||||
|
import { authStub } from 'test/fixtures/auth.stub';
|
||||||
|
import { IAccessRepositoryMock, newAccessRepositoryMock } from 'test/repositories/access.repository.mock';
|
||||||
|
import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock';
|
||||||
|
import { newPartnerRepositoryMock } from 'test/repositories/partner.repository.mock';
|
||||||
|
|
||||||
|
describe(TimelineService.name, () => {
|
||||||
|
let sut: TimelineService;
|
||||||
|
let accessMock: IAccessRepositoryMock;
|
||||||
|
let assetMock: jest.Mocked<IAssetRepository>;
|
||||||
|
let partnerMock: jest.Mocked<IPartnerRepository>;
|
||||||
|
beforeEach(() => {
|
||||||
|
accessMock = newAccessRepositoryMock();
|
||||||
|
assetMock = newAssetRepositoryMock();
|
||||||
|
partnerMock = newPartnerRepositoryMock();
|
||||||
|
|
||||||
|
sut = new TimelineService(accessMock, assetMock, partnerMock);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getTimeBuckets', () => {
|
||||||
|
it("should return buckets if userId and albumId aren't set", async () => {
|
||||||
|
assetMock.getTimeBuckets.mockResolvedValue([{ timeBucket: 'bucket', count: 1 }]);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
sut.getTimeBuckets(authStub.admin, {
|
||||||
|
size: TimeBucketSize.DAY,
|
||||||
|
}),
|
||||||
|
).resolves.toEqual(expect.arrayContaining([{ timeBucket: 'bucket', count: 1 }]));
|
||||||
|
expect(assetMock.getTimeBuckets).toHaveBeenCalledWith({
|
||||||
|
size: TimeBucketSize.DAY,
|
||||||
|
userIds: [authStub.admin.user.id],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getTimeBucket', () => {
|
||||||
|
it('should return the assets for a album time bucket if user has album.read', async () => {
|
||||||
|
accessMock.album.checkOwnerAccess.mockResolvedValue(new Set(['album-id']));
|
||||||
|
assetMock.getTimeBucket.mockResolvedValue([assetStub.image]);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
sut.getTimeBucket(authStub.admin, { size: TimeBucketSize.DAY, timeBucket: 'bucket', albumId: 'album-id' }),
|
||||||
|
).resolves.toEqual(expect.arrayContaining([expect.objectContaining({ id: 'asset-id' })]));
|
||||||
|
|
||||||
|
expect(accessMock.album.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['album-id']));
|
||||||
|
expect(assetMock.getTimeBucket).toHaveBeenCalledWith('bucket', {
|
||||||
|
size: TimeBucketSize.DAY,
|
||||||
|
timeBucket: 'bucket',
|
||||||
|
albumId: 'album-id',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the assets for a archive time bucket if user has archive.read', async () => {
|
||||||
|
assetMock.getTimeBucket.mockResolvedValue([assetStub.image]);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
sut.getTimeBucket(authStub.admin, {
|
||||||
|
size: TimeBucketSize.DAY,
|
||||||
|
timeBucket: 'bucket',
|
||||||
|
isArchived: true,
|
||||||
|
userId: authStub.admin.user.id,
|
||||||
|
}),
|
||||||
|
).resolves.toEqual(expect.arrayContaining([expect.objectContaining({ id: 'asset-id' })]));
|
||||||
|
expect(assetMock.getTimeBucket).toHaveBeenCalledWith('bucket', {
|
||||||
|
size: TimeBucketSize.DAY,
|
||||||
|
timeBucket: 'bucket',
|
||||||
|
isArchived: true,
|
||||||
|
userIds: [authStub.admin.user.id],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the assets for a library time bucket if user has library.read', async () => {
|
||||||
|
assetMock.getTimeBucket.mockResolvedValue([assetStub.image]);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
sut.getTimeBucket(authStub.admin, {
|
||||||
|
size: TimeBucketSize.DAY,
|
||||||
|
timeBucket: 'bucket',
|
||||||
|
userId: authStub.admin.user.id,
|
||||||
|
}),
|
||||||
|
).resolves.toEqual(expect.arrayContaining([expect.objectContaining({ id: 'asset-id' })]));
|
||||||
|
expect(assetMock.getTimeBucket).toHaveBeenCalledWith('bucket', {
|
||||||
|
size: TimeBucketSize.DAY,
|
||||||
|
timeBucket: 'bucket',
|
||||||
|
userIds: [authStub.admin.user.id],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw an error if withParners is true and isArchived true or undefined', async () => {
|
||||||
|
await expect(
|
||||||
|
sut.getTimeBucket(authStub.admin, {
|
||||||
|
size: TimeBucketSize.DAY,
|
||||||
|
timeBucket: 'bucket',
|
||||||
|
isArchived: true,
|
||||||
|
withPartners: true,
|
||||||
|
userId: authStub.admin.user.id,
|
||||||
|
}),
|
||||||
|
).rejects.toThrow(BadRequestException);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
sut.getTimeBucket(authStub.admin, {
|
||||||
|
size: TimeBucketSize.DAY,
|
||||||
|
timeBucket: 'bucket',
|
||||||
|
isArchived: undefined,
|
||||||
|
withPartners: true,
|
||||||
|
userId: authStub.admin.user.id,
|
||||||
|
}),
|
||||||
|
).rejects.toThrow(BadRequestException);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw an error if withParners is true and isFavorite is either true or false', async () => {
|
||||||
|
await expect(
|
||||||
|
sut.getTimeBucket(authStub.admin, {
|
||||||
|
size: TimeBucketSize.DAY,
|
||||||
|
timeBucket: 'bucket',
|
||||||
|
isFavorite: true,
|
||||||
|
withPartners: true,
|
||||||
|
userId: authStub.admin.user.id,
|
||||||
|
}),
|
||||||
|
).rejects.toThrow(BadRequestException);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
sut.getTimeBucket(authStub.admin, {
|
||||||
|
size: TimeBucketSize.DAY,
|
||||||
|
timeBucket: 'bucket',
|
||||||
|
isFavorite: false,
|
||||||
|
withPartners: true,
|
||||||
|
userId: authStub.admin.user.id,
|
||||||
|
}),
|
||||||
|
).rejects.toThrow(BadRequestException);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw an error if withParners is true and isTrash is true', async () => {
|
||||||
|
await expect(
|
||||||
|
sut.getTimeBucket(authStub.admin, {
|
||||||
|
size: TimeBucketSize.DAY,
|
||||||
|
timeBucket: 'bucket',
|
||||||
|
isTrashed: true,
|
||||||
|
withPartners: true,
|
||||||
|
userId: authStub.admin.user.id,
|
||||||
|
}),
|
||||||
|
).rejects.toThrow(BadRequestException);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
86
server/src/services/timeline.service.ts
Normal file
86
server/src/services/timeline.service.ts
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
import { BadRequestException, Inject } from '@nestjs/common';
|
||||||
|
import { AccessCore, Permission } from 'src/cores/access.core';
|
||||||
|
import { AssetResponseDto, SanitizedAssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto';
|
||||||
|
import { AuthDto } from 'src/dtos/auth.dto';
|
||||||
|
import { TimeBucketAssetDto, TimeBucketDto, TimeBucketResponseDto } from 'src/dtos/time-bucket.dto';
|
||||||
|
import { IAccessRepository } from 'src/interfaces/access.interface';
|
||||||
|
import { IAssetRepository, TimeBucketOptions } from 'src/interfaces/asset.interface';
|
||||||
|
import { IPartnerRepository } from 'src/interfaces/partner.interface';
|
||||||
|
|
||||||
|
export class TimelineService {
|
||||||
|
private accessCore: AccessCore;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@Inject(IAccessRepository) accessRepository: IAccessRepository,
|
||||||
|
@Inject(IAssetRepository) private repository: IAssetRepository,
|
||||||
|
@Inject(IPartnerRepository) private partnerRepository: IPartnerRepository,
|
||||||
|
) {
|
||||||
|
this.accessCore = AccessCore.create(accessRepository);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getTimeBuckets(auth: AuthDto, dto: TimeBucketDto): Promise<TimeBucketResponseDto[]> {
|
||||||
|
await this.timeBucketChecks(auth, dto);
|
||||||
|
const timeBucketOptions = await this.buildTimeBucketOptions(auth, dto);
|
||||||
|
|
||||||
|
return this.repository.getTimeBuckets(timeBucketOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getTimeBucket(
|
||||||
|
auth: AuthDto,
|
||||||
|
dto: TimeBucketAssetDto,
|
||||||
|
): Promise<AssetResponseDto[] | SanitizedAssetResponseDto[]> {
|
||||||
|
await this.timeBucketChecks(auth, dto);
|
||||||
|
const timeBucketOptions = await this.buildTimeBucketOptions(auth, dto);
|
||||||
|
const assets = await this.repository.getTimeBucket(dto.timeBucket, timeBucketOptions);
|
||||||
|
return !auth.sharedLink || auth.sharedLink?.showExif
|
||||||
|
? assets.map((asset) => mapAsset(asset, { withStack: true, auth }))
|
||||||
|
: assets.map((asset) => mapAsset(asset, { stripMetadata: true, auth }));
|
||||||
|
}
|
||||||
|
|
||||||
|
private async buildTimeBucketOptions(auth: AuthDto, dto: TimeBucketDto): Promise<TimeBucketOptions> {
|
||||||
|
const { userId, ...options } = dto;
|
||||||
|
let userIds: string[] | undefined = undefined;
|
||||||
|
|
||||||
|
if (userId) {
|
||||||
|
userIds = [userId];
|
||||||
|
|
||||||
|
if (dto.withPartners) {
|
||||||
|
const partners = await this.partnerRepository.getAll(auth.user.id);
|
||||||
|
const partnersIds = partners
|
||||||
|
.filter((partner) => partner.sharedBy && partner.sharedWith && partner.inTimeline)
|
||||||
|
.map((partner) => partner.sharedById);
|
||||||
|
|
||||||
|
userIds.push(...partnersIds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ...options, userIds };
|
||||||
|
}
|
||||||
|
|
||||||
|
private async timeBucketChecks(auth: AuthDto, dto: TimeBucketDto) {
|
||||||
|
if (dto.albumId) {
|
||||||
|
await this.accessCore.requirePermission(auth, Permission.ALBUM_READ, [dto.albumId]);
|
||||||
|
} else {
|
||||||
|
dto.userId = dto.userId || auth.user.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dto.userId) {
|
||||||
|
await this.accessCore.requirePermission(auth, Permission.TIMELINE_READ, [dto.userId]);
|
||||||
|
if (dto.isArchived !== false) {
|
||||||
|
await this.accessCore.requirePermission(auth, Permission.ARCHIVE_READ, [dto.userId]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dto.withPartners) {
|
||||||
|
const requestedArchived = dto.isArchived === true || dto.isArchived === undefined;
|
||||||
|
const requestedFavorite = dto.isFavorite === true || dto.isFavorite === false;
|
||||||
|
const requestedTrash = dto.isTrashed === true;
|
||||||
|
|
||||||
|
if (requestedArchived || requestedFavorite || requestedTrash) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
'withPartners is only supported for non-archived, non-trashed, non-favorited assets',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue