Compare commits

...

38 commits

Author SHA1 Message Date
Alex Tran
bd865d02b3 merge main 2023-08-16 15:10:59 -05:00
Alex Tran
7bc8918067 merge main 2023-08-16 13:30:20 -05:00
Alex Tran
29e103d96d merge main 2023-08-14 17:50:09 -05:00
Jason Rasmussen
e8fdddf08e
feat: persist people rules 2023-08-14 17:55:55 -04:00
Jason Rasmussen
947132ac77
chore: open api 2023-08-14 17:44:36 -04:00
Jason Rasmussen
b7b3735c40
feat: api validation 2023-08-14 17:44:30 -04:00
Alex Tran
89021ce995 styling 2023-08-14 11:58:42 -05:00
Alex Tran
e00b37f4d2 remove faces from payload 2023-08-14 11:04:23 -05:00
Alex Tran
7e28522cb1 Change to set 2023-08-13 20:35:04 -05:00
Alex Tran
e682de67fa better selection 2023-08-13 16:53:57 -05:00
Alex Tran
1eb3cdca42 api 2023-08-13 10:35:07 -05:00
Alex Tran
e2ad4ac5b3 add faces 2023-08-13 10:11:22 -05:00
Alex Tran
a7b62a5ad3 Merge branch 'main' of github.com:immich-app/immich into dev/smart-album 2023-08-13 09:07:33 -05:00
Alex Tran
9d23d37601 face selection 2023-08-12 23:35:04 -05:00
Alex Tran
66ebdf1556 show faces 2023-08-12 23:21:49 -05:00
Alex Tran
f7eab1cc9c component order 2023-08-12 22:53:48 -05:00
Alex Tran
048dbc83ba layout 2023-08-12 20:25:25 -05:00
Alex Tran
3576d078cf Hooking up events 2023-08-12 18:04:41 -05:00
Alex Tran
9bd2934aa2 event listener 2023-08-12 17:57:53 -05:00
Alex Tran
ac18f7515a layout 2023-08-12 17:26:22 -05:00
Alex Tran
968fc77183 styling 2023-08-12 16:20:25 -05:00
Alex Tran
207049df00 stronk type 2023-08-12 14:55:26 -05:00
Alex Tran
980c6b0dbb ui: Modal work 2023-08-12 10:36:21 -05:00
Jason Rasmussen
ea6bc8fb10
chore: open api 2023-08-11 21:58:29 -04:00
Jason Rasmussen
6e2624da7c
refactor: rule controller 2023-08-11 21:57:55 -04:00
Alex Tran
a37adc0c96 job 2023-08-11 15:27:51 -05:00
Alex Tran
37b4f8455b job 2023-08-11 15:20:12 -05:00
Alex Tran
3c3de8b2af Merge branch 'main' of github.com:immich-app/immich into dev/smart-album 2023-08-11 11:48:24 -05:00
Alex Tran
fa8ce261d8 naming 2023-08-10 23:21:24 -05:00
Alex Tran
69c95e2b73 remove rule 2023-08-10 21:11:14 -05:00
Alex Tran
51ffac5d15 get album response 2023-08-10 21:00:36 -05:00
Alex Tran
70cf100407 dev: create rule 2023-08-10 20:56:51 -05:00
Alex Tran
22e5c6ead2 dev: controller 2023-08-10 16:23:48 -05:00
Alex Tran
4a34198b78 dev: remove index 2023-08-10 14:52:14 -05:00
Alex Tran
a85780c930 dev: naming 2023-08-10 14:51:51 -05:00
Alex Tran
717dfe51e7 dev: stub 2023-08-10 13:45:21 -05:00
Alex Tran
5168278215 entity 2023-08-10 13:42:00 -05:00
Alex Tran
d92dbbe655 entity 2023-08-10 13:26:42 -05:00
83 changed files with 3657 additions and 36 deletions

View file

@ -252,6 +252,12 @@ export interface AlbumResponseDto {
* @memberof AlbumResponseDto * @memberof AlbumResponseDto
*/ */
'ownerId': string; 'ownerId': string;
/**
*
* @type {Array<RuleResponseDto>}
* @memberof AlbumResponseDto
*/
'rules': Array<RuleResponseDto>;
/** /**
* *
* @type {boolean} * @type {boolean}
@ -325,6 +331,12 @@ export interface AllJobStatusResponseDto {
* @memberof AllJobStatusResponseDto * @memberof AllJobStatusResponseDto
*/ */
'sidecar': JobStatusDto; 'sidecar': JobStatusDto;
/**
*
* @type {JobStatusDto}
* @memberof AllJobStatusResponseDto
*/
'smartAlbum': JobStatusDto;
/** /**
* *
* @type {JobStatusDto} * @type {JobStatusDto}
@ -946,6 +958,33 @@ export interface CreateProfileImageResponseDto {
*/ */
'userId': string; 'userId': string;
} }
/**
*
* @export
* @interface CreateRuleDto
*/
export interface CreateRuleDto {
/**
*
* @type {string}
* @memberof CreateRuleDto
*/
'albumId': string;
/**
*
* @type {RuleKey}
* @memberof CreateRuleDto
*/
'key': RuleKey;
/**
*
* @type {string}
* @memberof CreateRuleDto
*/
'value': string;
}
/** /**
* *
* @export * @export
@ -1509,7 +1548,8 @@ export const JobName = {
BackgroundTask: 'backgroundTask', BackgroundTask: 'backgroundTask',
StorageTemplateMigration: 'storageTemplateMigration', StorageTemplateMigration: 'storageTemplateMigration',
Search: 'search', Search: 'search',
Sidecar: 'sidecar' Sidecar: 'sidecar',
SmartAlbum: 'smartAlbum'
} as const; } as const;
export type JobName = typeof JobName[keyof typeof JobName]; export type JobName = typeof JobName[keyof typeof JobName];
@ -1904,6 +1944,59 @@ export interface QueueStatusDto {
*/ */
'isPaused': boolean; 'isPaused': boolean;
} }
/**
*
* @export
* @enum {string}
*/
export const RuleKey = {
Person: 'person',
TakenAfter: 'taken-after',
City: 'city',
State: 'state',
Country: 'country',
CameraMake: 'camera-make',
CameraModel: 'camera-model',
Location: 'location'
} as const;
export type RuleKey = typeof RuleKey[keyof typeof RuleKey];
/**
*
* @export
* @interface RuleResponseDto
*/
export interface RuleResponseDto {
/**
*
* @type {string}
* @memberof RuleResponseDto
*/
'id': string;
/**
*
* @type {RuleKey}
* @memberof RuleResponseDto
*/
'key': RuleKey;
/**
*
* @type {string}
* @memberof RuleResponseDto
*/
'ownerId': string;
/**
*
* @type {string}
* @memberof RuleResponseDto
*/
'value': string;
}
/** /**
* *
* @export * @export
@ -2635,6 +2728,12 @@ export interface SystemConfigJobDto {
* @memberof SystemConfigJobDto * @memberof SystemConfigJobDto
*/ */
'sidecar': JobSettingsDto; 'sidecar': JobSettingsDto;
/**
*
* @type {JobSettingsDto}
* @memberof SystemConfigJobDto
*/
'smartAlbum': JobSettingsDto;
/** /**
* *
* @type {JobSettingsDto} * @type {JobSettingsDto}
@ -3020,6 +3119,27 @@ export interface UpdateAssetDto {
*/ */
'tagIds'?: Array<string>; 'tagIds'?: Array<string>;
} }
/**
*
* @export
* @interface UpdateRuleDto
*/
export interface UpdateRuleDto {
/**
*
* @type {RuleKey}
* @memberof UpdateRuleDto
*/
'key': RuleKey;
/**
*
* @type {string}
* @memberof UpdateRuleDto
*/
'value': string;
}
/** /**
* *
* @export * @export
@ -9700,6 +9820,404 @@ export class PersonApi extends BaseAPI {
} }
/**
* RuleApi - axios parameter creator
* @export
*/
export const RuleApiAxiosParamCreator = function (configuration?: Configuration) {
return {
/**
*
* @param {CreateRuleDto} createRuleDto
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
createRule: async (createRuleDto: CreateRuleDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'createRuleDto' is not null or undefined
assertParamExists('createRule', 'createRuleDto', createRuleDto)
const localVarPath = `/rule`;
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions;
if (configuration) {
baseOptions = configuration.baseOptions;
}
const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
// authentication cookie required
// authentication api_key required
await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration)
// authentication bearer required
// http bearer authentication required
await setBearerAuthToObject(localVarHeaderParameter, configuration)
localVarHeaderParameter['Content-Type'] = 'application/json';
setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
localVarRequestOptions.data = serializeDataIfNeeded(createRuleDto, localVarRequestOptions, configuration)
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
/**
*
* @param {string} id
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
getRule: async (id: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'id' is not null or undefined
assertParamExists('getRule', 'id', id)
const localVarPath = `/rule/{id}`
.replace(`{${"id"}}`, encodeURIComponent(String(id)));
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions;
if (configuration) {
baseOptions = configuration.baseOptions;
}
const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
// authentication cookie required
// authentication api_key required
await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration)
// authentication bearer required
// http bearer authentication required
await setBearerAuthToObject(localVarHeaderParameter, configuration)
setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
/**
*
* @param {string} id
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
removeRule: async (id: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'id' is not null or undefined
assertParamExists('removeRule', 'id', id)
const localVarPath = `/rule/{id}`
.replace(`{${"id"}}`, encodeURIComponent(String(id)));
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions;
if (configuration) {
baseOptions = configuration.baseOptions;
}
const localVarRequestOptions = { method: 'DELETE', ...baseOptions, ...options};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
// authentication cookie required
// authentication api_key required
await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration)
// authentication bearer required
// http bearer authentication required
await setBearerAuthToObject(localVarHeaderParameter, configuration)
setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
/**
*
* @param {string} id
* @param {UpdateRuleDto} updateRuleDto
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
updateRule: async (id: string, updateRuleDto: UpdateRuleDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'id' is not null or undefined
assertParamExists('updateRule', 'id', id)
// verify required parameter 'updateRuleDto' is not null or undefined
assertParamExists('updateRule', 'updateRuleDto', updateRuleDto)
const localVarPath = `/rule/{id}`
.replace(`{${"id"}}`, encodeURIComponent(String(id)));
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions;
if (configuration) {
baseOptions = configuration.baseOptions;
}
const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
// authentication cookie required
// authentication api_key required
await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration)
// authentication bearer required
// http bearer authentication required
await setBearerAuthToObject(localVarHeaderParameter, configuration)
localVarHeaderParameter['Content-Type'] = 'application/json';
setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
localVarRequestOptions.data = serializeDataIfNeeded(updateRuleDto, localVarRequestOptions, configuration)
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
}
};
/**
* RuleApi - functional programming interface
* @export
*/
export const RuleApiFp = function(configuration?: Configuration) {
const localVarAxiosParamCreator = RuleApiAxiosParamCreator(configuration)
return {
/**
*
* @param {CreateRuleDto} createRuleDto
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async createRule(createRuleDto: CreateRuleDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<RuleResponseDto>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.createRule(createRuleDto, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
*
* @param {string} id
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async getRule(id: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<RuleResponseDto>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.getRule(id, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
*
* @param {string} id
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async removeRule(id: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.removeRule(id, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
*
* @param {string} id
* @param {UpdateRuleDto} updateRuleDto
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async updateRule(id: string, updateRuleDto: UpdateRuleDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<RuleResponseDto>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.updateRule(id, updateRuleDto, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
}
};
/**
* RuleApi - factory interface
* @export
*/
export const RuleApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {
const localVarFp = RuleApiFp(configuration)
return {
/**
*
* @param {RuleApiCreateRuleRequest} requestParameters Request parameters.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
createRule(requestParameters: RuleApiCreateRuleRequest, options?: AxiosRequestConfig): AxiosPromise<RuleResponseDto> {
return localVarFp.createRule(requestParameters.createRuleDto, options).then((request) => request(axios, basePath));
},
/**
*
* @param {RuleApiGetRuleRequest} requestParameters Request parameters.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
getRule(requestParameters: RuleApiGetRuleRequest, options?: AxiosRequestConfig): AxiosPromise<RuleResponseDto> {
return localVarFp.getRule(requestParameters.id, options).then((request) => request(axios, basePath));
},
/**
*
* @param {RuleApiRemoveRuleRequest} requestParameters Request parameters.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
removeRule(requestParameters: RuleApiRemoveRuleRequest, options?: AxiosRequestConfig): AxiosPromise<void> {
return localVarFp.removeRule(requestParameters.id, options).then((request) => request(axios, basePath));
},
/**
*
* @param {RuleApiUpdateRuleRequest} requestParameters Request parameters.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
updateRule(requestParameters: RuleApiUpdateRuleRequest, options?: AxiosRequestConfig): AxiosPromise<RuleResponseDto> {
return localVarFp.updateRule(requestParameters.id, requestParameters.updateRuleDto, options).then((request) => request(axios, basePath));
},
};
};
/**
* Request parameters for createRule operation in RuleApi.
* @export
* @interface RuleApiCreateRuleRequest
*/
export interface RuleApiCreateRuleRequest {
/**
*
* @type {CreateRuleDto}
* @memberof RuleApiCreateRule
*/
readonly createRuleDto: CreateRuleDto
}
/**
* Request parameters for getRule operation in RuleApi.
* @export
* @interface RuleApiGetRuleRequest
*/
export interface RuleApiGetRuleRequest {
/**
*
* @type {string}
* @memberof RuleApiGetRule
*/
readonly id: string
}
/**
* Request parameters for removeRule operation in RuleApi.
* @export
* @interface RuleApiRemoveRuleRequest
*/
export interface RuleApiRemoveRuleRequest {
/**
*
* @type {string}
* @memberof RuleApiRemoveRule
*/
readonly id: string
}
/**
* Request parameters for updateRule operation in RuleApi.
* @export
* @interface RuleApiUpdateRuleRequest
*/
export interface RuleApiUpdateRuleRequest {
/**
*
* @type {string}
* @memberof RuleApiUpdateRule
*/
readonly id: string
/**
*
* @type {UpdateRuleDto}
* @memberof RuleApiUpdateRule
*/
readonly updateRuleDto: UpdateRuleDto
}
/**
* RuleApi - object-oriented interface
* @export
* @class RuleApi
* @extends {BaseAPI}
*/
export class RuleApi extends BaseAPI {
/**
*
* @param {RuleApiCreateRuleRequest} requestParameters Request parameters.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof RuleApi
*/
public createRule(requestParameters: RuleApiCreateRuleRequest, options?: AxiosRequestConfig) {
return RuleApiFp(this.configuration).createRule(requestParameters.createRuleDto, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @param {RuleApiGetRuleRequest} requestParameters Request parameters.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof RuleApi
*/
public getRule(requestParameters: RuleApiGetRuleRequest, options?: AxiosRequestConfig) {
return RuleApiFp(this.configuration).getRule(requestParameters.id, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @param {RuleApiRemoveRuleRequest} requestParameters Request parameters.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof RuleApi
*/
public removeRule(requestParameters: RuleApiRemoveRuleRequest, options?: AxiosRequestConfig) {
return RuleApiFp(this.configuration).removeRule(requestParameters.id, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @param {RuleApiUpdateRuleRequest} requestParameters Request parameters.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof RuleApi
*/
public updateRule(requestParameters: RuleApiUpdateRuleRequest, options?: AxiosRequestConfig) {
return RuleApiFp(this.configuration).updateRule(requestParameters.id, requestParameters.updateRuleDto, options).then((request) => request(this.axios, this.basePath));
}
}
/** /**
* SearchApi - axios parameter creator * SearchApi - axios parameter creator
* @export * @export

View file

@ -38,6 +38,7 @@ doc/CheckExistingAssetsDto.md
doc/CheckExistingAssetsResponseDto.md doc/CheckExistingAssetsResponseDto.md
doc/CreateAlbumDto.md doc/CreateAlbumDto.md
doc/CreateProfileImageResponseDto.md doc/CreateProfileImageResponseDto.md
doc/CreateRuleDto.md
doc/CreateTagDto.md doc/CreateTagDto.md
doc/CreateUserDto.md doc/CreateUserDto.md
doc/CuratedLocationsResponseDto.md doc/CuratedLocationsResponseDto.md
@ -75,6 +76,9 @@ doc/PersonApi.md
doc/PersonResponseDto.md doc/PersonResponseDto.md
doc/PersonUpdateDto.md doc/PersonUpdateDto.md
doc/QueueStatusDto.md doc/QueueStatusDto.md
doc/RuleApi.md
doc/RuleKey.md
doc/RuleResponseDto.md
doc/SearchAlbumResponseDto.md doc/SearchAlbumResponseDto.md
doc/SearchApi.md doc/SearchApi.md
doc/SearchAssetDto.md doc/SearchAssetDto.md
@ -118,6 +122,7 @@ doc/TranscodeHWAccel.md
doc/TranscodePolicy.md doc/TranscodePolicy.md
doc/UpdateAlbumDto.md doc/UpdateAlbumDto.md
doc/UpdateAssetDto.md doc/UpdateAssetDto.md
doc/UpdateRuleDto.md
doc/UpdateTagDto.md doc/UpdateTagDto.md
doc/UpdateUserDto.md doc/UpdateUserDto.md
doc/UsageByUserDto.md doc/UsageByUserDto.md
@ -136,6 +141,7 @@ lib/api/job_api.dart
lib/api/o_auth_api.dart lib/api/o_auth_api.dart
lib/api/partner_api.dart lib/api/partner_api.dart
lib/api/person_api.dart lib/api/person_api.dart
lib/api/rule_api.dart
lib/api/search_api.dart lib/api/search_api.dart
lib/api/server_info_api.dart lib/api/server_info_api.dart
lib/api/shared_link_api.dart lib/api/shared_link_api.dart
@ -181,6 +187,7 @@ lib/model/check_existing_assets_dto.dart
lib/model/check_existing_assets_response_dto.dart lib/model/check_existing_assets_response_dto.dart
lib/model/create_album_dto.dart lib/model/create_album_dto.dart
lib/model/create_profile_image_response_dto.dart lib/model/create_profile_image_response_dto.dart
lib/model/create_rule_dto.dart
lib/model/create_tag_dto.dart lib/model/create_tag_dto.dart
lib/model/create_user_dto.dart lib/model/create_user_dto.dart
lib/model/curated_locations_response_dto.dart lib/model/curated_locations_response_dto.dart
@ -214,6 +221,8 @@ lib/model/people_update_item.dart
lib/model/person_response_dto.dart lib/model/person_response_dto.dart
lib/model/person_update_dto.dart lib/model/person_update_dto.dart
lib/model/queue_status_dto.dart lib/model/queue_status_dto.dart
lib/model/rule_key.dart
lib/model/rule_response_dto.dart
lib/model/search_album_response_dto.dart lib/model/search_album_response_dto.dart
lib/model/search_asset_dto.dart lib/model/search_asset_dto.dart
lib/model/search_asset_response_dto.dart lib/model/search_asset_response_dto.dart
@ -252,6 +261,7 @@ lib/model/transcode_hw_accel.dart
lib/model/transcode_policy.dart lib/model/transcode_policy.dart
lib/model/update_album_dto.dart lib/model/update_album_dto.dart
lib/model/update_asset_dto.dart lib/model/update_asset_dto.dart
lib/model/update_rule_dto.dart
lib/model/update_tag_dto.dart lib/model/update_tag_dto.dart
lib/model/update_user_dto.dart lib/model/update_user_dto.dart
lib/model/usage_by_user_dto.dart lib/model/usage_by_user_dto.dart
@ -295,6 +305,7 @@ test/check_existing_assets_dto_test.dart
test/check_existing_assets_response_dto_test.dart test/check_existing_assets_response_dto_test.dart
test/create_album_dto_test.dart test/create_album_dto_test.dart
test/create_profile_image_response_dto_test.dart test/create_profile_image_response_dto_test.dart
test/create_rule_dto_test.dart
test/create_tag_dto_test.dart test/create_tag_dto_test.dart
test/create_user_dto_test.dart test/create_user_dto_test.dart
test/curated_locations_response_dto_test.dart test/curated_locations_response_dto_test.dart
@ -332,6 +343,9 @@ test/person_api_test.dart
test/person_response_dto_test.dart test/person_response_dto_test.dart
test/person_update_dto_test.dart test/person_update_dto_test.dart
test/queue_status_dto_test.dart test/queue_status_dto_test.dart
test/rule_api_test.dart
test/rule_key_test.dart
test/rule_response_dto_test.dart
test/search_album_response_dto_test.dart test/search_album_response_dto_test.dart
test/search_api_test.dart test/search_api_test.dart
test/search_asset_dto_test.dart test/search_asset_dto_test.dart
@ -375,6 +389,7 @@ test/transcode_hw_accel_test.dart
test/transcode_policy_test.dart test/transcode_policy_test.dart
test/update_album_dto_test.dart test/update_album_dto_test.dart
test/update_asset_dto_test.dart test/update_asset_dto_test.dart
test/update_rule_dto_test.dart
test/update_tag_dto_test.dart test/update_tag_dto_test.dart
test/update_user_dto_test.dart test/update_user_dto_test.dart
test/usage_by_user_dto_test.dart test/usage_by_user_dto_test.dart

View file

@ -137,6 +137,10 @@ Class | Method | HTTP request | Description
*PersonApi* | [**mergePerson**](doc//PersonApi.md#mergeperson) | **POST** /person/{id}/merge | *PersonApi* | [**mergePerson**](doc//PersonApi.md#mergeperson) | **POST** /person/{id}/merge |
*PersonApi* | [**updatePeople**](doc//PersonApi.md#updatepeople) | **PUT** /person | *PersonApi* | [**updatePeople**](doc//PersonApi.md#updatepeople) | **PUT** /person |
*PersonApi* | [**updatePerson**](doc//PersonApi.md#updateperson) | **PUT** /person/{id} | *PersonApi* | [**updatePerson**](doc//PersonApi.md#updateperson) | **PUT** /person/{id} |
*RuleApi* | [**createRule**](doc//RuleApi.md#createrule) | **POST** /rule |
*RuleApi* | [**getRule**](doc//RuleApi.md#getrule) | **GET** /rule/{id} |
*RuleApi* | [**removeRule**](doc//RuleApi.md#removerule) | **DELETE** /rule/{id} |
*RuleApi* | [**updateRule**](doc//RuleApi.md#updaterule) | **PUT** /rule/{id} |
*SearchApi* | [**getExploreData**](doc//SearchApi.md#getexploredata) | **GET** /search/explore | *SearchApi* | [**getExploreData**](doc//SearchApi.md#getexploredata) | **GET** /search/explore |
*SearchApi* | [**getSearchConfig**](doc//SearchApi.md#getsearchconfig) | **GET** /search/config | *SearchApi* | [**getSearchConfig**](doc//SearchApi.md#getsearchconfig) | **GET** /search/config |
*SearchApi* | [**search**](doc//SearchApi.md#search) | **GET** /search | *SearchApi* | [**search**](doc//SearchApi.md#search) | **GET** /search |
@ -210,6 +214,7 @@ Class | Method | HTTP request | Description
- [CheckExistingAssetsResponseDto](doc//CheckExistingAssetsResponseDto.md) - [CheckExistingAssetsResponseDto](doc//CheckExistingAssetsResponseDto.md)
- [CreateAlbumDto](doc//CreateAlbumDto.md) - [CreateAlbumDto](doc//CreateAlbumDto.md)
- [CreateProfileImageResponseDto](doc//CreateProfileImageResponseDto.md) - [CreateProfileImageResponseDto](doc//CreateProfileImageResponseDto.md)
- [CreateRuleDto](doc//CreateRuleDto.md)
- [CreateTagDto](doc//CreateTagDto.md) - [CreateTagDto](doc//CreateTagDto.md)
- [CreateUserDto](doc//CreateUserDto.md) - [CreateUserDto](doc//CreateUserDto.md)
- [CuratedLocationsResponseDto](doc//CuratedLocationsResponseDto.md) - [CuratedLocationsResponseDto](doc//CuratedLocationsResponseDto.md)
@ -243,6 +248,8 @@ Class | Method | HTTP request | Description
- [PersonResponseDto](doc//PersonResponseDto.md) - [PersonResponseDto](doc//PersonResponseDto.md)
- [PersonUpdateDto](doc//PersonUpdateDto.md) - [PersonUpdateDto](doc//PersonUpdateDto.md)
- [QueueStatusDto](doc//QueueStatusDto.md) - [QueueStatusDto](doc//QueueStatusDto.md)
- [RuleKey](doc//RuleKey.md)
- [RuleResponseDto](doc//RuleResponseDto.md)
- [SearchAlbumResponseDto](doc//SearchAlbumResponseDto.md) - [SearchAlbumResponseDto](doc//SearchAlbumResponseDto.md)
- [SearchAssetDto](doc//SearchAssetDto.md) - [SearchAssetDto](doc//SearchAssetDto.md)
- [SearchAssetResponseDto](doc//SearchAssetResponseDto.md) - [SearchAssetResponseDto](doc//SearchAssetResponseDto.md)
@ -281,6 +288,7 @@ Class | Method | HTTP request | Description
- [TranscodePolicy](doc//TranscodePolicy.md) - [TranscodePolicy](doc//TranscodePolicy.md)
- [UpdateAlbumDto](doc//UpdateAlbumDto.md) - [UpdateAlbumDto](doc//UpdateAlbumDto.md)
- [UpdateAssetDto](doc//UpdateAssetDto.md) - [UpdateAssetDto](doc//UpdateAssetDto.md)
- [UpdateRuleDto](doc//UpdateRuleDto.md)
- [UpdateTagDto](doc//UpdateTagDto.md) - [UpdateTagDto](doc//UpdateTagDto.md)
- [UpdateUserDto](doc//UpdateUserDto.md) - [UpdateUserDto](doc//UpdateUserDto.md)
- [UsageByUserDto](doc//UsageByUserDto.md) - [UsageByUserDto](doc//UsageByUserDto.md)

View file

@ -20,6 +20,7 @@ Name | Type | Description | Notes
**lastModifiedAssetTimestamp** | [**DateTime**](DateTime.md) | | [optional] **lastModifiedAssetTimestamp** | [**DateTime**](DateTime.md) | | [optional]
**owner** | [**UserResponseDto**](UserResponseDto.md) | | **owner** | [**UserResponseDto**](UserResponseDto.md) | |
**ownerId** | **String** | | **ownerId** | **String** | |
**rules** | [**List<RuleResponseDto>**](RuleResponseDto.md) | | [default to const []]
**shared** | **bool** | | **shared** | **bool** | |
**sharedUsers** | [**List<UserResponseDto>**](UserResponseDto.md) | | [default to const []] **sharedUsers** | [**List<UserResponseDto>**](UserResponseDto.md) | | [default to const []]
**startDate** | [**DateTime**](DateTime.md) | | [optional] **startDate** | [**DateTime**](DateTime.md) | | [optional]

View file

@ -15,6 +15,7 @@ Name | Type | Description | Notes
**recognizeFaces** | [**JobStatusDto**](JobStatusDto.md) | | **recognizeFaces** | [**JobStatusDto**](JobStatusDto.md) | |
**search** | [**JobStatusDto**](JobStatusDto.md) | | **search** | [**JobStatusDto**](JobStatusDto.md) | |
**sidecar** | [**JobStatusDto**](JobStatusDto.md) | | **sidecar** | [**JobStatusDto**](JobStatusDto.md) | |
**smartAlbum** | [**JobStatusDto**](JobStatusDto.md) | |
**storageTemplateMigration** | [**JobStatusDto**](JobStatusDto.md) | | **storageTemplateMigration** | [**JobStatusDto**](JobStatusDto.md) | |
**thumbnailGeneration** | [**JobStatusDto**](JobStatusDto.md) | | **thumbnailGeneration** | [**JobStatusDto**](JobStatusDto.md) | |
**videoConversion** | [**JobStatusDto**](JobStatusDto.md) | | **videoConversion** | [**JobStatusDto**](JobStatusDto.md) | |

17
mobile/openapi/doc/CreateRuleDto.md generated Normal file
View file

@ -0,0 +1,17 @@
# openapi.model.CreateRuleDto
## Load the model package
```dart
import 'package:openapi/api.dart';
```
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**albumId** | **String** | |
**key** | [**RuleKey**](RuleKey.md) | |
**value** | **String** | |
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)

238
mobile/openapi/doc/RuleApi.md generated Normal file
View file

@ -0,0 +1,238 @@
# openapi.api.RuleApi
## Load the API package
```dart
import 'package:openapi/api.dart';
```
All URIs are relative to */api*
Method | HTTP request | Description
------------- | ------------- | -------------
[**createRule**](RuleApi.md#createrule) | **POST** /rule |
[**getRule**](RuleApi.md#getrule) | **GET** /rule/{id} |
[**removeRule**](RuleApi.md#removerule) | **DELETE** /rule/{id} |
[**updateRule**](RuleApi.md#updaterule) | **PUT** /rule/{id} |
# **createRule**
> RuleResponseDto createRule(createRuleDto)
### Example
```dart
import 'package:openapi/api.dart';
// TODO Configure API key authorization: cookie
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKey = 'YOUR_API_KEY';
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKeyPrefix = 'Bearer';
// TODO Configure API key authorization: api_key
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKey = 'YOUR_API_KEY';
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKeyPrefix = 'Bearer';
// TODO Configure HTTP Bearer authorization: bearer
// Case 1. Use String Token
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken('YOUR_ACCESS_TOKEN');
// Case 2. Use Function which generate token.
// String yourTokenGeneratorFunction() { ... }
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
final api_instance = RuleApi();
final createRuleDto = CreateRuleDto(); // CreateRuleDto |
try {
final result = api_instance.createRule(createRuleDto);
print(result);
} catch (e) {
print('Exception when calling RuleApi->createRule: $e\n');
}
```
### Parameters
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**createRuleDto** | [**CreateRuleDto**](CreateRuleDto.md)| |
### Return type
[**RuleResponseDto**](RuleResponseDto.md)
### Authorization
[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer)
### HTTP request headers
- **Content-Type**: application/json
- **Accept**: application/json
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
# **getRule**
> RuleResponseDto getRule(id)
### Example
```dart
import 'package:openapi/api.dart';
// TODO Configure API key authorization: cookie
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKey = 'YOUR_API_KEY';
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKeyPrefix = 'Bearer';
// TODO Configure API key authorization: api_key
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKey = 'YOUR_API_KEY';
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKeyPrefix = 'Bearer';
// TODO Configure HTTP Bearer authorization: bearer
// Case 1. Use String Token
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken('YOUR_ACCESS_TOKEN');
// Case 2. Use Function which generate token.
// String yourTokenGeneratorFunction() { ... }
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
final api_instance = RuleApi();
final id = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String |
try {
final result = api_instance.getRule(id);
print(result);
} catch (e) {
print('Exception when calling RuleApi->getRule: $e\n');
}
```
### Parameters
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**id** | **String**| |
### Return type
[**RuleResponseDto**](RuleResponseDto.md)
### Authorization
[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer)
### HTTP request headers
- **Content-Type**: Not defined
- **Accept**: application/json
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
# **removeRule**
> removeRule(id)
### Example
```dart
import 'package:openapi/api.dart';
// TODO Configure API key authorization: cookie
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKey = 'YOUR_API_KEY';
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKeyPrefix = 'Bearer';
// TODO Configure API key authorization: api_key
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKey = 'YOUR_API_KEY';
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKeyPrefix = 'Bearer';
// TODO Configure HTTP Bearer authorization: bearer
// Case 1. Use String Token
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken('YOUR_ACCESS_TOKEN');
// Case 2. Use Function which generate token.
// String yourTokenGeneratorFunction() { ... }
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
final api_instance = RuleApi();
final id = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String |
try {
api_instance.removeRule(id);
} catch (e) {
print('Exception when calling RuleApi->removeRule: $e\n');
}
```
### Parameters
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**id** | **String**| |
### Return type
void (empty response body)
### Authorization
[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer)
### HTTP request headers
- **Content-Type**: Not defined
- **Accept**: Not defined
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
# **updateRule**
> RuleResponseDto updateRule(id, updateRuleDto)
### Example
```dart
import 'package:openapi/api.dart';
// TODO Configure API key authorization: cookie
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKey = 'YOUR_API_KEY';
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKeyPrefix = 'Bearer';
// TODO Configure API key authorization: api_key
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKey = 'YOUR_API_KEY';
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKeyPrefix = 'Bearer';
// TODO Configure HTTP Bearer authorization: bearer
// Case 1. Use String Token
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken('YOUR_ACCESS_TOKEN');
// Case 2. Use Function which generate token.
// String yourTokenGeneratorFunction() { ... }
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
final api_instance = RuleApi();
final id = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String |
final updateRuleDto = UpdateRuleDto(); // UpdateRuleDto |
try {
final result = api_instance.updateRule(id, updateRuleDto);
print(result);
} catch (e) {
print('Exception when calling RuleApi->updateRule: $e\n');
}
```
### Parameters
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**id** | **String**| |
**updateRuleDto** | [**UpdateRuleDto**](UpdateRuleDto.md)| |
### Return type
[**RuleResponseDto**](RuleResponseDto.md)
### Authorization
[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer)
### HTTP request headers
- **Content-Type**: application/json
- **Accept**: application/json
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)

14
mobile/openapi/doc/RuleKey.md generated Normal file
View file

@ -0,0 +1,14 @@
# openapi.model.RuleKey
## Load the model package
```dart
import 'package:openapi/api.dart';
```
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)

18
mobile/openapi/doc/RuleResponseDto.md generated Normal file
View file

@ -0,0 +1,18 @@
# openapi.model.RuleResponseDto
## Load the model package
```dart
import 'package:openapi/api.dart';
```
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**id** | **String** | |
**key** | [**RuleKey**](RuleKey.md) | |
**ownerId** | **String** | |
**value** | **String** | |
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)

View file

@ -15,6 +15,7 @@ Name | Type | Description | Notes
**recognizeFaces** | [**JobSettingsDto**](JobSettingsDto.md) | | **recognizeFaces** | [**JobSettingsDto**](JobSettingsDto.md) | |
**search** | [**JobSettingsDto**](JobSettingsDto.md) | | **search** | [**JobSettingsDto**](JobSettingsDto.md) | |
**sidecar** | [**JobSettingsDto**](JobSettingsDto.md) | | **sidecar** | [**JobSettingsDto**](JobSettingsDto.md) | |
**smartAlbum** | [**JobSettingsDto**](JobSettingsDto.md) | |
**storageTemplateMigration** | [**JobSettingsDto**](JobSettingsDto.md) | | **storageTemplateMigration** | [**JobSettingsDto**](JobSettingsDto.md) | |
**thumbnailGeneration** | [**JobSettingsDto**](JobSettingsDto.md) | | **thumbnailGeneration** | [**JobSettingsDto**](JobSettingsDto.md) | |
**videoConversion** | [**JobSettingsDto**](JobSettingsDto.md) | | **videoConversion** | [**JobSettingsDto**](JobSettingsDto.md) | |

16
mobile/openapi/doc/UpdateRuleDto.md generated Normal file
View file

@ -0,0 +1,16 @@
# openapi.model.UpdateRuleDto
## Load the model package
```dart
import 'package:openapi/api.dart';
```
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**key** | [**RuleKey**](RuleKey.md) | |
**value** | **String** | |
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)

View file

@ -36,6 +36,7 @@ part 'api/job_api.dart';
part 'api/o_auth_api.dart'; part 'api/o_auth_api.dart';
part 'api/partner_api.dart'; part 'api/partner_api.dart';
part 'api/person_api.dart'; part 'api/person_api.dart';
part 'api/rule_api.dart';
part 'api/search_api.dart'; part 'api/search_api.dart';
part 'api/server_info_api.dart'; part 'api/server_info_api.dart';
part 'api/shared_link_api.dart'; part 'api/shared_link_api.dart';
@ -74,6 +75,7 @@ part 'model/check_existing_assets_dto.dart';
part 'model/check_existing_assets_response_dto.dart'; part 'model/check_existing_assets_response_dto.dart';
part 'model/create_album_dto.dart'; part 'model/create_album_dto.dart';
part 'model/create_profile_image_response_dto.dart'; part 'model/create_profile_image_response_dto.dart';
part 'model/create_rule_dto.dart';
part 'model/create_tag_dto.dart'; part 'model/create_tag_dto.dart';
part 'model/create_user_dto.dart'; part 'model/create_user_dto.dart';
part 'model/curated_locations_response_dto.dart'; part 'model/curated_locations_response_dto.dart';
@ -107,6 +109,8 @@ part 'model/people_update_item.dart';
part 'model/person_response_dto.dart'; part 'model/person_response_dto.dart';
part 'model/person_update_dto.dart'; part 'model/person_update_dto.dart';
part 'model/queue_status_dto.dart'; part 'model/queue_status_dto.dart';
part 'model/rule_key.dart';
part 'model/rule_response_dto.dart';
part 'model/search_album_response_dto.dart'; part 'model/search_album_response_dto.dart';
part 'model/search_asset_dto.dart'; part 'model/search_asset_dto.dart';
part 'model/search_asset_response_dto.dart'; part 'model/search_asset_response_dto.dart';
@ -145,6 +149,7 @@ part 'model/transcode_hw_accel.dart';
part 'model/transcode_policy.dart'; part 'model/transcode_policy.dart';
part 'model/update_album_dto.dart'; part 'model/update_album_dto.dart';
part 'model/update_asset_dto.dart'; part 'model/update_asset_dto.dart';
part 'model/update_rule_dto.dart';
part 'model/update_tag_dto.dart'; part 'model/update_tag_dto.dart';
part 'model/update_user_dto.dart'; part 'model/update_user_dto.dart';
part 'model/usage_by_user_dto.dart'; part 'model/usage_by_user_dto.dart';

205
mobile/openapi/lib/api/rule_api.dart generated Normal file
View file

@ -0,0 +1,205 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.12
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
part of openapi.api;
class RuleApi {
RuleApi([ApiClient? apiClient]) : apiClient = apiClient ?? defaultApiClient;
final ApiClient apiClient;
/// Performs an HTTP 'POST /rule' operation and returns the [Response].
/// Parameters:
///
/// * [CreateRuleDto] createRuleDto (required):
Future<Response> createRuleWithHttpInfo(CreateRuleDto createRuleDto,) async {
// ignore: prefer_const_declarations
final path = r'/rule';
// ignore: prefer_final_locals
Object? postBody = createRuleDto;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>['application/json'];
return apiClient.invokeAPI(
path,
'POST',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// Parameters:
///
/// * [CreateRuleDto] createRuleDto (required):
Future<RuleResponseDto?> createRule(CreateRuleDto createRuleDto,) async {
final response = await createRuleWithHttpInfo(createRuleDto,);
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
// When a remote server returns no body with a status of 204, we shall not decode it.
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
// FormatException when trying to decode an empty string.
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'RuleResponseDto',) as RuleResponseDto;
}
return null;
}
/// Performs an HTTP 'GET /rule/{id}' operation and returns the [Response].
/// Parameters:
///
/// * [String] id (required):
Future<Response> getRuleWithHttpInfo(String id,) async {
// ignore: prefer_const_declarations
final path = r'/rule/{id}'
.replaceAll('{id}', id);
// ignore: prefer_final_locals
Object? postBody;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>[];
return apiClient.invokeAPI(
path,
'GET',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// Parameters:
///
/// * [String] id (required):
Future<RuleResponseDto?> getRule(String id,) async {
final response = await getRuleWithHttpInfo(id,);
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
// When a remote server returns no body with a status of 204, we shall not decode it.
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
// FormatException when trying to decode an empty string.
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'RuleResponseDto',) as RuleResponseDto;
}
return null;
}
/// Performs an HTTP 'DELETE /rule/{id}' operation and returns the [Response].
/// Parameters:
///
/// * [String] id (required):
Future<Response> removeRuleWithHttpInfo(String id,) async {
// ignore: prefer_const_declarations
final path = r'/rule/{id}'
.replaceAll('{id}', id);
// ignore: prefer_final_locals
Object? postBody;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>[];
return apiClient.invokeAPI(
path,
'DELETE',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// Parameters:
///
/// * [String] id (required):
Future<void> removeRule(String id,) async {
final response = await removeRuleWithHttpInfo(id,);
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
}
/// Performs an HTTP 'PUT /rule/{id}' operation and returns the [Response].
/// Parameters:
///
/// * [String] id (required):
///
/// * [UpdateRuleDto] updateRuleDto (required):
Future<Response> updateRuleWithHttpInfo(String id, UpdateRuleDto updateRuleDto,) async {
// ignore: prefer_const_declarations
final path = r'/rule/{id}'
.replaceAll('{id}', id);
// ignore: prefer_final_locals
Object? postBody = updateRuleDto;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>['application/json'];
return apiClient.invokeAPI(
path,
'PUT',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// Parameters:
///
/// * [String] id (required):
///
/// * [UpdateRuleDto] updateRuleDto (required):
Future<RuleResponseDto?> updateRule(String id, UpdateRuleDto updateRuleDto,) async {
final response = await updateRuleWithHttpInfo(id, updateRuleDto,);
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
// When a remote server returns no body with a status of 204, we shall not decode it.
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
// FormatException when trying to decode an empty string.
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'RuleResponseDto',) as RuleResponseDto;
}
return null;
}
}

View file

@ -243,6 +243,8 @@ class ApiClient {
return CreateAlbumDto.fromJson(value); return CreateAlbumDto.fromJson(value);
case 'CreateProfileImageResponseDto': case 'CreateProfileImageResponseDto':
return CreateProfileImageResponseDto.fromJson(value); return CreateProfileImageResponseDto.fromJson(value);
case 'CreateRuleDto':
return CreateRuleDto.fromJson(value);
case 'CreateTagDto': case 'CreateTagDto':
return CreateTagDto.fromJson(value); return CreateTagDto.fromJson(value);
case 'CreateUserDto': case 'CreateUserDto':
@ -309,6 +311,10 @@ class ApiClient {
return PersonUpdateDto.fromJson(value); return PersonUpdateDto.fromJson(value);
case 'QueueStatusDto': case 'QueueStatusDto':
return QueueStatusDto.fromJson(value); return QueueStatusDto.fromJson(value);
case 'RuleKey':
return RuleKeyTypeTransformer().decode(value);
case 'RuleResponseDto':
return RuleResponseDto.fromJson(value);
case 'SearchAlbumResponseDto': case 'SearchAlbumResponseDto':
return SearchAlbumResponseDto.fromJson(value); return SearchAlbumResponseDto.fromJson(value);
case 'SearchAssetDto': case 'SearchAssetDto':
@ -385,6 +391,8 @@ class ApiClient {
return UpdateAlbumDto.fromJson(value); return UpdateAlbumDto.fromJson(value);
case 'UpdateAssetDto': case 'UpdateAssetDto':
return UpdateAssetDto.fromJson(value); return UpdateAssetDto.fromJson(value);
case 'UpdateRuleDto':
return UpdateRuleDto.fromJson(value);
case 'UpdateTagDto': case 'UpdateTagDto':
return UpdateTagDto.fromJson(value); return UpdateTagDto.fromJson(value);
case 'UpdateUserDto': case 'UpdateUserDto':

View file

@ -70,6 +70,9 @@ String parameterToString(dynamic value) {
if (value is JobName) { if (value is JobName) {
return JobNameTypeTransformer().encode(value).toString(); return JobNameTypeTransformer().encode(value).toString();
} }
if (value is RuleKey) {
return RuleKeyTypeTransformer().encode(value).toString();
}
if (value is SharedLinkType) { if (value is SharedLinkType) {
return SharedLinkTypeTypeTransformer().encode(value).toString(); return SharedLinkTypeTypeTransformer().encode(value).toString();
} }

View file

@ -25,6 +25,7 @@ class AlbumResponseDto {
this.lastModifiedAssetTimestamp, this.lastModifiedAssetTimestamp,
required this.owner, required this.owner,
required this.ownerId, required this.ownerId,
this.rules = const [],
required this.shared, required this.shared,
this.sharedUsers = const [], this.sharedUsers = const [],
this.startDate, this.startDate,
@ -67,6 +68,8 @@ class AlbumResponseDto {
String ownerId; String ownerId;
List<RuleResponseDto> rules;
bool shared; bool shared;
List<UserResponseDto> sharedUsers; List<UserResponseDto> sharedUsers;
@ -95,6 +98,7 @@ class AlbumResponseDto {
other.lastModifiedAssetTimestamp == lastModifiedAssetTimestamp && other.lastModifiedAssetTimestamp == lastModifiedAssetTimestamp &&
other.owner == owner && other.owner == owner &&
other.ownerId == ownerId && other.ownerId == ownerId &&
other.rules == rules &&
other.shared == shared && other.shared == shared &&
other.sharedUsers == sharedUsers && other.sharedUsers == sharedUsers &&
other.startDate == startDate && other.startDate == startDate &&
@ -115,13 +119,14 @@ class AlbumResponseDto {
(lastModifiedAssetTimestamp == null ? 0 : lastModifiedAssetTimestamp!.hashCode) + (lastModifiedAssetTimestamp == null ? 0 : lastModifiedAssetTimestamp!.hashCode) +
(owner.hashCode) + (owner.hashCode) +
(ownerId.hashCode) + (ownerId.hashCode) +
(rules.hashCode) +
(shared.hashCode) + (shared.hashCode) +
(sharedUsers.hashCode) + (sharedUsers.hashCode) +
(startDate == null ? 0 : startDate!.hashCode) + (startDate == null ? 0 : startDate!.hashCode) +
(updatedAt.hashCode); (updatedAt.hashCode);
@override @override
String toString() => 'AlbumResponseDto[albumName=$albumName, albumThumbnailAssetId=$albumThumbnailAssetId, assetCount=$assetCount, assets=$assets, createdAt=$createdAt, description=$description, endDate=$endDate, hasSharedLink=$hasSharedLink, id=$id, lastModifiedAssetTimestamp=$lastModifiedAssetTimestamp, owner=$owner, ownerId=$ownerId, shared=$shared, sharedUsers=$sharedUsers, startDate=$startDate, updatedAt=$updatedAt]'; String toString() => 'AlbumResponseDto[albumName=$albumName, albumThumbnailAssetId=$albumThumbnailAssetId, assetCount=$assetCount, assets=$assets, createdAt=$createdAt, description=$description, endDate=$endDate, hasSharedLink=$hasSharedLink, id=$id, lastModifiedAssetTimestamp=$lastModifiedAssetTimestamp, owner=$owner, ownerId=$ownerId, rules=$rules, shared=$shared, sharedUsers=$sharedUsers, startDate=$startDate, updatedAt=$updatedAt]';
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final json = <String, dynamic>{}; final json = <String, dynamic>{};
@ -149,6 +154,7 @@ class AlbumResponseDto {
} }
json[r'owner'] = this.owner; json[r'owner'] = this.owner;
json[r'ownerId'] = this.ownerId; json[r'ownerId'] = this.ownerId;
json[r'rules'] = this.rules;
json[r'shared'] = this.shared; json[r'shared'] = this.shared;
json[r'sharedUsers'] = this.sharedUsers; json[r'sharedUsers'] = this.sharedUsers;
if (this.startDate != null) { if (this.startDate != null) {
@ -180,6 +186,7 @@ class AlbumResponseDto {
lastModifiedAssetTimestamp: mapDateTime(json, r'lastModifiedAssetTimestamp', ''), lastModifiedAssetTimestamp: mapDateTime(json, r'lastModifiedAssetTimestamp', ''),
owner: UserResponseDto.fromJson(json[r'owner'])!, owner: UserResponseDto.fromJson(json[r'owner'])!,
ownerId: mapValueOfType<String>(json, r'ownerId')!, ownerId: mapValueOfType<String>(json, r'ownerId')!,
rules: RuleResponseDto.listFromJson(json[r'rules']),
shared: mapValueOfType<bool>(json, r'shared')!, shared: mapValueOfType<bool>(json, r'shared')!,
sharedUsers: UserResponseDto.listFromJson(json[r'sharedUsers']), sharedUsers: UserResponseDto.listFromJson(json[r'sharedUsers']),
startDate: mapDateTime(json, r'startDate', ''), startDate: mapDateTime(json, r'startDate', ''),
@ -241,6 +248,7 @@ class AlbumResponseDto {
'id', 'id',
'owner', 'owner',
'ownerId', 'ownerId',
'rules',
'shared', 'shared',
'sharedUsers', 'sharedUsers',
'updatedAt', 'updatedAt',

View file

@ -20,6 +20,7 @@ class AllJobStatusResponseDto {
required this.recognizeFaces, required this.recognizeFaces,
required this.search, required this.search,
required this.sidecar, required this.sidecar,
required this.smartAlbum,
required this.storageTemplateMigration, required this.storageTemplateMigration,
required this.thumbnailGeneration, required this.thumbnailGeneration,
required this.videoConversion, required this.videoConversion,
@ -39,6 +40,8 @@ class AllJobStatusResponseDto {
JobStatusDto sidecar; JobStatusDto sidecar;
JobStatusDto smartAlbum;
JobStatusDto storageTemplateMigration; JobStatusDto storageTemplateMigration;
JobStatusDto thumbnailGeneration; JobStatusDto thumbnailGeneration;
@ -54,6 +57,7 @@ class AllJobStatusResponseDto {
other.recognizeFaces == recognizeFaces && other.recognizeFaces == recognizeFaces &&
other.search == search && other.search == search &&
other.sidecar == sidecar && other.sidecar == sidecar &&
other.smartAlbum == smartAlbum &&
other.storageTemplateMigration == storageTemplateMigration && other.storageTemplateMigration == storageTemplateMigration &&
other.thumbnailGeneration == thumbnailGeneration && other.thumbnailGeneration == thumbnailGeneration &&
other.videoConversion == videoConversion; other.videoConversion == videoConversion;
@ -68,12 +72,13 @@ class AllJobStatusResponseDto {
(recognizeFaces.hashCode) + (recognizeFaces.hashCode) +
(search.hashCode) + (search.hashCode) +
(sidecar.hashCode) + (sidecar.hashCode) +
(smartAlbum.hashCode) +
(storageTemplateMigration.hashCode) + (storageTemplateMigration.hashCode) +
(thumbnailGeneration.hashCode) + (thumbnailGeneration.hashCode) +
(videoConversion.hashCode); (videoConversion.hashCode);
@override @override
String toString() => 'AllJobStatusResponseDto[backgroundTask=$backgroundTask, clipEncoding=$clipEncoding, metadataExtraction=$metadataExtraction, objectTagging=$objectTagging, recognizeFaces=$recognizeFaces, search=$search, sidecar=$sidecar, storageTemplateMigration=$storageTemplateMigration, thumbnailGeneration=$thumbnailGeneration, videoConversion=$videoConversion]'; String toString() => 'AllJobStatusResponseDto[backgroundTask=$backgroundTask, clipEncoding=$clipEncoding, metadataExtraction=$metadataExtraction, objectTagging=$objectTagging, recognizeFaces=$recognizeFaces, search=$search, sidecar=$sidecar, smartAlbum=$smartAlbum, storageTemplateMigration=$storageTemplateMigration, thumbnailGeneration=$thumbnailGeneration, videoConversion=$videoConversion]';
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final json = <String, dynamic>{}; final json = <String, dynamic>{};
@ -84,6 +89,7 @@ class AllJobStatusResponseDto {
json[r'recognizeFaces'] = this.recognizeFaces; json[r'recognizeFaces'] = this.recognizeFaces;
json[r'search'] = this.search; json[r'search'] = this.search;
json[r'sidecar'] = this.sidecar; json[r'sidecar'] = this.sidecar;
json[r'smartAlbum'] = this.smartAlbum;
json[r'storageTemplateMigration'] = this.storageTemplateMigration; json[r'storageTemplateMigration'] = this.storageTemplateMigration;
json[r'thumbnailGeneration'] = this.thumbnailGeneration; json[r'thumbnailGeneration'] = this.thumbnailGeneration;
json[r'videoConversion'] = this.videoConversion; json[r'videoConversion'] = this.videoConversion;
@ -105,6 +111,7 @@ class AllJobStatusResponseDto {
recognizeFaces: JobStatusDto.fromJson(json[r'recognizeFaces'])!, recognizeFaces: JobStatusDto.fromJson(json[r'recognizeFaces'])!,
search: JobStatusDto.fromJson(json[r'search'])!, search: JobStatusDto.fromJson(json[r'search'])!,
sidecar: JobStatusDto.fromJson(json[r'sidecar'])!, sidecar: JobStatusDto.fromJson(json[r'sidecar'])!,
smartAlbum: JobStatusDto.fromJson(json[r'smartAlbum'])!,
storageTemplateMigration: JobStatusDto.fromJson(json[r'storageTemplateMigration'])!, storageTemplateMigration: JobStatusDto.fromJson(json[r'storageTemplateMigration'])!,
thumbnailGeneration: JobStatusDto.fromJson(json[r'thumbnailGeneration'])!, thumbnailGeneration: JobStatusDto.fromJson(json[r'thumbnailGeneration'])!,
videoConversion: JobStatusDto.fromJson(json[r'videoConversion'])!, videoConversion: JobStatusDto.fromJson(json[r'videoConversion'])!,
@ -162,6 +169,7 @@ class AllJobStatusResponseDto {
'recognizeFaces', 'recognizeFaces',
'search', 'search',
'sidecar', 'sidecar',
'smartAlbum',
'storageTemplateMigration', 'storageTemplateMigration',
'thumbnailGeneration', 'thumbnailGeneration',
'videoConversion', 'videoConversion',

View file

@ -0,0 +1,114 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.12
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
part of openapi.api;
class CreateRuleDto {
/// Returns a new [CreateRuleDto] instance.
CreateRuleDto({
required this.albumId,
required this.key,
required this.value,
});
String albumId;
RuleKey key;
String value;
@override
bool operator ==(Object other) => identical(this, other) || other is CreateRuleDto &&
other.albumId == albumId &&
other.key == key &&
other.value == value;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(albumId.hashCode) +
(key.hashCode) +
(value.hashCode);
@override
String toString() => 'CreateRuleDto[albumId=$albumId, key=$key, value=$value]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
json[r'albumId'] = this.albumId;
json[r'key'] = this.key;
json[r'value'] = this.value;
return json;
}
/// Returns a new [CreateRuleDto] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static CreateRuleDto? fromJson(dynamic value) {
if (value is Map) {
final json = value.cast<String, dynamic>();
return CreateRuleDto(
albumId: mapValueOfType<String>(json, r'albumId')!,
key: RuleKey.fromJson(json[r'key'])!,
value: mapValueOfType<String>(json, r'value')!,
);
}
return null;
}
static List<CreateRuleDto> listFromJson(dynamic json, {bool growable = false,}) {
final result = <CreateRuleDto>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = CreateRuleDto.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, CreateRuleDto> mapFromJson(dynamic json) {
final map = <String, CreateRuleDto>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = CreateRuleDto.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of CreateRuleDto-objects as value to a dart map
static Map<String, List<CreateRuleDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<CreateRuleDto>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = CreateRuleDto.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
'albumId',
'key',
'value',
};
}

View file

@ -33,6 +33,7 @@ class JobName {
static const storageTemplateMigration = JobName._(r'storageTemplateMigration'); static const storageTemplateMigration = JobName._(r'storageTemplateMigration');
static const search = JobName._(r'search'); static const search = JobName._(r'search');
static const sidecar = JobName._(r'sidecar'); static const sidecar = JobName._(r'sidecar');
static const smartAlbum = JobName._(r'smartAlbum');
/// List of all possible values in this [enum][JobName]. /// List of all possible values in this [enum][JobName].
static const values = <JobName>[ static const values = <JobName>[
@ -46,6 +47,7 @@ class JobName {
storageTemplateMigration, storageTemplateMigration,
search, search,
sidecar, sidecar,
smartAlbum,
]; ];
static JobName? fromJson(dynamic value) => JobNameTypeTransformer().decode(value); static JobName? fromJson(dynamic value) => JobNameTypeTransformer().decode(value);
@ -94,6 +96,7 @@ class JobNameTypeTransformer {
case r'storageTemplateMigration': return JobName.storageTemplateMigration; case r'storageTemplateMigration': return JobName.storageTemplateMigration;
case r'search': return JobName.search; case r'search': return JobName.search;
case r'sidecar': return JobName.sidecar; case r'sidecar': return JobName.sidecar;
case r'smartAlbum': return JobName.smartAlbum;
default: default:
if (!allowNull) { if (!allowNull) {
throw ArgumentError('Unknown enum value to decode: $data'); throw ArgumentError('Unknown enum value to decode: $data');

103
mobile/openapi/lib/model/rule_key.dart generated Normal file
View file

@ -0,0 +1,103 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.12
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
part of openapi.api;
class RuleKey {
/// Instantiate a new enum with the provided [value].
const RuleKey._(this.value);
/// The underlying value of this enum member.
final String value;
@override
String toString() => value;
String toJson() => value;
static const person = RuleKey._(r'person');
static const takenAfter = RuleKey._(r'taken-after');
static const city = RuleKey._(r'city');
static const state = RuleKey._(r'state');
static const country = RuleKey._(r'country');
static const cameraMake = RuleKey._(r'camera-make');
static const cameraModel = RuleKey._(r'camera-model');
static const location = RuleKey._(r'location');
/// List of all possible values in this [enum][RuleKey].
static const values = <RuleKey>[
person,
takenAfter,
city,
state,
country,
cameraMake,
cameraModel,
location,
];
static RuleKey? fromJson(dynamic value) => RuleKeyTypeTransformer().decode(value);
static List<RuleKey>? listFromJson(dynamic json, {bool growable = false,}) {
final result = <RuleKey>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = RuleKey.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
}
/// Transformation class that can [encode] an instance of [RuleKey] to String,
/// and [decode] dynamic data back to [RuleKey].
class RuleKeyTypeTransformer {
factory RuleKeyTypeTransformer() => _instance ??= const RuleKeyTypeTransformer._();
const RuleKeyTypeTransformer._();
String encode(RuleKey data) => data.value;
/// Decodes a [dynamic value][data] to a RuleKey.
///
/// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully,
/// then null is returned. However, if [allowNull] is false and the [dynamic value][data]
/// cannot be decoded successfully, then an [UnimplementedError] is thrown.
///
/// The [allowNull] is very handy when an API changes and a new enum value is added or removed,
/// and users are still using an old app with the old code.
RuleKey? decode(dynamic data, {bool allowNull = true}) {
if (data != null) {
switch (data) {
case r'person': return RuleKey.person;
case r'taken-after': return RuleKey.takenAfter;
case r'city': return RuleKey.city;
case r'state': return RuleKey.state;
case r'country': return RuleKey.country;
case r'camera-make': return RuleKey.cameraMake;
case r'camera-model': return RuleKey.cameraModel;
case r'location': return RuleKey.location;
default:
if (!allowNull) {
throw ArgumentError('Unknown enum value to decode: $data');
}
}
}
return null;
}
/// Singleton [RuleKeyTypeTransformer] instance.
static RuleKeyTypeTransformer? _instance;
}

View file

@ -0,0 +1,122 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.12
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
part of openapi.api;
class RuleResponseDto {
/// Returns a new [RuleResponseDto] instance.
RuleResponseDto({
required this.id,
required this.key,
required this.ownerId,
required this.value,
});
String id;
RuleKey key;
String ownerId;
String value;
@override
bool operator ==(Object other) => identical(this, other) || other is RuleResponseDto &&
other.id == id &&
other.key == key &&
other.ownerId == ownerId &&
other.value == value;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(id.hashCode) +
(key.hashCode) +
(ownerId.hashCode) +
(value.hashCode);
@override
String toString() => 'RuleResponseDto[id=$id, key=$key, ownerId=$ownerId, value=$value]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
json[r'id'] = this.id;
json[r'key'] = this.key;
json[r'ownerId'] = this.ownerId;
json[r'value'] = this.value;
return json;
}
/// Returns a new [RuleResponseDto] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static RuleResponseDto? fromJson(dynamic value) {
if (value is Map) {
final json = value.cast<String, dynamic>();
return RuleResponseDto(
id: mapValueOfType<String>(json, r'id')!,
key: RuleKey.fromJson(json[r'key'])!,
ownerId: mapValueOfType<String>(json, r'ownerId')!,
value: mapValueOfType<String>(json, r'value')!,
);
}
return null;
}
static List<RuleResponseDto> listFromJson(dynamic json, {bool growable = false,}) {
final result = <RuleResponseDto>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = RuleResponseDto.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, RuleResponseDto> mapFromJson(dynamic json) {
final map = <String, RuleResponseDto>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = RuleResponseDto.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of RuleResponseDto-objects as value to a dart map
static Map<String, List<RuleResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<RuleResponseDto>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = RuleResponseDto.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
'id',
'key',
'ownerId',
'value',
};
}

View file

@ -20,6 +20,7 @@ class SystemConfigJobDto {
required this.recognizeFaces, required this.recognizeFaces,
required this.search, required this.search,
required this.sidecar, required this.sidecar,
required this.smartAlbum,
required this.storageTemplateMigration, required this.storageTemplateMigration,
required this.thumbnailGeneration, required this.thumbnailGeneration,
required this.videoConversion, required this.videoConversion,
@ -39,6 +40,8 @@ class SystemConfigJobDto {
JobSettingsDto sidecar; JobSettingsDto sidecar;
JobSettingsDto smartAlbum;
JobSettingsDto storageTemplateMigration; JobSettingsDto storageTemplateMigration;
JobSettingsDto thumbnailGeneration; JobSettingsDto thumbnailGeneration;
@ -54,6 +57,7 @@ class SystemConfigJobDto {
other.recognizeFaces == recognizeFaces && other.recognizeFaces == recognizeFaces &&
other.search == search && other.search == search &&
other.sidecar == sidecar && other.sidecar == sidecar &&
other.smartAlbum == smartAlbum &&
other.storageTemplateMigration == storageTemplateMigration && other.storageTemplateMigration == storageTemplateMigration &&
other.thumbnailGeneration == thumbnailGeneration && other.thumbnailGeneration == thumbnailGeneration &&
other.videoConversion == videoConversion; other.videoConversion == videoConversion;
@ -68,12 +72,13 @@ class SystemConfigJobDto {
(recognizeFaces.hashCode) + (recognizeFaces.hashCode) +
(search.hashCode) + (search.hashCode) +
(sidecar.hashCode) + (sidecar.hashCode) +
(smartAlbum.hashCode) +
(storageTemplateMigration.hashCode) + (storageTemplateMigration.hashCode) +
(thumbnailGeneration.hashCode) + (thumbnailGeneration.hashCode) +
(videoConversion.hashCode); (videoConversion.hashCode);
@override @override
String toString() => 'SystemConfigJobDto[backgroundTask=$backgroundTask, clipEncoding=$clipEncoding, metadataExtraction=$metadataExtraction, objectTagging=$objectTagging, recognizeFaces=$recognizeFaces, search=$search, sidecar=$sidecar, storageTemplateMigration=$storageTemplateMigration, thumbnailGeneration=$thumbnailGeneration, videoConversion=$videoConversion]'; String toString() => 'SystemConfigJobDto[backgroundTask=$backgroundTask, clipEncoding=$clipEncoding, metadataExtraction=$metadataExtraction, objectTagging=$objectTagging, recognizeFaces=$recognizeFaces, search=$search, sidecar=$sidecar, smartAlbum=$smartAlbum, storageTemplateMigration=$storageTemplateMigration, thumbnailGeneration=$thumbnailGeneration, videoConversion=$videoConversion]';
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final json = <String, dynamic>{}; final json = <String, dynamic>{};
@ -84,6 +89,7 @@ class SystemConfigJobDto {
json[r'recognizeFaces'] = this.recognizeFaces; json[r'recognizeFaces'] = this.recognizeFaces;
json[r'search'] = this.search; json[r'search'] = this.search;
json[r'sidecar'] = this.sidecar; json[r'sidecar'] = this.sidecar;
json[r'smartAlbum'] = this.smartAlbum;
json[r'storageTemplateMigration'] = this.storageTemplateMigration; json[r'storageTemplateMigration'] = this.storageTemplateMigration;
json[r'thumbnailGeneration'] = this.thumbnailGeneration; json[r'thumbnailGeneration'] = this.thumbnailGeneration;
json[r'videoConversion'] = this.videoConversion; json[r'videoConversion'] = this.videoConversion;
@ -105,6 +111,7 @@ class SystemConfigJobDto {
recognizeFaces: JobSettingsDto.fromJson(json[r'recognizeFaces'])!, recognizeFaces: JobSettingsDto.fromJson(json[r'recognizeFaces'])!,
search: JobSettingsDto.fromJson(json[r'search'])!, search: JobSettingsDto.fromJson(json[r'search'])!,
sidecar: JobSettingsDto.fromJson(json[r'sidecar'])!, sidecar: JobSettingsDto.fromJson(json[r'sidecar'])!,
smartAlbum: JobSettingsDto.fromJson(json[r'smartAlbum'])!,
storageTemplateMigration: JobSettingsDto.fromJson(json[r'storageTemplateMigration'])!, storageTemplateMigration: JobSettingsDto.fromJson(json[r'storageTemplateMigration'])!,
thumbnailGeneration: JobSettingsDto.fromJson(json[r'thumbnailGeneration'])!, thumbnailGeneration: JobSettingsDto.fromJson(json[r'thumbnailGeneration'])!,
videoConversion: JobSettingsDto.fromJson(json[r'videoConversion'])!, videoConversion: JobSettingsDto.fromJson(json[r'videoConversion'])!,
@ -162,6 +169,7 @@ class SystemConfigJobDto {
'recognizeFaces', 'recognizeFaces',
'search', 'search',
'sidecar', 'sidecar',
'smartAlbum',
'storageTemplateMigration', 'storageTemplateMigration',
'thumbnailGeneration', 'thumbnailGeneration',
'videoConversion', 'videoConversion',

View file

@ -0,0 +1,106 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.12
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
part of openapi.api;
class UpdateRuleDto {
/// Returns a new [UpdateRuleDto] instance.
UpdateRuleDto({
required this.key,
required this.value,
});
RuleKey key;
String value;
@override
bool operator ==(Object other) => identical(this, other) || other is UpdateRuleDto &&
other.key == key &&
other.value == value;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(key.hashCode) +
(value.hashCode);
@override
String toString() => 'UpdateRuleDto[key=$key, value=$value]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
json[r'key'] = this.key;
json[r'value'] = this.value;
return json;
}
/// Returns a new [UpdateRuleDto] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static UpdateRuleDto? fromJson(dynamic value) {
if (value is Map) {
final json = value.cast<String, dynamic>();
return UpdateRuleDto(
key: RuleKey.fromJson(json[r'key'])!,
value: mapValueOfType<String>(json, r'value')!,
);
}
return null;
}
static List<UpdateRuleDto> listFromJson(dynamic json, {bool growable = false,}) {
final result = <UpdateRuleDto>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = UpdateRuleDto.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, UpdateRuleDto> mapFromJson(dynamic json) {
final map = <String, UpdateRuleDto>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = UpdateRuleDto.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of UpdateRuleDto-objects as value to a dart map
static Map<String, List<UpdateRuleDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<UpdateRuleDto>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = UpdateRuleDto.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
'key',
'value',
};
}

View file

@ -76,6 +76,11 @@ void main() {
// TODO // TODO
}); });
// List<RuleResponseDto> rules (default value: const [])
test('to test the property `rules`', () async {
// TODO
});
// bool shared // bool shared
test('to test the property `shared`', () async { test('to test the property `shared`', () async {
// TODO // TODO

View file

@ -51,6 +51,11 @@ void main() {
// TODO // TODO
}); });
// JobStatusDto smartAlbum
test('to test the property `smartAlbum`', () async {
// TODO
});
// JobStatusDto storageTemplateMigration // JobStatusDto storageTemplateMigration
test('to test the property `storageTemplateMigration`', () async { test('to test the property `storageTemplateMigration`', () async {
// TODO // TODO

View file

@ -0,0 +1,37 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.12
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
import 'package:openapi/api.dart';
import 'package:test/test.dart';
// tests for CreateRuleDto
void main() {
// final instance = CreateRuleDto();
group('test CreateRuleDto', () {
// String albumId
test('to test the property `albumId`', () async {
// TODO
});
// RuleKey key
test('to test the property `key`', () async {
// TODO
});
// String value
test('to test the property `value`', () async {
// TODO
});
});
}

41
mobile/openapi/test/rule_api_test.dart generated Normal file
View file

@ -0,0 +1,41 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.12
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
import 'package:openapi/api.dart';
import 'package:test/test.dart';
/// tests for RuleApi
void main() {
// final instance = RuleApi();
group('tests for RuleApi', () {
//Future<RuleResponseDto> createRule(CreateRuleDto createRuleDto) async
test('test createRule', () async {
// TODO
});
//Future<RuleResponseDto> getRule(String id) async
test('test getRule', () async {
// TODO
});
//Future removeRule(String id) async
test('test removeRule', () async {
// TODO
});
//Future<RuleResponseDto> updateRule(String id, UpdateRuleDto updateRuleDto) async
test('test updateRule', () async {
// TODO
});
});
}

21
mobile/openapi/test/rule_key_test.dart generated Normal file
View file

@ -0,0 +1,21 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.12
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
import 'package:openapi/api.dart';
import 'package:test/test.dart';
// tests for RuleKey
void main() {
group('test RuleKey', () {
});
}

View file

@ -0,0 +1,42 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.12
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
import 'package:openapi/api.dart';
import 'package:test/test.dart';
// tests for RuleResponseDto
void main() {
// final instance = RuleResponseDto();
group('test RuleResponseDto', () {
// String id
test('to test the property `id`', () async {
// TODO
});
// RuleKey key
test('to test the property `key`', () async {
// TODO
});
// String ownerId
test('to test the property `ownerId`', () async {
// TODO
});
// String value
test('to test the property `value`', () async {
// TODO
});
});
}

View file

@ -51,6 +51,11 @@ void main() {
// TODO // TODO
}); });
// JobSettingsDto smartAlbum
test('to test the property `smartAlbum`', () async {
// TODO
});
// JobSettingsDto storageTemplateMigration // JobSettingsDto storageTemplateMigration
test('to test the property `storageTemplateMigration`', () async { test('to test the property `storageTemplateMigration`', () async {
// TODO // TODO

View file

@ -0,0 +1,32 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.12
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
import 'package:openapi/api.dart';
import 'package:test/test.dart';
// tests for UpdateRuleDto
void main() {
// final instance = UpdateRuleDto();
group('test UpdateRuleDto', () {
// RuleKey key
test('to test the property `key`', () async {
// TODO
});
// String value
test('to test the property `value`', () async {
// TODO
});
});
}

View file

@ -2976,6 +2976,173 @@
] ]
} }
}, },
"/rule": {
"post": {
"operationId": "createRule",
"parameters": [],
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CreateRuleDto"
}
}
},
"required": true
},
"responses": {
"201": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/RuleResponseDto"
}
}
},
"description": ""
}
},
"security": [
{
"bearer": []
},
{
"cookie": []
},
{
"api_key": []
}
],
"tags": [
"Rule"
]
}
},
"/rule/{id}": {
"delete": {
"operationId": "removeRule",
"parameters": [
{
"name": "id",
"required": true,
"in": "path",
"schema": {
"format": "uuid",
"type": "string"
}
}
],
"responses": {
"200": {
"description": ""
}
},
"security": [
{
"bearer": []
},
{
"cookie": []
},
{
"api_key": []
}
],
"tags": [
"Rule"
]
},
"get": {
"operationId": "getRule",
"parameters": [
{
"name": "id",
"required": true,
"in": "path",
"schema": {
"format": "uuid",
"type": "string"
}
}
],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/RuleResponseDto"
}
}
},
"description": ""
}
},
"security": [
{
"bearer": []
},
{
"cookie": []
},
{
"api_key": []
}
],
"tags": [
"Rule"
]
},
"put": {
"operationId": "updateRule",
"parameters": [
{
"name": "id",
"required": true,
"in": "path",
"schema": {
"format": "uuid",
"type": "string"
}
}
],
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UpdateRuleDto"
}
}
},
"required": true
},
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/RuleResponseDto"
}
}
},
"description": ""
}
},
"security": [
{
"bearer": []
},
{
"cookie": []
},
{
"api_key": []
}
],
"tags": [
"Rule"
]
}
},
"/search": { "/search": {
"get": { "get": {
"operationId": "search", "operationId": "search",
@ -4792,6 +4959,12 @@
"ownerId": { "ownerId": {
"type": "string" "type": "string"
}, },
"rules": {
"items": {
"$ref": "#/components/schemas/RuleResponseDto"
},
"type": "array"
},
"shared": { "shared": {
"type": "boolean" "type": "boolean"
}, },
@ -4823,7 +4996,8 @@
"sharedUsers", "sharedUsers",
"hasSharedLink", "hasSharedLink",
"assets", "assets",
"owner" "owner",
"rules"
], ],
"type": "object" "type": "object"
}, },
@ -4850,6 +5024,9 @@
"sidecar": { "sidecar": {
"$ref": "#/components/schemas/JobStatusDto" "$ref": "#/components/schemas/JobStatusDto"
}, },
"smartAlbum": {
"$ref": "#/components/schemas/JobStatusDto"
},
"storageTemplateMigration": { "storageTemplateMigration": {
"$ref": "#/components/schemas/JobStatusDto" "$ref": "#/components/schemas/JobStatusDto"
}, },
@ -4870,7 +5047,8 @@
"backgroundTask", "backgroundTask",
"search", "search",
"recognizeFaces", "recognizeFaces",
"sidecar" "sidecar",
"smartAlbum"
], ],
"type": "object" "type": "object"
}, },
@ -5414,6 +5592,26 @@
], ],
"type": "object" "type": "object"
}, },
"CreateRuleDto": {
"properties": {
"albumId": {
"format": "uuid",
"type": "string"
},
"key": {
"$ref": "#/components/schemas/RuleKey"
},
"value": {
"type": "string"
}
},
"required": [
"key",
"value",
"albumId"
],
"type": "object"
},
"CreateTagDto": { "CreateTagDto": {
"properties": { "properties": {
"name": { "name": {
@ -5846,7 +6044,8 @@
"backgroundTask", "backgroundTask",
"storageTemplateMigration", "storageTemplateMigration",
"search", "search",
"sidecar" "sidecar",
"smartAlbum"
], ],
"type": "string" "type": "string"
}, },
@ -6170,6 +6369,42 @@
], ],
"type": "object" "type": "object"
}, },
"RuleKey": {
"enum": [
"person",
"taken-after",
"city",
"state",
"country",
"camera-make",
"camera-model",
"location"
],
"type": "string"
},
"RuleResponseDto": {
"properties": {
"id": {
"type": "string"
},
"key": {
"$ref": "#/components/schemas/RuleKey"
},
"ownerId": {
"type": "string"
},
"value": {
"type": "string"
}
},
"required": [
"key",
"value",
"id",
"ownerId"
],
"type": "object"
},
"SearchAlbumResponseDto": { "SearchAlbumResponseDto": {
"properties": { "properties": {
"count": { "count": {
@ -6753,6 +6988,9 @@
"sidecar": { "sidecar": {
"$ref": "#/components/schemas/JobSettingsDto" "$ref": "#/components/schemas/JobSettingsDto"
}, },
"smartAlbum": {
"$ref": "#/components/schemas/JobSettingsDto"
},
"storageTemplateMigration": { "storageTemplateMigration": {
"$ref": "#/components/schemas/JobSettingsDto" "$ref": "#/components/schemas/JobSettingsDto"
}, },
@ -6773,7 +7011,8 @@
"backgroundTask", "backgroundTask",
"search", "search",
"recognizeFaces", "recognizeFaces",
"sidecar" "sidecar",
"smartAlbum"
], ],
"type": "object" "type": "object"
}, },
@ -7049,6 +7288,21 @@
}, },
"type": "object" "type": "object"
}, },
"UpdateRuleDto": {
"properties": {
"key": {
"$ref": "#/components/schemas/RuleKey"
},
"value": {
"type": "string"
}
},
"required": [
"key",
"value"
],
"type": "object"
},
"UpdateTagDto": { "UpdateTagDto": {
"properties": { "properties": {
"name": { "name": {

View file

@ -19,6 +19,11 @@ export enum Permission {
ALBUM_SHARE = 'album.share', ALBUM_SHARE = 'album.share',
ALBUM_DOWNLOAD = 'album.download', ALBUM_DOWNLOAD = 'album.download',
RULE_READ = 'rule.read',
RULE_CREATE = 'rule.create',
RULE_UPDATE = 'rule.update',
RULE_DELETE = 'rule.delete',
ARCHIVE_READ = 'archive.read', ARCHIVE_READ = 'archive.read',
LIBRARY_READ = 'library.read', LIBRARY_READ = 'library.read',
@ -158,6 +163,18 @@ export class AccessCore {
case Permission.ALBUM_REMOVE_ASSET: case Permission.ALBUM_REMOVE_ASSET:
return this.repository.album.hasOwnerAccess(authUser.id, id); return this.repository.album.hasOwnerAccess(authUser.id, id);
case Permission.RULE_CREATE:
// id is albumId here
return (
(await this.repository.album.hasOwnerAccess(authUser.id, id)) ||
(await this.repository.album.hasSharedAlbumAccess(authUser.id, id))
);
case Permission.RULE_READ:
case Permission.RULE_UPDATE:
case Permission.RULE_DELETE:
return this.repository.rule.hasOwnerAccess(authUser.id, id);
case Permission.ARCHIVE_READ: case Permission.ARCHIVE_READ:
return authUser.id === id; return authUser.id === id;

View file

@ -14,6 +14,10 @@ export interface IAccessRepository {
hasSharedLinkAccess(sharedLinkId: string, albumId: string): Promise<boolean>; hasSharedLinkAccess(sharedLinkId: string, albumId: string): Promise<boolean>;
}; };
rule: {
hasOwnerAccess(userId: string, ruleId: string): Promise<boolean>;
};
library: { library: {
hasPartnerAccess(userId: string, partnerId: string): Promise<boolean>; hasPartnerAccess(userId: string, partnerId: string): Promise<boolean>;
}; };

View file

@ -2,6 +2,7 @@ import { AlbumEntity } from '@app/infra/entities';
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
import { AssetResponseDto, mapAsset } from '../asset'; import { AssetResponseDto, mapAsset } from '../asset';
import { mapUser, UserResponseDto } from '../user'; import { mapUser, UserResponseDto } from '../user';
import { mapRule, RuleResponseDto } from './dto';
export class AlbumResponseDto { export class AlbumResponseDto {
id!: string; id!: string;
@ -21,16 +22,11 @@ export class AlbumResponseDto {
lastModifiedAssetTimestamp?: Date; lastModifiedAssetTimestamp?: Date;
startDate?: Date; startDate?: Date;
endDate?: Date; endDate?: Date;
rules!: RuleResponseDto[];
} }
export const mapAlbum = (entity: AlbumEntity, withAssets: boolean): AlbumResponseDto => { export const mapAlbum = (entity: AlbumEntity, withAssets: boolean): AlbumResponseDto => {
const sharedUsers: UserResponseDto[] = []; const sharedUsers: UserResponseDto[] = (entity.sharedUsers || []).map(mapUser);
entity.sharedUsers?.forEach((user) => {
const userDto = mapUser(user);
sharedUsers.push(userDto);
});
const assets = entity.assets || []; const assets = entity.assets || [];
const hasSharedLink = entity.sharedLinks?.length > 0; const hasSharedLink = entity.sharedLinks?.length > 0;
@ -52,6 +48,7 @@ export const mapAlbum = (entity: AlbumEntity, withAssets: boolean): AlbumRespons
endDate: assets.at(-1)?.fileCreatedAt || undefined, endDate: assets.at(-1)?.fileCreatedAt || undefined,
assets: (withAssets ? assets : []).map((asset) => mapAsset(asset)), assets: (withAssets ? assets : []).map((asset) => mapAsset(asset)),
assetCount: entity.assets?.length || 0, assetCount: entity.assets?.length || 0,
rules: (entity.rules || []).map(mapRule),
}; };
}; };

View file

@ -185,6 +185,7 @@ describe(AlbumService.name, () => {
endDate: undefined, endDate: undefined,
hasSharedLink: false, hasSharedLink: false,
updatedAt: expect.anything(), updatedAt: expect.anything(),
rules: [],
}); });
expect(jobMock.queue).toHaveBeenCalledWith({ expect(jobMock.queue).toHaveBeenCalledWith({

View file

@ -3,3 +3,4 @@ export * from './album-create.dto';
export * from './album-update.dto'; export * from './album-update.dto';
export * from './album.dto'; export * from './album.dto';
export * from './get-albums.dto'; export * from './get-albums.dto';
export * from '../../rule/rule.dto';

View file

@ -18,6 +18,7 @@ import { StorageTemplateService } from './storage-template';
import { INITIAL_SYSTEM_CONFIG, SystemConfigService } from './system-config'; import { INITIAL_SYSTEM_CONFIG, SystemConfigService } from './system-config';
import { TagService } from './tag'; import { TagService } from './tag';
import { UserService } from './user'; import { UserService } from './user';
import { RuleService } from './rule';
const providers: Provider[] = [ const providers: Provider[] = [
AlbumService, AlbumService,
@ -30,6 +31,7 @@ const providers: Provider[] = [
MetadataService, MetadataService,
PersonService, PersonService,
PartnerService, PartnerService,
RuleService,
SearchService, SearchService,
ServerInfoService, ServerInfoService,
SharedLinkService, SharedLinkService,

View file

@ -15,6 +15,7 @@ export * from './media';
export * from './metadata'; export * from './metadata';
export * from './partner'; export * from './partner';
export * from './person'; export * from './person';
export * from './rule';
export * from './search'; export * from './search';
export * from './server-info'; export * from './server-info';
export * from './shared-link'; export * from './shared-link';

View file

@ -9,6 +9,7 @@ export enum QueueName {
STORAGE_TEMPLATE_MIGRATION = 'storageTemplateMigration', STORAGE_TEMPLATE_MIGRATION = 'storageTemplateMigration',
SEARCH = 'search', SEARCH = 'search',
SIDECAR = 'sidecar', SIDECAR = 'sidecar',
SMART_ALBUM = 'smartAlbum',
} }
export enum JobCommand { export enum JobCommand {
@ -75,6 +76,9 @@ export enum JobName {
QUEUE_SIDECAR = 'queue-sidecar', QUEUE_SIDECAR = 'queue-sidecar',
SIDECAR_DISCOVERY = 'sidecar-discovery', SIDECAR_DISCOVERY = 'sidecar-discovery',
SIDECAR_SYNC = 'sidecar-sync', SIDECAR_SYNC = 'sidecar-sync',
// smart albums
SMART_ALBUM_INDEX = 'smart-album-index',
} }
export const JOBS_ASSET_PAGINATION_SIZE = 1000; export const JOBS_ASSET_PAGINATION_SIZE = 1000;
@ -138,4 +142,7 @@ export const JOBS_TO_QUEUE: Record<JobName, QueueName> = {
[JobName.QUEUE_SIDECAR]: QueueName.SIDECAR, [JobName.QUEUE_SIDECAR]: QueueName.SIDECAR,
[JobName.SIDECAR_DISCOVERY]: QueueName.SIDECAR, [JobName.SIDECAR_DISCOVERY]: QueueName.SIDECAR,
[JobName.SIDECAR_SYNC]: QueueName.SIDECAR, [JobName.SIDECAR_SYNC]: QueueName.SIDECAR,
// smart albums
[JobName.SMART_ALBUM_INDEX]: QueueName.SMART_ALBUM,
}; };

View file

@ -78,4 +78,7 @@ export class AllJobStatusResponseDto implements Record<QueueName, JobStatusDto>
@ApiProperty({ type: JobStatusDto }) @ApiProperty({ type: JobStatusDto })
[QueueName.SIDECAR]!: JobStatusDto; [QueueName.SIDECAR]!: JobStatusDto;
@ApiProperty({ type: JobStatusDto })
[QueueName.SMART_ALBUM]!: JobStatusDto;
} }

View file

@ -29,3 +29,13 @@ export interface IBulkEntityJob extends IBaseJob {
export interface IDeleteFilesJob extends IBaseJob { export interface IDeleteFilesJob extends IBaseJob {
files: Array<string | null | undefined>; files: Array<string | null | undefined>;
} }
export interface ISmartAlbumIndexJob extends IBaseJob {
albumId: string;
ruleId: string;
}
export interface ISmartAlbumInsertJob extends IBaseJob {
albumId: string;
assetId: string;
}

View file

@ -6,6 +6,8 @@ import {
IDeleteFilesJob, IDeleteFilesJob,
IEntityJob, IEntityJob,
IFaceThumbnailJob, IFaceThumbnailJob,
ISmartAlbumIndexJob,
ISmartAlbumInsertJob,
} from './job.interface'; } from './job.interface';
export interface JobCounts { export interface JobCounts {
@ -80,7 +82,10 @@ export type JobItem =
| { name: JobName.SEARCH_INDEX_ALBUM; data: IBulkEntityJob } | { name: JobName.SEARCH_INDEX_ALBUM; data: IBulkEntityJob }
| { name: JobName.SEARCH_REMOVE_ASSET; data: IBulkEntityJob } | { name: JobName.SEARCH_REMOVE_ASSET; data: IBulkEntityJob }
| { name: JobName.SEARCH_REMOVE_ALBUM; data: IBulkEntityJob } | { name: JobName.SEARCH_REMOVE_ALBUM; data: IBulkEntityJob }
| { name: JobName.SEARCH_REMOVE_FACE; data: IAssetFaceJob }; | { name: JobName.SEARCH_REMOVE_FACE; data: IAssetFaceJob }
// Smart album
| { name: JobName.SMART_ALBUM_INDEX; data: ISmartAlbumIndexJob };
export type JobHandler<T = any> = (data: T) => boolean | Promise<boolean>; export type JobHandler<T = any> = (data: T) => boolean | Promise<boolean>;

View file

@ -95,6 +95,7 @@ describe(JobService.name, () => {
[QueueName.THUMBNAIL_GENERATION]: expectedJobStatus, [QueueName.THUMBNAIL_GENERATION]: expectedJobStatus,
[QueueName.VIDEO_CONVERSION]: expectedJobStatus, [QueueName.VIDEO_CONVERSION]: expectedJobStatus,
[QueueName.RECOGNIZE_FACES]: expectedJobStatus, [QueueName.RECOGNIZE_FACES]: expectedJobStatus,
[QueueName.SMART_ALBUM]: expectedJobStatus,
[QueueName.SIDECAR]: expectedJobStatus, [QueueName.SIDECAR]: expectedJobStatus,
}); });
}); });
@ -224,6 +225,7 @@ describe(JobService.name, () => {
[QueueName.RECOGNIZE_FACES]: { concurrency: 10 }, [QueueName.RECOGNIZE_FACES]: { concurrency: 10 },
[QueueName.SEARCH]: { concurrency: 10 }, [QueueName.SEARCH]: { concurrency: 10 },
[QueueName.SIDECAR]: { concurrency: 10 }, [QueueName.SIDECAR]: { concurrency: 10 },
[QueueName.SMART_ALBUM]: { concurrency: 10 },
[QueueName.STORAGE_TEMPLATE_MIGRATION]: { concurrency: 10 }, [QueueName.STORAGE_TEMPLATE_MIGRATION]: { concurrency: 10 },
[QueueName.THUMBNAIL_GENERATION]: { concurrency: 10 }, [QueueName.THUMBNAIL_GENERATION]: { concurrency: 10 },
[QueueName.VIDEO_CONVERSION]: { concurrency: 10 }, [QueueName.VIDEO_CONVERSION]: { concurrency: 10 },
@ -236,6 +238,7 @@ describe(JobService.name, () => {
expect(jobMock.setConcurrency).toHaveBeenCalledWith(QueueName.OBJECT_TAGGING, 10); expect(jobMock.setConcurrency).toHaveBeenCalledWith(QueueName.OBJECT_TAGGING, 10);
expect(jobMock.setConcurrency).toHaveBeenCalledWith(QueueName.RECOGNIZE_FACES, 10); expect(jobMock.setConcurrency).toHaveBeenCalledWith(QueueName.RECOGNIZE_FACES, 10);
expect(jobMock.setConcurrency).toHaveBeenCalledWith(QueueName.SIDECAR, 10); expect(jobMock.setConcurrency).toHaveBeenCalledWith(QueueName.SIDECAR, 10);
expect(jobMock.setConcurrency).toHaveBeenCalledWith(QueueName.SMART_ALBUM, 10);
expect(jobMock.setConcurrency).toHaveBeenCalledWith(QueueName.STORAGE_TEMPLATE_MIGRATION, 10); expect(jobMock.setConcurrency).toHaveBeenCalledWith(QueueName.STORAGE_TEMPLATE_MIGRATION, 10);
expect(jobMock.setConcurrency).toHaveBeenCalledWith(QueueName.THUMBNAIL_GENERATION, 10); expect(jobMock.setConcurrency).toHaveBeenCalledWith(QueueName.THUMBNAIL_GENERATION, 10);
expect(jobMock.setConcurrency).toHaveBeenCalledWith(QueueName.VIDEO_CONVERSION, 10); expect(jobMock.setConcurrency).toHaveBeenCalledWith(QueueName.VIDEO_CONVERSION, 10);

View file

@ -0,0 +1,3 @@
export * from './rule.dto';
export * from './rule.repository';
export * from './rule.service';

View file

@ -0,0 +1,101 @@
import { GeoRuleValue, RuleEntity, RuleKey, RuleValue, RuleValueType, RULE_TO_TYPE } from '@app/infra/entities';
import { ApiProperty } from '@nestjs/swagger';
import { ClassConstructor, plainToInstance, Transform, Type } from 'class-transformer';
import {
IsDate,
IsEnum,
IsLatitude,
IsLongitude,
IsNotEmpty,
IsNumber,
IsPositive,
IsString,
IsUUID,
ValidateNested,
} from 'class-validator';
import { ValidateUUID } from '../domain.util';
class UUIDRuleDto {
@IsUUID(4)
value!: string;
}
class StringRuleDto {
@IsString()
@IsNotEmpty()
value!: string;
}
class DateRuleDto {
@IsDate()
@Type(() => Date)
value!: Date;
}
class GeoRuleValueDto implements GeoRuleValue {
@IsLatitude()
lat!: number;
@IsLongitude()
lng!: number;
@IsPositive()
@IsNumber()
radius!: number;
}
class GeoRuleDto {
@ValidateNested()
@Type(() => GeoRuleValueDto)
value!: GeoRuleValueDto;
}
const toRuleValueDto = (key: RuleKey, value: RuleValue) => {
const type = RULE_TO_TYPE[key];
const map: Record<RuleValueType, ClassConstructor<{ value: RuleValue }>> = {
[RuleValueType.UUID]: UUIDRuleDto,
[RuleValueType.STRING]: StringRuleDto,
[RuleValueType.DATE]: DateRuleDto,
[RuleValueType.GEO]: GeoRuleDto,
};
if (type && map[type]) {
return plainToInstance(map[type], { value });
}
return value;
};
export class UpdateRuleDto {
@ApiProperty({ enumName: 'RuleKey', enum: RuleKey })
@IsEnum(RuleKey)
key!: RuleKey;
@ApiProperty({ type: String })
@ValidateNested()
@Transform(({ obj, value }) => toRuleValueDto(obj.key, value))
value!: { value: RuleValue };
}
export class CreateRuleDto extends UpdateRuleDto {
@ValidateUUID()
albumId!: string;
}
export class RuleResponseDto {
id!: string;
@ApiProperty({ enumName: 'RuleKey', enum: RuleKey })
key!: RuleKey;
@ApiProperty({ type: String })
value!: any;
ownerId!: string;
}
export const mapRule = (rule: RuleEntity): RuleResponseDto => {
return {
id: rule.id,
key: rule.key,
value: rule.value,
ownerId: rule.ownerId,
};
};

View file

@ -0,0 +1,10 @@
import { RuleEntity } from '@app/infra/entities';
export const IRuleRepository = 'IRuleRepository';
export interface IRuleRepository {
get(id: string): Promise<RuleEntity | null>;
create(rule: Partial<RuleEntity>): Promise<RuleEntity>;
update(rule: Partial<RuleEntity>): Promise<RuleEntity>;
delete(rule: RuleEntity): Promise<RuleEntity>;
}

View file

@ -0,0 +1,84 @@
import { BadRequestException } from '@nestjs/common';
import { authStub, IAccessRepositoryMock, newAccessRepositoryMock, newRuleRepositoryMock, ruleStub } from '@test';
import { RuleKey } from '../../infra/entities/rule.entity';
import { RuleResponseDto } from './rule.dto';
import { IRuleRepository } from './rule.repository';
import { RuleService } from './rule.service';
const responseDto: RuleResponseDto = {
id: 'rule-1',
key: RuleKey.CITY,
value: 'Chandler',
ownerId: authStub.admin.id,
};
describe(RuleService.name, () => {
let sut: RuleService;
let accessMock: jest.Mocked<IAccessRepositoryMock>;
let ruleMock: jest.Mocked<IRuleRepository>;
beforeEach(async () => {
accessMock = newAccessRepositoryMock();
ruleMock = newRuleRepositoryMock();
sut = new RuleService(accessMock, ruleMock);
});
it('should be defined', () => {
expect(sut).toBeDefined();
});
describe('create', () => {
it('should require album access', async () => {
accessMock.album.hasOwnerAccess.mockResolvedValue(false);
accessMock.album.hasSharedAlbumAccess.mockResolvedValue(false);
await expect(
sut.create(authStub.admin, {
albumId: 'not-found-album',
key: RuleKey.CITY,
value: { value: 'abc' },
}),
).rejects.toBeInstanceOf(BadRequestException);
expect(accessMock.album.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'not-found-album');
expect(accessMock.album.hasSharedAlbumAccess).toHaveBeenCalledWith(authStub.admin.id, 'not-found-album');
});
it('should create a rule', async () => {
accessMock.album.hasOwnerAccess.mockResolvedValue(true);
ruleMock.create.mockResolvedValue(ruleStub.rule1);
await expect(
sut.create(authStub.admin, {
albumId: 'album-123',
key: RuleKey.CITY,
value: { value: 'abc' },
}),
).resolves.toEqual(responseDto);
expect(accessMock.album.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'album-123');
});
});
describe('get', () => {
it('should throw a bad request when the rule is not found', async () => {
accessMock.rule.hasOwnerAccess.mockResolvedValue(false);
await expect(sut.get(authStub.admin, 'rule-1')).rejects.toBeInstanceOf(BadRequestException);
});
it('should get a rule by id', async () => {
accessMock.rule.hasOwnerAccess.mockResolvedValue(true);
ruleMock.get.mockResolvedValue(ruleStub.rule1);
await expect(sut.get(authStub.admin, 'rule-1')).resolves.toEqual(responseDto);
expect(ruleMock.get).toHaveBeenCalledWith('rule-1');
});
});
describe('update', () => {
it('should throw a bad request when the rule is not found', async () => {
accessMock.rule.hasOwnerAccess.mockResolvedValue(false);
await expect(
sut.update(authStub.admin, 'rule-1', {
key: RuleKey.CITY,
value: { value: 'Atlanta' },
}),
).rejects.toBeInstanceOf(BadRequestException);
});
});
});

View file

@ -0,0 +1,60 @@
import { BadRequestException, Inject, Injectable } from '@nestjs/common';
import { RuleKey } from '../../infra/entities/rule.entity';
import { AccessCore, IAccessRepository, Permission } from '../access';
import { AuthUserDto } from '../auth';
import { CreateRuleDto, mapRule, UpdateRuleDto } from './rule.dto';
import { IRuleRepository } from './rule.repository';
@Injectable()
export class RuleService {
private access: AccessCore;
constructor(
@Inject(IAccessRepository) accessRepository: IAccessRepository,
@Inject(IRuleRepository) private repository: IRuleRepository,
) {
this.access = new AccessCore(accessRepository);
}
async get(authUser: AuthUserDto, id: string) {
await this.access.requirePermission(authUser, Permission.RULE_READ, id);
const rule = await this.findOrFail(id);
return mapRule(rule);
}
async create(authUser: AuthUserDto, dto: CreateRuleDto) {
await this.access.requirePermission(authUser, Permission.RULE_CREATE, dto.albumId);
if (dto.key === RuleKey.PERSON) {
// TODO: validate personId
}
const rule = await this.repository.create({
key: dto.key,
value: dto.value.value,
albumId: dto.albumId,
ownerId: authUser.id,
});
return mapRule(rule);
}
async update(authUser: AuthUserDto, id: string, dto: UpdateRuleDto) {
await this.access.requirePermission(authUser, Permission.RULE_UPDATE, id);
const rule = await this.repository.update({ id, key: dto.key, value: dto.value.value });
return mapRule(rule);
}
async remove(authUser: AuthUserDto, id: string) {
await this.access.requirePermission(authUser, Permission.RULE_DELETE, id);
const rule = await this.findOrFail(id);
await this.repository.delete(rule);
}
private async findOrFail(id: string) {
const rule = await this.repository.get(id);
if (!rule) {
throw new BadRequestException('Rule not found');
}
return rule;
}
}

View file

@ -70,4 +70,10 @@ export class SystemConfigJobDto implements Record<QueueName, JobSettingsDto> {
@IsObject() @IsObject()
@Type(() => JobSettingsDto) @Type(() => JobSettingsDto)
[QueueName.SIDECAR]!: JobSettingsDto; [QueueName.SIDECAR]!: JobSettingsDto;
@ApiProperty({ type: JobSettingsDto })
@ValidateNested()
@IsObject()
@Type(() => JobSettingsDto)
[QueueName.SMART_ALBUM]!: JobSettingsDto;
} }

View file

@ -43,6 +43,7 @@ export const defaults = Object.freeze<SystemConfig>({
[QueueName.STORAGE_TEMPLATE_MIGRATION]: { concurrency: 5 }, [QueueName.STORAGE_TEMPLATE_MIGRATION]: { concurrency: 5 },
[QueueName.THUMBNAIL_GENERATION]: { concurrency: 5 }, [QueueName.THUMBNAIL_GENERATION]: { concurrency: 5 },
[QueueName.VIDEO_CONVERSION]: { concurrency: 1 }, [QueueName.VIDEO_CONVERSION]: { concurrency: 1 },
[QueueName.SMART_ALBUM]: { concurrency: 1 },
}, },
oauth: { oauth: {
enabled: false, enabled: false,

View file

@ -29,6 +29,7 @@ const updatedConfig = Object.freeze<SystemConfig>({
[QueueName.RECOGNIZE_FACES]: { concurrency: 2 }, [QueueName.RECOGNIZE_FACES]: { concurrency: 2 },
[QueueName.SEARCH]: { concurrency: 5 }, [QueueName.SEARCH]: { concurrency: 5 },
[QueueName.SIDECAR]: { concurrency: 5 }, [QueueName.SIDECAR]: { concurrency: 5 },
[QueueName.SMART_ALBUM]: { concurrency: 1 },
[QueueName.STORAGE_TEMPLATE_MIGRATION]: { concurrency: 5 }, [QueueName.STORAGE_TEMPLATE_MIGRATION]: { concurrency: 5 },
[QueueName.THUMBNAIL_GENERATION]: { concurrency: 5 }, [QueueName.THUMBNAIL_GENERATION]: { concurrency: 5 },
[QueueName.VIDEO_CONVERSION]: { concurrency: 1 }, [QueueName.VIDEO_CONVERSION]: { concurrency: 1 },

View file

@ -21,6 +21,7 @@ import {
OAuthController, OAuthController,
PartnerController, PartnerController,
PersonController, PersonController,
RuleController,
SearchController, SearchController,
ServerInfoController, ServerInfoController,
SharedLinkController, SharedLinkController,
@ -46,6 +47,7 @@ import {
JobController, JobController,
OAuthController, OAuthController,
PartnerController, PartnerController,
RuleController,
SearchController, SearchController,
ServerInfoController, ServerInfoController,
SharedLinkController, SharedLinkController,

View file

@ -7,6 +7,7 @@ export * from './job.controller';
export * from './oauth.controller'; export * from './oauth.controller';
export * from './partner.controller'; export * from './partner.controller';
export * from './person.controller'; export * from './person.controller';
export * from './rule.controller';
export * from './search.controller'; export * from './search.controller';
export * from './server-info.controller'; export * from './server-info.controller';
export * from './shared-link.controller'; export * from './shared-link.controller';

View file

@ -0,0 +1,44 @@
import {
AuthUserDto,
CreateRuleDto as CreateDto,
RuleResponseDto,
RuleService,
UpdateRuleDto as UpdateDto,
} from '@app/domain';
import { Body, Controller, Delete, Get, Param, Post, Put } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { Authenticated, AuthUser } from '../app.guard';
import { UseValidation } from '../app.utils';
import { UUIDParamDto } from './dto/uuid-param.dto';
@ApiTags('Rule')
@Controller('rule')
@Authenticated()
@UseValidation()
export class RuleController {
constructor(private service: RuleService) {}
@Post()
createRule(@AuthUser() authUser: AuthUserDto, @Body() dto: CreateDto): Promise<RuleResponseDto> {
return this.service.create(authUser, dto);
}
@Get(':id')
getRule(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto): Promise<RuleResponseDto> {
return this.service.get(authUser, id);
}
@Put(':id')
updateRule(
@AuthUser() authUser: AuthUserDto,
@Param() { id }: UUIDParamDto,
@Body() dto: UpdateDto,
): Promise<RuleResponseDto> {
return this.service.update(authUser, id, dto);
}
@Delete(':id')
removeRule(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto) {
return this.service.remove(authUser, id);
}
}

View file

@ -10,6 +10,7 @@ import {
UpdateDateColumn, UpdateDateColumn,
} from 'typeorm'; } from 'typeorm';
import { AssetEntity } from './asset.entity'; import { AssetEntity } from './asset.entity';
import { RuleEntity } from './rule.entity';
import { SharedLinkEntity } from './shared-link.entity'; import { SharedLinkEntity } from './shared-link.entity';
import { UserEntity } from './user.entity'; import { UserEntity } from './user.entity';
@ -52,4 +53,7 @@ export class AlbumEntity {
@OneToMany(() => SharedLinkEntity, (link) => link.album) @OneToMany(() => SharedLinkEntity, (link) => link.album)
sharedLinks!: SharedLinkEntity[]; sharedLinks!: SharedLinkEntity[];
@OneToMany(() => RuleEntity, (rule) => rule.album)
rules!: RuleEntity[];
} }

View file

@ -4,6 +4,7 @@ import { AssetFaceEntity } from './asset-face.entity';
import { AssetEntity } from './asset.entity'; import { AssetEntity } from './asset.entity';
import { PartnerEntity } from './partner.entity'; import { PartnerEntity } from './partner.entity';
import { PersonEntity } from './person.entity'; import { PersonEntity } from './person.entity';
import { RuleEntity } from './rule.entity';
import { SharedLinkEntity } from './shared-link.entity'; import { SharedLinkEntity } from './shared-link.entity';
import { SmartInfoEntity } from './smart-info.entity'; import { SmartInfoEntity } from './smart-info.entity';
import { SystemConfigEntity } from './system-config.entity'; import { SystemConfigEntity } from './system-config.entity';
@ -18,6 +19,7 @@ export * from './asset.entity';
export * from './exif.entity'; export * from './exif.entity';
export * from './partner.entity'; export * from './partner.entity';
export * from './person.entity'; export * from './person.entity';
export * from './rule.entity';
export * from './shared-link.entity'; export * from './shared-link.entity';
export * from './smart-info.entity'; export * from './smart-info.entity';
export * from './system-config.entity'; export * from './system-config.entity';
@ -33,6 +35,7 @@ export const databaseEntities = [
PartnerEntity, PartnerEntity,
PersonEntity, PersonEntity,
SharedLinkEntity, SharedLinkEntity,
RuleEntity,
SmartInfoEntity, SmartInfoEntity,
SystemConfigEntity, SystemConfigEntity,
TagEntity, TagEntity,

View file

@ -0,0 +1,65 @@
import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from 'typeorm';
import { JSON_TRANSFORMER } from '../infra.util';
import { AlbumEntity } from './album.entity';
import { UserEntity } from './user.entity';
@Entity('rules')
export class RuleEntity<T = RuleValue> {
@PrimaryGeneratedColumn('uuid')
id!: string;
@Column()
key!: RuleKey;
@Column({ type: 'varchar', transformer: JSON_TRANSFORMER })
value!: T;
@Column()
ownerId!: string;
@ManyToOne(() => UserEntity)
owner!: UserEntity;
@Column()
albumId!: string;
@ManyToOne(() => AlbumEntity, (album) => album.rules, { onDelete: 'CASCADE', onUpdate: 'CASCADE' })
album!: AlbumEntity;
}
export enum RuleKey {
PERSON = 'person',
TAKEN_AFTER = 'taken-after',
CITY = 'city',
STATE = 'state',
COUNTRY = 'country',
CAMERA_MAKE = 'camera-make',
CAMERA_MODEL = 'camera-model',
LOCATION = 'location',
}
export type RuleValue = string | Date | GeoRuleValue;
export enum RuleValueType {
UUID = 'uuid',
STRING = 'string',
DATE = 'date',
GEO = 'geo',
}
export interface GeoRuleValue {
lat: number;
lng: number;
radius: number;
}
export const RULE_TO_TYPE: Record<RuleKey, RuleValueType> = {
[RuleKey.PERSON]: RuleValueType.UUID,
[RuleKey.TAKEN_AFTER]: RuleValueType.DATE,
[RuleKey.CITY]: RuleValueType.STRING,
[RuleKey.STATE]: RuleValueType.STRING,
[RuleKey.COUNTRY]: RuleValueType.STRING,
[RuleKey.CAMERA_MAKE]: RuleValueType.STRING,
[RuleKey.CAMERA_MODEL]: RuleValueType.STRING,
[RuleKey.LOCATION]: RuleValueType.GEO,
};

View file

@ -1,13 +1,13 @@
import { QueueName } from '@app/domain/job/job.constants'; import { QueueName } from '@app/domain/job/job.constants';
import { Column, Entity, PrimaryColumn } from 'typeorm'; import { Column, Entity, PrimaryColumn } from 'typeorm';
import { JSON_TRANSFORMER } from '../infra.util';
@Entity('system_config') @Entity('system_config')
export class SystemConfigEntity<T = SystemConfigValue> { export class SystemConfigEntity<T = SystemConfigValue> {
@PrimaryColumn() @PrimaryColumn()
key!: SystemConfigKey; key!: SystemConfigKey;
@Column({ type: 'varchar', nullable: true, transformer: { to: JSON.stringify, from: JSON.parse } }) @Column({ type: 'varchar', nullable: true, transformer: JSON_TRANSFORMER }) value!: T;
value!: T;
} }
export type SystemConfigValue = string | number | boolean; export type SystemConfigValue = string | number | boolean;

View file

@ -13,6 +13,7 @@ import {
immichAppConfig, immichAppConfig,
IPartnerRepository, IPartnerRepository,
IPersonRepository, IPersonRepository,
IRuleRepository,
ISearchRepository, ISearchRepository,
ISharedLinkRepository, ISharedLinkRepository,
ISmartInfoRepository, ISmartInfoRepository,
@ -45,6 +46,7 @@ import {
MediaRepository, MediaRepository,
PartnerRepository, PartnerRepository,
PersonRepository, PersonRepository,
RuleRepository,
SharedLinkRepository, SharedLinkRepository,
SmartInfoRepository, SmartInfoRepository,
SystemConfigRepository, SystemConfigRepository,
@ -67,6 +69,7 @@ const providers: Provider[] = [
{ provide: IMachineLearningRepository, useClass: MachineLearningRepository }, { provide: IMachineLearningRepository, useClass: MachineLearningRepository },
{ provide: IPartnerRepository, useClass: PartnerRepository }, { provide: IPartnerRepository, useClass: PartnerRepository },
{ provide: IPersonRepository, useClass: PersonRepository }, { provide: IPersonRepository, useClass: PersonRepository },
{ provide: IRuleRepository, useClass: RuleRepository },
{ provide: ISearchRepository, useClass: TypesenseRepository }, { provide: ISearchRepository, useClass: TypesenseRepository },
{ provide: ISharedLinkRepository, useClass: SharedLinkRepository }, { provide: ISharedLinkRepository, useClass: SharedLinkRepository },
{ provide: ISmartInfoRepository, useClass: SmartInfoRepository }, { provide: ISmartInfoRepository, useClass: SmartInfoRepository },

View file

@ -0,0 +1 @@
export const JSON_TRANSFORMER = { to: JSON.stringify, from: JSON.parse };

View file

@ -1,13 +1,14 @@
import { IAccessRepository } from '@app/domain'; import { IAccessRepository } from '@app/domain';
import { InjectRepository } from '@nestjs/typeorm'; import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm'; import { Repository } from 'typeorm';
import { AlbumEntity, AssetEntity, PartnerEntity, SharedLinkEntity } from '../entities'; import { AlbumEntity, AssetEntity, PartnerEntity, RuleEntity, SharedLinkEntity } from '../entities';
export class AccessRepository implements IAccessRepository { export class AccessRepository implements IAccessRepository {
constructor( constructor(
@InjectRepository(AssetEntity) private assetRepository: Repository<AssetEntity>, @InjectRepository(AssetEntity) private assetRepository: Repository<AssetEntity>,
@InjectRepository(AlbumEntity) private albumRepository: Repository<AlbumEntity>, @InjectRepository(AlbumEntity) private albumRepository: Repository<AlbumEntity>,
@InjectRepository(PartnerEntity) private partnerRepository: Repository<PartnerEntity>, @InjectRepository(PartnerEntity) private partnerRepository: Repository<PartnerEntity>,
@InjectRepository(RuleEntity) private ruleRepository: Repository<RuleEntity>,
@InjectRepository(SharedLinkEntity) private sharedLinkRepository: Repository<SharedLinkEntity>, @InjectRepository(SharedLinkEntity) private sharedLinkRepository: Repository<SharedLinkEntity>,
) {} ) {}
@ -156,4 +157,15 @@ export class AccessRepository implements IAccessRepository {
}); });
}, },
}; };
rule = {
hasOwnerAccess: (userId: string, ruleId: string): Promise<boolean> => {
return this.ruleRepository.exist({
where: {
id: ruleId,
ownerId: userId,
},
});
},
};
} }

View file

@ -19,6 +19,7 @@ export class AlbumRepository implements IAlbumRepository {
sharedUsers: true, sharedUsers: true,
assets: false, assets: false,
sharedLinks: true, sharedLinks: true,
rules: true,
}; };
const order: FindOptionsOrder<AlbumEntity> = {}; const order: FindOptionsOrder<AlbumEntity> = {};
@ -44,6 +45,7 @@ export class AlbumRepository implements IAlbumRepository {
relations: { relations: {
owner: true, owner: true,
sharedUsers: true, sharedUsers: true,
rules: true,
}, },
}); });
} }
@ -51,7 +53,7 @@ export class AlbumRepository implements IAlbumRepository {
getByAssetId(ownerId: string, assetId: string): Promise<AlbumEntity[]> { getByAssetId(ownerId: string, assetId: string): Promise<AlbumEntity[]> {
return this.repository.find({ return this.repository.find({
where: { ownerId, assets: { id: assetId } }, where: { ownerId, assets: { id: assetId } },
relations: { owner: true, sharedUsers: true }, relations: { owner: true, sharedUsers: true, rules: true },
order: { createdAt: 'DESC' }, order: { createdAt: 'DESC' },
}); });
} }
@ -107,7 +109,7 @@ export class AlbumRepository implements IAlbumRepository {
getOwned(ownerId: string): Promise<AlbumEntity[]> { getOwned(ownerId: string): Promise<AlbumEntity[]> {
return this.repository.find({ return this.repository.find({
relations: { sharedUsers: true, sharedLinks: true, owner: true }, relations: { sharedUsers: true, sharedLinks: true, owner: true, rules: true },
where: { ownerId }, where: { ownerId },
order: { createdAt: 'DESC' }, order: { createdAt: 'DESC' },
}); });
@ -118,7 +120,7 @@ export class AlbumRepository implements IAlbumRepository {
*/ */
getShared(ownerId: string): Promise<AlbumEntity[]> { getShared(ownerId: string): Promise<AlbumEntity[]> {
return this.repository.find({ return this.repository.find({
relations: { sharedUsers: true, sharedLinks: true, owner: true }, relations: { sharedUsers: true, sharedLinks: true, owner: true, rules: true },
where: [ where: [
{ sharedUsers: { id: ownerId } }, { sharedUsers: { id: ownerId } },
{ sharedLinks: { userId: ownerId } }, { sharedLinks: { userId: ownerId } },
@ -133,7 +135,7 @@ export class AlbumRepository implements IAlbumRepository {
*/ */
getNotShared(ownerId: string): Promise<AlbumEntity[]> { getNotShared(ownerId: string): Promise<AlbumEntity[]> {
return this.repository.find({ return this.repository.find({
relations: { sharedUsers: true, sharedLinks: true, owner: true }, relations: { sharedUsers: true, sharedLinks: true, owner: true, rules: true },
where: { ownerId, sharedUsers: { id: IsNull() }, sharedLinks: { id: IsNull() } }, where: { ownerId, sharedUsers: { id: IsNull() }, sharedLinks: { id: IsNull() } },
order: { createdAt: 'DESC' }, order: { createdAt: 'DESC' },
}); });
@ -196,6 +198,7 @@ export class AlbumRepository implements IAlbumRepository {
sharedUsers: true, sharedUsers: true,
sharedLinks: true, sharedLinks: true,
assets: true, assets: true,
rules: true,
}, },
}); });
} }

View file

@ -12,6 +12,7 @@ export * from './machine-learning.repository';
export * from './media.repository'; export * from './media.repository';
export * from './partner.repository'; export * from './partner.repository';
export * from './person.repository'; export * from './person.repository';
export * from './rule.repository';
export * from './shared-link.repository'; export * from './shared-link.repository';
export * from './smart-info.repository'; export * from './smart-info.repository';
export * from './system-config.repository'; export * from './system-config.repository';

View file

@ -0,0 +1,29 @@
import { IRuleRepository } from '@app/domain';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { RuleEntity } from '../entities';
export class RuleRepository implements IRuleRepository {
constructor(@InjectRepository(RuleEntity) private repository: Repository<RuleEntity>) {}
get(id: string): Promise<RuleEntity | null> {
return this.repository.findOne({ where: { id } });
}
create(rule: Partial<RuleEntity>): Promise<RuleEntity> {
return this.save(rule);
}
update(rule: Partial<RuleEntity>): Promise<RuleEntity> {
return this.save(rule);
}
delete(rule: RuleEntity): Promise<RuleEntity> {
return this.repository.remove(rule);
}
private async save(rule: Partial<RuleEntity>): Promise<RuleEntity> {
await this.repository.save(rule);
return this.repository.findOneOrFail({ where: { id: rule.id } });
}
}

View file

@ -74,6 +74,9 @@ export class AppService {
[JobName.QUEUE_SIDECAR]: (data) => this.metadataService.handleQueueSidecar(data), [JobName.QUEUE_SIDECAR]: (data) => this.metadataService.handleQueueSidecar(data),
[JobName.SIDECAR_DISCOVERY]: (data) => this.metadataService.handleSidecarDiscovery(data), [JobName.SIDECAR_DISCOVERY]: (data) => this.metadataService.handleSidecarDiscovery(data),
[JobName.SIDECAR_SYNC]: () => this.metadataService.handleSidecarSync(), [JobName.SIDECAR_SYNC]: () => this.metadataService.handleSidecarSync(),
[JobName.SMART_ALBUM_INDEX]: (data) => {
throw new Error('Not implemented SMART_ALBUM_INDEX');
},
}); });
process.on('uncaughtException', (error: Error | any) => { process.on('uncaughtException', (error: Error | any) => {

View file

@ -0,0 +1,222 @@
import { AlbumResponseDto, LoginResponseDto } from '@app/domain';
import { AppModule, RuleController } from '@app/immich';
import { RuleKey } from '@app/infra/entities';
import { INestApplication } from '@nestjs/common';
import { Test, TestingModule } from '@nestjs/testing';
import request from 'supertest';
import { errorStub, uuidStub } from '../fixtures';
import { api, db } from '../test-utils';
describe(`${RuleController.name} (e2e)`, () => {
let app: INestApplication;
let server: any;
let accessToken: string;
let album: AlbumResponseDto;
let loginResponse: LoginResponseDto;
beforeAll(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = await moduleFixture.createNestApplication().init();
server = app.getHttpServer();
});
beforeEach(async () => {
await db.reset();
await api.adminSignUp(server);
loginResponse = await api.adminLogin(server);
accessToken = loginResponse.accessToken;
album = await api.albumApi.create(server, accessToken, { albumName: 'New album' });
});
afterAll(async () => {
await db.disconnect();
await app.close();
});
describe('POST /rule', () => {
const tests = {
invalid: [
{
should: 'reject an invalid uuid',
dto: () => ({ albumId: uuidStub.invalid, key: RuleKey.CITY, value: 'Chandler' }),
},
{
should: 'reject an album that does not exist',
dto: () => ({ albumId: uuidStub.notFound, key: RuleKey.CITY, value: 'Chandler' }),
},
{
should: 'reject invalid keys',
dto: (albumId: string) => ({ albumId, key: 'invalid', value: 'Chandler' }),
},
{
should: 'validate string field values',
dto: (albumId: string) => ({ albumId, key: RuleKey.CAMERA_MAKE, value: true }),
},
{
should: 'validate date field values',
dto: (albumId: string) => ({ albumId, key: RuleKey.TAKEN_AFTER, value: 'Chandler' }),
},
{
should: 'reject an empty geo field value',
dto: (albumId: string) => ({ albumId, key: RuleKey.LOCATION, value: {} }),
},
{
should: 'validate geo.lat field values',
dto: (albumId: string) => ({ albumId, key: RuleKey.LOCATION, value: { lat: 200, lng: 50, radius: 5 } }),
},
{
should: 'validate geo.lng field values',
dto: (albumId: string) => ({ albumId, key: RuleKey.LOCATION, value: { lat: 50, lng: 200, radius: 5 } }),
},
{
should: 'validate geo.radius field values',
dto: (albumId: string) => ({ albumId, key: RuleKey.LOCATION, value: { lat: 50, lng: 50, radius: false } }),
},
],
};
it('should require authentication', async () => {
const { status, body } = await request(server).post('/rule').send({ albumId: uuidStub.notFound });
expect(status).toBe(401);
expect(body).toEqual(errorStub.unauthorized);
});
for (const { should, dto } of tests.invalid) {
it(should, async () => {
const { status, body } = await request(server)
.post('/rule')
.set('Authorization', `Bearer ${accessToken}`)
.send(dto(album.id));
expect(status).toBe(400);
expect(body).toEqual(errorStub.badRequest);
});
}
it('should create a rule for camera make', async () => {
const { status, body } = await request(server)
.post('/rule')
.set('Authorization', `Bearer ${accessToken}`)
.send({ albumId: album.id, key: RuleKey.CAMERA_MAKE, value: 'Cannon' });
expect(status).toBe(201);
expect(body).toEqual({
id: expect.any(String),
ownerId: loginResponse.userId,
key: RuleKey.CAMERA_MAKE,
value: 'Cannon',
});
});
it('should create a rule for camera model', async () => {
const { status, body } = await request(server)
.post('/rule')
.set('Authorization', `Bearer ${accessToken}`)
.send({ albumId: album.id, key: RuleKey.CAMERA_MODEL, value: 'E0S 5D Mark III' });
expect(status).toBe(201);
expect(body).toEqual({
id: expect.any(String),
ownerId: loginResponse.userId,
key: RuleKey.CAMERA_MODEL,
value: 'E0S 5D Mark III',
});
});
it('should create a rule for city', async () => {
const { status, body } = await request(server)
.post('/rule')
.set('Authorization', `Bearer ${accessToken}`)
.send({ albumId: album.id, key: RuleKey.CITY, value: 'Chandler' });
expect(status).toBe(201);
expect(body).toEqual({
id: expect.any(String),
ownerId: loginResponse.userId,
key: RuleKey.CITY,
value: 'Chandler',
});
});
it('should create a rule for state', async () => {
const { status, body } = await request(server)
.post('/rule')
.set('Authorization', `Bearer ${accessToken}`)
.send({ albumId: album.id, key: RuleKey.STATE, value: 'Arizona' });
expect(status).toBe(201);
expect(body).toEqual({
id: expect.any(String),
ownerId: loginResponse.userId,
key: RuleKey.STATE,
value: 'Arizona',
});
});
it('should create a rule for country', async () => {
const { status, body } = await request(server)
.post('/rule')
.set('Authorization', `Bearer ${accessToken}`)
.send({ albumId: album.id, key: RuleKey.COUNTRY, value: 'United States' });
expect(status).toBe(201);
expect(body).toEqual({
id: expect.any(String),
ownerId: loginResponse.userId,
key: RuleKey.COUNTRY,
value: 'United States',
});
});
it('should create a rule with a person', async () => {
const { status, body } = await request(server)
.post('/rule')
.set('Authorization', `Bearer ${accessToken}`)
.send({ albumId: album.id, key: RuleKey.PERSON, value: '4b5d0632-1bc1-48d1-8c89-174fd26bf29d' });
expect(body).toEqual({
id: expect.any(String),
ownerId: loginResponse.userId,
key: RuleKey.PERSON,
value: '4b5d0632-1bc1-48d1-8c89-174fd26bf29d',
});
expect(status).toBe(201);
});
it('should create a rule with taken after', async () => {
const { status, body } = await request(server)
.post('/rule')
.set('Authorization', `Bearer ${accessToken}`)
.send({ albumId: album.id, key: RuleKey.TAKEN_AFTER, value: '2023-08-14T20:12:34.908Z' });
expect(status).toBe(201);
expect(body).toEqual({
id: expect.any(String),
ownerId: loginResponse.userId,
key: RuleKey.TAKEN_AFTER,
value: '2023-08-14T20:12:34.908Z',
});
});
});
describe('GET /rule/:id', () => {
it('should require authentication', async () => {
const { status, body } = await request(server).get(`/rule/${uuidStub.notFound}`);
expect(status).toBe(401);
expect(body).toEqual(errorStub.unauthorized);
});
});
describe('PUT /rule/:id', () => {
it('should require authentication', async () => {
const { status, body } = await request(server)
.put(`/rule/${uuidStub.notFound}`)
.send({ albumId: uuidStub.notFound });
expect(status).toBe(401);
expect(body).toEqual(errorStub.unauthorized);
});
});
describe('DELETE /rule/:id', () => {
it('should require authentication', async () => {
const { status, body } = await request(server).delete(`/rule/${uuidStub.notFound}`);
expect(status).toBe(401);
expect(body).toEqual(errorStub.unauthorized);
});
});
});

View file

@ -17,6 +17,7 @@ export const albumStub = {
updatedAt: new Date(), updatedAt: new Date(),
sharedLinks: [], sharedLinks: [],
sharedUsers: [], sharedUsers: [],
rules: [],
}), }),
sharedWithUser: Object.freeze<AlbumEntity>({ sharedWithUser: Object.freeze<AlbumEntity>({
id: 'album-2', id: 'album-2',
@ -31,6 +32,7 @@ export const albumStub = {
updatedAt: new Date(), updatedAt: new Date(),
sharedLinks: [], sharedLinks: [],
sharedUsers: [userStub.user1], sharedUsers: [userStub.user1],
rules: [],
}), }),
sharedWithMultiple: Object.freeze<AlbumEntity>({ sharedWithMultiple: Object.freeze<AlbumEntity>({
id: 'album-3', id: 'album-3',
@ -45,6 +47,7 @@ export const albumStub = {
updatedAt: new Date(), updatedAt: new Date(),
sharedLinks: [], sharedLinks: [],
sharedUsers: [userStub.user1, userStub.user2], sharedUsers: [userStub.user1, userStub.user2],
rules: [],
}), }),
sharedWithAdmin: Object.freeze<AlbumEntity>({ sharedWithAdmin: Object.freeze<AlbumEntity>({
id: 'album-3', id: 'album-3',
@ -59,6 +62,7 @@ export const albumStub = {
updatedAt: new Date(), updatedAt: new Date(),
sharedLinks: [], sharedLinks: [],
sharedUsers: [userStub.admin], sharedUsers: [userStub.admin],
rules: [],
}), }),
oneAsset: Object.freeze<AlbumEntity>({ oneAsset: Object.freeze<AlbumEntity>({
id: 'album-4', id: 'album-4',
@ -73,6 +77,7 @@ export const albumStub = {
updatedAt: new Date(), updatedAt: new Date(),
sharedLinks: [], sharedLinks: [],
sharedUsers: [], sharedUsers: [],
rules: [],
}), }),
twoAssets: Object.freeze<AlbumEntity>({ twoAssets: Object.freeze<AlbumEntity>({
id: 'album-4a', id: 'album-4a',
@ -87,6 +92,7 @@ export const albumStub = {
updatedAt: new Date(), updatedAt: new Date(),
sharedLinks: [], sharedLinks: [],
sharedUsers: [], sharedUsers: [],
rules: [],
}), }),
emptyWithInvalidThumbnail: Object.freeze<AlbumEntity>({ emptyWithInvalidThumbnail: Object.freeze<AlbumEntity>({
id: 'album-5', id: 'album-5',
@ -101,6 +107,7 @@ export const albumStub = {
updatedAt: new Date(), updatedAt: new Date(),
sharedLinks: [], sharedLinks: [],
sharedUsers: [], sharedUsers: [],
rules: [],
}), }),
emptyWithValidThumbnail: Object.freeze<AlbumEntity>({ emptyWithValidThumbnail: Object.freeze<AlbumEntity>({
id: 'album-5', id: 'album-5',
@ -115,6 +122,7 @@ export const albumStub = {
updatedAt: new Date(), updatedAt: new Date(),
sharedLinks: [], sharedLinks: [],
sharedUsers: [], sharedUsers: [],
rules: [],
}), }),
oneAssetInvalidThumbnail: Object.freeze<AlbumEntity>({ oneAssetInvalidThumbnail: Object.freeze<AlbumEntity>({
id: 'album-6', id: 'album-6',
@ -129,6 +137,7 @@ export const albumStub = {
updatedAt: new Date(), updatedAt: new Date(),
sharedLinks: [], sharedLinks: [],
sharedUsers: [], sharedUsers: [],
rules: [],
}), }),
oneAssetValidThumbnail: Object.freeze<AlbumEntity>({ oneAssetValidThumbnail: Object.freeze<AlbumEntity>({
id: 'album-6', id: 'album-6',
@ -143,5 +152,6 @@ export const albumStub = {
updatedAt: new Date(), updatedAt: new Date(),
sharedLinks: [], sharedLinks: [],
sharedUsers: [], sharedUsers: [],
rules: [],
}), }),
}; };

View file

@ -17,7 +17,7 @@ export const errorStub = {
badRequest: { badRequest: {
error: 'Bad Request', error: 'Bad Request',
statusCode: 400, statusCode: 400,
message: expect.any(Array), message: expect.anything(),
}, },
incorrectLogin: { incorrectLogin: {
error: 'Unauthorized', error: 'Unauthorized',

View file

@ -9,6 +9,7 @@ export * from './file.stub';
export * from './media.stub'; export * from './media.stub';
export * from './partner.stub'; export * from './partner.stub';
export * from './person.stub'; export * from './person.stub';
export * from './rule.stub';
export * from './search.stub'; export * from './search.stub';
export * from './shared-link.stub'; export * from './shared-link.stub';
export * from './system-config.stub'; export * from './system-config.stub';

15
server/test/fixtures/rule.stub.ts vendored Normal file
View file

@ -0,0 +1,15 @@
import { RuleEntity, RuleKey } from '@app/infra/entities';
import { albumStub } from './album.stub';
import { userStub } from './user.stub';
export const ruleStub = {
rule1: Object.freeze<RuleEntity>({
id: 'rule-1',
key: RuleKey.CITY,
value: 'Chandler',
owner: userStub.admin,
ownerId: userStub.admin.id,
album: albumStub.empty,
albumId: albumStub.empty.id,
}),
};

View file

@ -80,6 +80,7 @@ const albumResponse: AlbumResponseDto = {
hasSharedLink: false, hasSharedLink: false,
assets: [], assets: [],
assetCount: 1, assetCount: 1,
rules: [],
}; };
export const sharedLinkStub = { export const sharedLinkStub = {
@ -222,6 +223,7 @@ export const sharedLinkStub = {
sidecarPath: null, sidecarPath: null,
}, },
], ],
rules: [],
}, },
}), }),
}; };

View file

@ -4,6 +4,7 @@ export type IAccessRepositoryMock = {
asset: jest.Mocked<IAccessRepository['asset']>; asset: jest.Mocked<IAccessRepository['asset']>;
album: jest.Mocked<IAccessRepository['album']>; album: jest.Mocked<IAccessRepository['album']>;
library: jest.Mocked<IAccessRepository['library']>; library: jest.Mocked<IAccessRepository['library']>;
rule: jest.Mocked<IAccessRepository['rule']>;
}; };
export const newAccessRepositoryMock = (): IAccessRepositoryMock => { export const newAccessRepositoryMock = (): IAccessRepositoryMock => {
@ -24,5 +25,9 @@ export const newAccessRepositoryMock = (): IAccessRepositoryMock => {
library: { library: {
hasPartnerAccess: jest.fn(), hasPartnerAccess: jest.fn(),
}, },
rule: {
hasOwnerAccess: jest.fn(),
},
}; };
}; };

View file

@ -10,6 +10,7 @@ export * from './machine-learning.repository.mock';
export * from './media.repository.mock'; export * from './media.repository.mock';
export * from './partner.repository.mock'; export * from './partner.repository.mock';
export * from './person.repository.mock'; export * from './person.repository.mock';
export * from './rule.repository.mock';
export * from './search.repository.mock'; export * from './search.repository.mock';
export * from './shared-link.repository.mock'; export * from './shared-link.repository.mock';
export * from './smart-info.repository.mock'; export * from './smart-info.repository.mock';

View file

@ -0,0 +1,10 @@
import { IRuleRepository } from '@app/domain';
export const newRuleRepositoryMock = (): jest.Mocked<IRuleRepository> => {
return {
get: jest.fn(),
create: jest.fn(),
update: jest.fn(),
delete: jest.fn(),
};
};

View file

@ -11,6 +11,7 @@ import {
OAuthApi, OAuthApi,
PartnerApi, PartnerApi,
PersonApi, PersonApi,
RuleApi,
SearchApi, SearchApi,
ServerInfoApi, ServerInfoApi,
SharedLinkApi, SharedLinkApi,
@ -30,6 +31,7 @@ export class ImmichApi {
public keyApi: APIKeyApi; public keyApi: APIKeyApi;
public oauthApi: OAuthApi; public oauthApi: OAuthApi;
public partnerApi: PartnerApi; public partnerApi: PartnerApi;
public ruleApi: RuleApi;
public searchApi: SearchApi; public searchApi: SearchApi;
public serverInfoApi: ServerInfoApi; public serverInfoApi: ServerInfoApi;
public sharedLinkApi: SharedLinkApi; public sharedLinkApi: SharedLinkApi;
@ -49,6 +51,7 @@ export class ImmichApi {
this.keyApi = new APIKeyApi(this.config); this.keyApi = new APIKeyApi(this.config);
this.oauthApi = new OAuthApi(this.config); this.oauthApi = new OAuthApi(this.config);
this.partnerApi = new PartnerApi(this.config); this.partnerApi = new PartnerApi(this.config);
this.ruleApi = new RuleApi(this.config);
this.searchApi = new SearchApi(this.config); this.searchApi = new SearchApi(this.config);
this.serverInfoApi = new ServerInfoApi(this.config); this.serverInfoApi = new ServerInfoApi(this.config);
this.sharedLinkApi = new SharedLinkApi(this.config); this.sharedLinkApi = new SharedLinkApi(this.config);
@ -116,6 +119,7 @@ export class ImmichApi {
[JobName.StorageTemplateMigration]: 'Storage Template Migration', [JobName.StorageTemplateMigration]: 'Storage Template Migration',
[JobName.BackgroundTask]: 'Background Tasks', [JobName.BackgroundTask]: 'Background Tasks',
[JobName.Search]: 'Search', [JobName.Search]: 'Search',
[JobName.SmartAlbum]: 'Smart Albums',
}; };
return names[jobName]; return names[jobName];

View file

@ -252,6 +252,12 @@ export interface AlbumResponseDto {
* @memberof AlbumResponseDto * @memberof AlbumResponseDto
*/ */
'ownerId': string; 'ownerId': string;
/**
*
* @type {Array<RuleResponseDto>}
* @memberof AlbumResponseDto
*/
'rules': Array<RuleResponseDto>;
/** /**
* *
* @type {boolean} * @type {boolean}
@ -325,6 +331,12 @@ export interface AllJobStatusResponseDto {
* @memberof AllJobStatusResponseDto * @memberof AllJobStatusResponseDto
*/ */
'sidecar': JobStatusDto; 'sidecar': JobStatusDto;
/**
*
* @type {JobStatusDto}
* @memberof AllJobStatusResponseDto
*/
'smartAlbum': JobStatusDto;
/** /**
* *
* @type {JobStatusDto} * @type {JobStatusDto}
@ -946,6 +958,33 @@ export interface CreateProfileImageResponseDto {
*/ */
'userId': string; 'userId': string;
} }
/**
*
* @export
* @interface CreateRuleDto
*/
export interface CreateRuleDto {
/**
*
* @type {string}
* @memberof CreateRuleDto
*/
'albumId': string;
/**
*
* @type {RuleKey}
* @memberof CreateRuleDto
*/
'key': RuleKey;
/**
*
* @type {string}
* @memberof CreateRuleDto
*/
'value': string;
}
/** /**
* *
* @export * @export
@ -1509,7 +1548,8 @@ export const JobName = {
BackgroundTask: 'backgroundTask', BackgroundTask: 'backgroundTask',
StorageTemplateMigration: 'storageTemplateMigration', StorageTemplateMigration: 'storageTemplateMigration',
Search: 'search', Search: 'search',
Sidecar: 'sidecar' Sidecar: 'sidecar',
SmartAlbum: 'smartAlbum'
} as const; } as const;
export type JobName = typeof JobName[keyof typeof JobName]; export type JobName = typeof JobName[keyof typeof JobName];
@ -1904,6 +1944,59 @@ export interface QueueStatusDto {
*/ */
'isPaused': boolean; 'isPaused': boolean;
} }
/**
*
* @export
* @enum {string}
*/
export const RuleKey = {
Person: 'person',
TakenAfter: 'taken-after',
City: 'city',
State: 'state',
Country: 'country',
CameraMake: 'camera-make',
CameraModel: 'camera-model',
Location: 'location'
} as const;
export type RuleKey = typeof RuleKey[keyof typeof RuleKey];
/**
*
* @export
* @interface RuleResponseDto
*/
export interface RuleResponseDto {
/**
*
* @type {string}
* @memberof RuleResponseDto
*/
'id': string;
/**
*
* @type {RuleKey}
* @memberof RuleResponseDto
*/
'key': RuleKey;
/**
*
* @type {string}
* @memberof RuleResponseDto
*/
'ownerId': string;
/**
*
* @type {string}
* @memberof RuleResponseDto
*/
'value': string;
}
/** /**
* *
* @export * @export
@ -2635,6 +2728,12 @@ export interface SystemConfigJobDto {
* @memberof SystemConfigJobDto * @memberof SystemConfigJobDto
*/ */
'sidecar': JobSettingsDto; 'sidecar': JobSettingsDto;
/**
*
* @type {JobSettingsDto}
* @memberof SystemConfigJobDto
*/
'smartAlbum': JobSettingsDto;
/** /**
* *
* @type {JobSettingsDto} * @type {JobSettingsDto}
@ -3020,6 +3119,27 @@ export interface UpdateAssetDto {
*/ */
'tagIds'?: Array<string>; 'tagIds'?: Array<string>;
} }
/**
*
* @export
* @interface UpdateRuleDto
*/
export interface UpdateRuleDto {
/**
*
* @type {RuleKey}
* @memberof UpdateRuleDto
*/
'key': RuleKey;
/**
*
* @type {string}
* @memberof UpdateRuleDto
*/
'value': string;
}
/** /**
* *
* @export * @export
@ -9700,6 +9820,404 @@ export class PersonApi extends BaseAPI {
} }
/**
* RuleApi - axios parameter creator
* @export
*/
export const RuleApiAxiosParamCreator = function (configuration?: Configuration) {
return {
/**
*
* @param {CreateRuleDto} createRuleDto
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
createRule: async (createRuleDto: CreateRuleDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'createRuleDto' is not null or undefined
assertParamExists('createRule', 'createRuleDto', createRuleDto)
const localVarPath = `/rule`;
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions;
if (configuration) {
baseOptions = configuration.baseOptions;
}
const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
// authentication cookie required
// authentication api_key required
await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration)
// authentication bearer required
// http bearer authentication required
await setBearerAuthToObject(localVarHeaderParameter, configuration)
localVarHeaderParameter['Content-Type'] = 'application/json';
setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
localVarRequestOptions.data = serializeDataIfNeeded(createRuleDto, localVarRequestOptions, configuration)
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
/**
*
* @param {string} id
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
getRule: async (id: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'id' is not null or undefined
assertParamExists('getRule', 'id', id)
const localVarPath = `/rule/{id}`
.replace(`{${"id"}}`, encodeURIComponent(String(id)));
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions;
if (configuration) {
baseOptions = configuration.baseOptions;
}
const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
// authentication cookie required
// authentication api_key required
await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration)
// authentication bearer required
// http bearer authentication required
await setBearerAuthToObject(localVarHeaderParameter, configuration)
setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
/**
*
* @param {string} id
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
removeRule: async (id: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'id' is not null or undefined
assertParamExists('removeRule', 'id', id)
const localVarPath = `/rule/{id}`
.replace(`{${"id"}}`, encodeURIComponent(String(id)));
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions;
if (configuration) {
baseOptions = configuration.baseOptions;
}
const localVarRequestOptions = { method: 'DELETE', ...baseOptions, ...options};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
// authentication cookie required
// authentication api_key required
await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration)
// authentication bearer required
// http bearer authentication required
await setBearerAuthToObject(localVarHeaderParameter, configuration)
setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
/**
*
* @param {string} id
* @param {UpdateRuleDto} updateRuleDto
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
updateRule: async (id: string, updateRuleDto: UpdateRuleDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'id' is not null or undefined
assertParamExists('updateRule', 'id', id)
// verify required parameter 'updateRuleDto' is not null or undefined
assertParamExists('updateRule', 'updateRuleDto', updateRuleDto)
const localVarPath = `/rule/{id}`
.replace(`{${"id"}}`, encodeURIComponent(String(id)));
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions;
if (configuration) {
baseOptions = configuration.baseOptions;
}
const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
// authentication cookie required
// authentication api_key required
await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration)
// authentication bearer required
// http bearer authentication required
await setBearerAuthToObject(localVarHeaderParameter, configuration)
localVarHeaderParameter['Content-Type'] = 'application/json';
setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
localVarRequestOptions.data = serializeDataIfNeeded(updateRuleDto, localVarRequestOptions, configuration)
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
}
};
/**
* RuleApi - functional programming interface
* @export
*/
export const RuleApiFp = function(configuration?: Configuration) {
const localVarAxiosParamCreator = RuleApiAxiosParamCreator(configuration)
return {
/**
*
* @param {CreateRuleDto} createRuleDto
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async createRule(createRuleDto: CreateRuleDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<RuleResponseDto>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.createRule(createRuleDto, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
*
* @param {string} id
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async getRule(id: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<RuleResponseDto>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.getRule(id, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
*
* @param {string} id
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async removeRule(id: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.removeRule(id, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
*
* @param {string} id
* @param {UpdateRuleDto} updateRuleDto
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async updateRule(id: string, updateRuleDto: UpdateRuleDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<RuleResponseDto>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.updateRule(id, updateRuleDto, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
}
};
/**
* RuleApi - factory interface
* @export
*/
export const RuleApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {
const localVarFp = RuleApiFp(configuration)
return {
/**
*
* @param {RuleApiCreateRuleRequest} requestParameters Request parameters.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
createRule(requestParameters: RuleApiCreateRuleRequest, options?: AxiosRequestConfig): AxiosPromise<RuleResponseDto> {
return localVarFp.createRule(requestParameters.createRuleDto, options).then((request) => request(axios, basePath));
},
/**
*
* @param {RuleApiGetRuleRequest} requestParameters Request parameters.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
getRule(requestParameters: RuleApiGetRuleRequest, options?: AxiosRequestConfig): AxiosPromise<RuleResponseDto> {
return localVarFp.getRule(requestParameters.id, options).then((request) => request(axios, basePath));
},
/**
*
* @param {RuleApiRemoveRuleRequest} requestParameters Request parameters.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
removeRule(requestParameters: RuleApiRemoveRuleRequest, options?: AxiosRequestConfig): AxiosPromise<void> {
return localVarFp.removeRule(requestParameters.id, options).then((request) => request(axios, basePath));
},
/**
*
* @param {RuleApiUpdateRuleRequest} requestParameters Request parameters.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
updateRule(requestParameters: RuleApiUpdateRuleRequest, options?: AxiosRequestConfig): AxiosPromise<RuleResponseDto> {
return localVarFp.updateRule(requestParameters.id, requestParameters.updateRuleDto, options).then((request) => request(axios, basePath));
},
};
};
/**
* Request parameters for createRule operation in RuleApi.
* @export
* @interface RuleApiCreateRuleRequest
*/
export interface RuleApiCreateRuleRequest {
/**
*
* @type {CreateRuleDto}
* @memberof RuleApiCreateRule
*/
readonly createRuleDto: CreateRuleDto
}
/**
* Request parameters for getRule operation in RuleApi.
* @export
* @interface RuleApiGetRuleRequest
*/
export interface RuleApiGetRuleRequest {
/**
*
* @type {string}
* @memberof RuleApiGetRule
*/
readonly id: string
}
/**
* Request parameters for removeRule operation in RuleApi.
* @export
* @interface RuleApiRemoveRuleRequest
*/
export interface RuleApiRemoveRuleRequest {
/**
*
* @type {string}
* @memberof RuleApiRemoveRule
*/
readonly id: string
}
/**
* Request parameters for updateRule operation in RuleApi.
* @export
* @interface RuleApiUpdateRuleRequest
*/
export interface RuleApiUpdateRuleRequest {
/**
*
* @type {string}
* @memberof RuleApiUpdateRule
*/
readonly id: string
/**
*
* @type {UpdateRuleDto}
* @memberof RuleApiUpdateRule
*/
readonly updateRuleDto: UpdateRuleDto
}
/**
* RuleApi - object-oriented interface
* @export
* @class RuleApi
* @extends {BaseAPI}
*/
export class RuleApi extends BaseAPI {
/**
*
* @param {RuleApiCreateRuleRequest} requestParameters Request parameters.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof RuleApi
*/
public createRule(requestParameters: RuleApiCreateRuleRequest, options?: AxiosRequestConfig) {
return RuleApiFp(this.configuration).createRule(requestParameters.createRuleDto, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @param {RuleApiGetRuleRequest} requestParameters Request parameters.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof RuleApi
*/
public getRule(requestParameters: RuleApiGetRuleRequest, options?: AxiosRequestConfig) {
return RuleApiFp(this.configuration).getRule(requestParameters.id, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @param {RuleApiRemoveRuleRequest} requestParameters Request parameters.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof RuleApi
*/
public removeRule(requestParameters: RuleApiRemoveRuleRequest, options?: AxiosRequestConfig) {
return RuleApiFp(this.configuration).removeRule(requestParameters.id, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @param {RuleApiUpdateRuleRequest} requestParameters Request parameters.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof RuleApi
*/
public updateRule(requestParameters: RuleApiUpdateRuleRequest, options?: AxiosRequestConfig) {
return RuleApiFp(this.configuration).updateRule(requestParameters.id, requestParameters.updateRuleDto, options).then((request) => request(this.axios, this.basePath));
}
}
/** /**
* SearchApi - axios parameter creator * SearchApi - axios parameter creator
* @export * @export

View file

@ -57,6 +57,10 @@ input:focus-visible {
} }
@layer utilities { @layer utilities {
.immich-text-primary {
@apply text-immich-primary dark:text-immich-dark-primary;
}
.immich-form-input { .immich-form-input {
@apply rounded-xl bg-slate-200 p-4 text-sm focus:border-immich-primary disabled:cursor-not-allowed disabled:bg-gray-400 disabled:text-gray-200 dark:bg-gray-600 dark:text-immich-dark-fg dark:disabled:bg-gray-800; @apply rounded-xl bg-slate-200 p-4 text-sm focus:border-immich-primary disabled:cursor-not-allowed disabled:bg-gray-400 disabled:text-gray-200 dark:bg-gray-600 dark:text-immich-dark-fg dark:disabled:bg-gray-800;
} }

View file

@ -0,0 +1,61 @@
<script lang="ts">
import { createEventDispatcher, onMount } from 'svelte';
import { PersonResponseDto, api } from '@api';
import ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte';
import ArrowLeft from 'svelte-material-icons/ArrowLeft.svelte';
import Button from '$lib/components/elements/buttons/button.svelte';
import FaceThumbnail from '$lib/components/assets/thumbnail/face-thumbnail.svelte';
export let selectedIds: string[] = [];
let people: PersonResponseDto[] = [];
let newPeople: PersonResponseDto[] = [];
const dispatch = createEventDispatcher<{ close: void; confirm: PersonResponseDto[] }>();
onMount(async () => {
const { data } = await api.personApi.getAllPeople({ withHidden: false });
people = data.people.filter(({ id }) => !selectedIds.includes(id));
});
const handleSelection = (e: CustomEvent<{ person: PersonResponseDto }>) => {
const person = e.detail.person;
if (newPeople.some((p) => p.id === person.id)) {
newPeople = newPeople.filter((p) => p.id !== person.id);
} else {
newPeople = [...newPeople, person];
}
};
</script>
<ControlAppBar showBackButton backIcon={ArrowLeft} on:close-button-click={() => dispatch('close')}>
<svelte:fragment slot="leading">
<p class="font-medium text-immich-fg dark:text-immich-dark-fg">
{#if newPeople.length === 0}
Select faces
{:else if newPeople.length === 1}
1 person selected
{:else}
{newPeople.length} people selected
{/if}
</p>
</svelte:fragment>
<svelte:fragment slot="trailing">
<Button disabled={newPeople.length === 0} size="sm" title="Confirm" on:click={() => dispatch('confirm', newPeople)}
>Confirm</Button
>
</svelte:fragment>
</ControlAppBar>
<div class="mt-24 flex flex-wrap gap-2 px-8">
{#each people as person (person.id)}
<FaceThumbnail
{person}
thumbnailSize={180}
on:select={handleSelection}
on:click={handleSelection}
selected={newPeople.some((p) => p.id === person.id)}
/>
{/each}
</div>

View file

@ -0,0 +1,116 @@
<script lang="ts">
import BaseModal from '$lib/components/shared-components/base-modal.svelte';
import { PersonResponseDto, RuleKey, RuleResponseDto, api } from '@api';
import { createEventDispatcher } from 'svelte';
import Plus from 'svelte-material-icons/Plus.svelte';
import { fly } from 'svelte/transition';
import Button from '../../elements/buttons/button.svelte';
import Portal from '../../shared-components/portal/portal.svelte';
import FaceSelection from './face-selection.svelte';
export let rules: RuleResponseDto[] = [];
let peopleSelection = false;
let locationSelection = false;
$: peopleRules = rules.filter((rule) => rule.key === RuleKey.Person);
const dispatch = createEventDispatcher<{
submit: RuleResponseDto[];
close: void;
}>();
const handleSelectPeople = async (people: PersonResponseDto[]) => {
rules = [...rules, ...people.map((person) => ({ key: RuleKey.Person, value: person.id } as RuleResponseDto))];
peopleSelection = false;
};
const handleRemoveRule = async (rule: RuleResponseDto) => {
rules = rules.filter((_rule) => rule !== _rule);
};
</script>
<BaseModal
ignoreClickOutside
on:close={() => {
dispatch('close');
}}
>
<svelte:fragment slot="title">
<div class="flex place-items-center gap-2">
<p class="font-medium text-immich-fg dark:text-immich-dark-fg">Automatically add photos</p>
</div>
</svelte:fragment>
<section class="mb-5 px-5">
<!-- People Selection -->
<div id="people-selection">
<p class="text-sm">PEOPLE</p>
<div class="mt-4 flex flex-wrap gap-2">
{#each peopleRules as rule}
<button on:click={() => handleRemoveRule(rule)}>
<img src={api.getPeopleThumbnailUrl(rule.value)} alt={rule.value} class="h-20 w-20 rounded-lg" />
</button>
{/each}
<button
class="immich-text-primary border-1 flex h-20 w-20 place-content-center place-items-center rounded-lg border border-gray-300 hover:bg-gray-500/20 dark:border-gray-500"
on:click={() => (peopleSelection = true)}
>
<Plus size="24" />
</button>
</div>
</div>
<!-- Location Selection -->
<div id="location-selection" class="mt-5">
<p class="text-sm">LOCATION</p>
<div class="mt-4">
<button
class="immich-text-primary border-1 flex w-full place-content-center place-items-center rounded-3xl border border-gray-300 py-2 hover:bg-gray-500/20 dark:border-gray-500"
on:click={() => (locationSelection = true)}
>
<Plus size="24" />
</button>
</div>
</div>
<!-- Date Selection -->
<div id="date-selection" class="mt-5">
<p class="text-sm">START DATE</p>
<div class="mt-2">
<div class="text-xs">
<p>Only include photos after the set date.</p>
<p>Include all by default</p>
</div>
<div class="mt-4">
<Button size="sm">Select</Button>
</div>
</div>
</div>
</section>
<!-- Buttons rows -->
<svelte:fragment slot="sticky-bottom">
<div class="flex justify-end gap-2">
<Button size="sm" color="secondary" on:click={() => dispatch('close')}>Cancel</Button>
<Button size="sm" color="primary" on:click={() => dispatch('submit', rules)}>Confirm</Button>
</div>
</svelte:fragment>
</BaseModal>
<Portal target="body">
{#if peopleSelection}
<section
transition:fly={{ y: 500 }}
class="absolute left-0 top-0 z-[10000] h-full min-h-max w-full overflow-y-auto bg-gray-200 dark:bg-immich-dark-bg"
>
<FaceSelection
on:close={() => (peopleSelection = false)}
on:confirm={({ detail: people }) => handleSelectPeople(people)}
selectedIds={peopleRules.map(({ value }) => value)}
/>
</section>
{/if}
</Portal>

View file

@ -0,0 +1,130 @@
<script lang="ts">
import IntersectionObserver from '$lib/components/asset-viewer/intersection-observer.svelte';
import { api, PersonResponseDto } from '@api';
import { createEventDispatcher } from 'svelte';
import CheckCircle from 'svelte-material-icons/CheckCircle.svelte';
import { fade } from 'svelte/transition';
import ImageThumbnail from './image-thumbnail.svelte';
export let person: PersonResponseDto;
export let groupIndex = 0;
export let thumbnailSize: number | undefined = undefined;
export let thumbnailWidth: number | undefined = undefined;
export let thumbnailHeight: number | undefined = undefined;
export let selected = false;
export let selectionCandidate = false;
export let disabled = false;
export let readonly = false;
let mouseOver = false;
const dispatch = createEventDispatcher<{
click: { person: PersonResponseDto };
select: { person: PersonResponseDto };
'mouse-event': { isMouseOver: boolean; selectedGroupIndex: number };
}>();
$: dispatch('mouse-event', { isMouseOver: mouseOver, selectedGroupIndex: groupIndex });
$: [width, height] = ((): [number, number] => {
if (thumbnailSize) {
return [thumbnailSize, thumbnailSize];
}
if (thumbnailWidth && thumbnailHeight) {
return [thumbnailWidth, thumbnailHeight];
}
return [235, 235];
})();
const thumbnailClickedHandler = () => {
if (!disabled) {
dispatch('click', { person });
}
};
const thumbnailKeyDownHandler = (e: KeyboardEvent) => {
if (e.key === 'Enter') {
thumbnailClickedHandler();
}
};
const onIconClickedHandler = (e: MouseEvent) => {
e.stopPropagation();
if (!disabled) {
dispatch('select', { person });
}
};
</script>
<IntersectionObserver once={false} let:intersecting>
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
style:width="{width}px"
style:height="{height}px"
class="group relative overflow-hidden {disabled
? 'bg-gray-300'
: 'bg-immich-primary/20 dark:bg-immich-dark-primary/20'}"
class:cursor-not-allowed={disabled}
class:hover:cursor-pointer={!disabled}
on:mouseenter={() => (mouseOver = true)}
on:mouseleave={() => (mouseOver = false)}
on:click={thumbnailClickedHandler}
on:keydown={thumbnailKeyDownHandler}
>
{#if intersecting}
<div class="absolute z-20 h-full w-full">
<!-- Select asset button -->
{#if !readonly && (mouseOver || selected || selectionCandidate)}
<button
on:click={onIconClickedHandler}
class="absolute p-2 focus:outline-none"
class:cursor-not-allowed={disabled}
role="checkbox"
aria-checked={selected}
{disabled}
>
{#if disabled}
<CheckCircle size="24" class="text-zinc-800" />
{:else if selected}
<div class="rounded-full bg-[#D9DCEF] dark:bg-[#232932]">
<CheckCircle size="24" class="text-immich-primary" />
</div>
{:else}
<CheckCircle size="24" class="text-white/80 hover:text-white" />
{/if}
</button>
{/if}
</div>
<div
class="absolute h-full w-full select-none bg-gray-100 transition-transform dark:bg-immich-dark-gray"
class:scale-[0.85]={selected}
class:rounded-xl={selected}
>
<!-- Gradient overlay on hover -->
<div
class="absolute z-10 h-full w-full bg-gradient-to-b from-black/25 via-[transparent_25%] opacity-0 transition-opacity group-hover:opacity-100"
class:rounded-xl={selected}
/>
<ImageThumbnail
url={api.getPeopleThumbnailUrl(person.id)}
altText={person.name}
widthStyle="{width}px"
heightStyle="{height}px"
curve={selected}
/>
</div>
{#if selectionCandidate}
<div
class="absolute top-0 h-full w-full bg-immich-primary opacity-40"
in:fade={{ duration: 100 }}
out:fade={{ duration: 100 }}
/>
{/if}
{/if}
</div>
</IntersectionObserver>

View file

@ -9,6 +9,7 @@
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
export let zIndex = 9999; export let zIndex = 9999;
export let ignoreClickOutside = false;
onMount(() => { onMount(() => {
if (browser) { if (browser) {
@ -35,10 +36,10 @@
> >
<div <div
use:clickOutside use:clickOutside
on:outclick={() => dispatch('close')} on:outclick={() => !ignoreClickOutside && dispatch('close')}
class="max-h-[600px] min-h-[200px] w-[450px] rounded-lg bg-immich-bg shadow-md dark:bg-immich-dark-gray dark:text-immich-dark-fg" class="max-h-[700px] min-h-[200px] w-[450px] overflow-y-auto rounded-lg bg-immich-bg shadow-md dark:bg-immich-dark-gray dark:text-immich-dark-fg"
> >
<div class="flex place-items-center justify-between px-5 py-3"> <div class="sticky top-0 flex place-items-center justify-between bg-immich-bg px-5 py-3 dark:bg-immich-dark-gray">
<div> <div>
<slot name="title"> <slot name="title">
<p>Modal Title</p> <p>Modal Title</p>
@ -51,5 +52,11 @@
<div class=""> <div class="">
<slot /> <slot />
</div> </div>
{#if $$slots['sticky-bottom']}
<div class="sticky bottom-0 bg-immich-bg px-5 pb-5 pt-3 dark:bg-immich-dark-gray">
<slot name="sticky-bottom" />
</div>
{/if}
</div> </div>
</div> </div>

View file

@ -24,6 +24,7 @@
NotificationType, NotificationType,
notificationController, notificationController,
} from '$lib/components/shared-components/notification/notification'; } from '$lib/components/shared-components/notification/notification';
import RuleSelection from '$lib/components/album-page/rule-selection-form/rule-selection-form.svelte';
import UserAvatar from '$lib/components/shared-components/user-avatar.svelte'; import UserAvatar from '$lib/components/shared-components/user-avatar.svelte';
import { AppRoute, dateFormats } from '$lib/constants'; import { AppRoute, dateFormats } from '$lib/constants';
import { createAssetInteractionStore } from '$lib/stores/asset-interaction.store'; import { createAssetInteractionStore } from '$lib/stores/asset-interaction.store';
@ -33,7 +34,7 @@
import { downloadArchive } from '$lib/utils/asset-utils'; import { downloadArchive } from '$lib/utils/asset-utils';
import { openFileUploadDialog } from '$lib/utils/file-uploader'; import { openFileUploadDialog } from '$lib/utils/file-uploader';
import { handleError } from '$lib/utils/handle-error'; import { handleError } from '$lib/utils/handle-error';
import { TimeBucketSize, UserResponseDto, api } from '@api'; import { RuleResponseDto, TimeBucketSize, UserResponseDto, api } from '@api';
import ArrowLeft from 'svelte-material-icons/ArrowLeft.svelte'; import ArrowLeft from 'svelte-material-icons/ArrowLeft.svelte';
import DeleteOutline from 'svelte-material-icons/DeleteOutline.svelte'; import DeleteOutline from 'svelte-material-icons/DeleteOutline.svelte';
import DotsVertical from 'svelte-material-icons/DotsVertical.svelte'; import DotsVertical from 'svelte-material-icons/DotsVertical.svelte';
@ -41,6 +42,7 @@
import FolderDownloadOutline from 'svelte-material-icons/FolderDownloadOutline.svelte'; import FolderDownloadOutline from 'svelte-material-icons/FolderDownloadOutline.svelte';
import Link from 'svelte-material-icons/Link.svelte'; import Link from 'svelte-material-icons/Link.svelte';
import Plus from 'svelte-material-icons/Plus.svelte'; import Plus from 'svelte-material-icons/Plus.svelte';
import FaceMan from 'svelte-material-icons/FaceMan.svelte';
import ShareVariantOutline from 'svelte-material-icons/ShareVariantOutline.svelte'; import ShareVariantOutline from 'svelte-material-icons/ShareVariantOutline.svelte';
import type { PageData } from './$types'; import type { PageData } from './$types';
@ -57,6 +59,7 @@
SELECT_ASSETS = 'select-assets', SELECT_ASSETS = 'select-assets',
ALBUM_OPTIONS = 'album-options', ALBUM_OPTIONS = 'album-options',
VIEW_USERS = 'view-users', VIEW_USERS = 'view-users',
RULE_SELECTION = 'rule-selection',
VIEW = 'view', VIEW = 'view',
} }
@ -277,6 +280,28 @@
handleError(error, 'Error updating album description'); handleError(error, 'Error updating album description');
} }
}; };
const handleUpdateRules = async (rules: RuleResponseDto[]) => {
let ids = rules.filter((rule) => !!rule.id).map((rule) => rule.id);
for (const rule of album.rules) {
if (!ids.includes(rule.id)) {
await api.ruleApi.removeRule({ id: rule.id });
}
}
for (const { id, key, value } of rules) {
if (!id) {
await api.ruleApi.createRule({ createRuleDto: { albumId: album.id, key, value } });
} else {
await api.ruleApi.updateRule({ id, updateRuleDto: { key, value } });
}
}
await refreshAlbum();
viewMode = ViewMode.VIEW;
};
</script> </script>
<header> <header>
@ -342,7 +367,7 @@
<Button <Button
size="sm" size="sm"
rounded="lg" rounded="lg"
disabled={album.assetCount == 0} disabled={album.assetCount === 0}
on:click={() => (viewMode = ViewMode.SELECT_USERS)} on:click={() => (viewMode = ViewMode.SELECT_USERS)}
> >
Share Share
@ -356,7 +381,7 @@
<ControlAppBar on:close-button-click={handleCloseSelectAssets}> <ControlAppBar on:close-button-click={handleCloseSelectAssets}>
<svelte:fragment slot="leading"> <svelte:fragment slot="leading">
<p class="text-lg dark:text-immich-dark-fg"> <p class="text-lg dark:text-immich-dark-fg">
{#if $timelineSelected.size == 0} {#if $timelineSelected.size === 0}
Add to album Add to album
{:else} {:else}
{$timelineSelected.size.toLocaleString($locale)} selected {$timelineSelected.size.toLocaleString($locale)} selected
@ -402,7 +427,7 @@
<!-- ALBUM TITLE --> <!-- ALBUM TITLE -->
<section class="pt-24"> <section class="pt-24">
<input <input
on:keydown={(e) => e.key == 'Enter' && titleInput.blur()} on:keydown={(e) => e.key === 'Enter' && titleInput.blur()}
on:blur={handleUpdateName} on:blur={handleUpdateName}
class="w-[99%] border-b-2 border-transparent text-6xl text-immich-primary outline-none transition-all dark:text-immich-dark-primary {isOwned class="w-[99%] border-b-2 border-transparent text-6xl text-immich-primary outline-none transition-all dark:text-immich-dark-primary {isOwned
? 'hover:border-gray-400' ? 'hover:border-gray-400'
@ -478,14 +503,26 @@
{/if} {/if}
{#if album.assetCount === 0} {#if album.assetCount === 0}
<section id="empty-album" class=" mt-[200px] flex place-content-center place-items-center"> <section id="empty-album" class=" mt-[200px] flex flex-col place-content-center place-items-center">
<div class="w-[300px]"> <div class="w-[340px]">
<p class="text-xs dark:text-immich-dark-fg">ADD PHOTOS</p> <p class="text-sm dark:text-immich-dark-fg">ADD PHOTOS</p>
<button
on:click={() => (viewMode = ViewMode.RULE_SELECTION)}
class="mt-5 flex w-full place-items-center gap-6 rounded-md border bg-immich-bg px-8 py-8 text-immich-fg transition-all hover:bg-gray-100 hover:text-immich-primary dark:border-none dark:bg-immich-dark-gray dark:text-immich-dark-fg dark:hover:text-immich-dark-primary"
>
<span class="immich-text-primary"><FaceMan size="34" /> </span>
<div class="text-left">
<div class="text-lg">Select people & rules</div>
<div class="text-sm">Create an auto-updating album</div>
</div>
</button>
<button <button
on:click={() => (viewMode = ViewMode.SELECT_ASSETS)} on:click={() => (viewMode = ViewMode.SELECT_ASSETS)}
class="mt-5 flex w-full place-items-center gap-6 rounded-md border bg-immich-bg px-8 py-8 text-immich-fg transition-all hover:bg-gray-100 hover:text-immich-primary dark:border-none dark:bg-immich-dark-gray dark:text-immich-dark-fg dark:hover:text-immich-dark-primary" class="mt-5 flex w-full place-items-center gap-6 rounded-md border bg-immich-bg px-8 py-8 text-immich-fg transition-all hover:bg-gray-100 hover:text-immich-primary dark:border-none dark:bg-immich-dark-gray dark:text-immich-dark-fg dark:hover:text-immich-dark-primary"
> >
<span class="text-text-immich-primary dark:text-immich-dark-primary"><Plus size="24" /> </span> <span class="immich-text-primary"><Plus size="34" /> </span>
<span class="text-lg">Select photos</span> <span class="text-lg">Select photos</span>
</button> </button>
</div> </div>
@ -537,3 +574,11 @@
on:updated={({ detail: description }) => handleUpdateDescription(description)} on:updated={({ detail: description }) => handleUpdateDescription(description)}
/> />
{/if} {/if}
{#if viewMode === ViewMode.RULE_SELECTION}
<RuleSelection
on:close={() => (viewMode = ViewMode.VIEW)}
rules={album.rules}
on:submit={({ detail: rules }) => handleUpdateRules(rules)}
/>
{/if}