mirror of
https://github.com/immich-app/immich.git
synced 2026-05-30 18:55:19 -04:00
refactor: rule controller
This commit is contained in:
@@ -384,101 +384,6 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/album/{id}/rule": {
|
||||
"post": {
|
||||
"operationId": "createRule",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"format": "uuid",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/CreateRuleDto"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"201": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/AlbumEntity"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearer": []
|
||||
},
|
||||
{
|
||||
"cookie": []
|
||||
},
|
||||
{
|
||||
"api_key": []
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"Album"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/album/{id}/rule/{ruleId}": {
|
||||
"delete": {
|
||||
"operationId": "removeRule",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"format": "uuid",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "ruleId",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearer": []
|
||||
},
|
||||
{
|
||||
"cookie": []
|
||||
},
|
||||
{
|
||||
"api_key": []
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"Album"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/album/{id}/user/{userId}": {
|
||||
"delete": {
|
||||
"operationId": "removeUserFromAlbum",
|
||||
@@ -4835,85 +4740,6 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"AlbumEntity": {
|
||||
"properties": {
|
||||
"albumName": {
|
||||
"type": "string"
|
||||
},
|
||||
"albumThumbnailAsset": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/AssetEntity"
|
||||
}
|
||||
],
|
||||
"nullable": true
|
||||
},
|
||||
"albumThumbnailAssetId": {
|
||||
"nullable": true,
|
||||
"type": "string"
|
||||
},
|
||||
"assets": {
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/AssetEntity"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"createdAt": {
|
||||
"format": "date-time",
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"owner": {
|
||||
"$ref": "#/components/schemas/UserEntity"
|
||||
},
|
||||
"ownerId": {
|
||||
"type": "string"
|
||||
},
|
||||
"rules": {
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/RuleEntity"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"sharedLinks": {
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/SharedLinkEntity"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"sharedUsers": {
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/UserEntity"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"updatedAt": {
|
||||
"format": "date-time",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"owner",
|
||||
"ownerId",
|
||||
"albumName",
|
||||
"description",
|
||||
"createdAt",
|
||||
"updatedAt",
|
||||
"albumThumbnailAsset",
|
||||
"albumThumbnailAssetId",
|
||||
"sharedUsers",
|
||||
"assets",
|
||||
"sharedLinks",
|
||||
"rules"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"AlbumResponseDto": {
|
||||
"properties": {
|
||||
"albumName": {
|
||||
@@ -5125,223 +4951,6 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"AssetEntity": {
|
||||
"properties": {
|
||||
"albums": {
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/AlbumEntity"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"checksum": {
|
||||
"type": "object"
|
||||
},
|
||||
"createdAt": {
|
||||
"format": "date-time",
|
||||
"type": "string"
|
||||
},
|
||||
"deviceAssetId": {
|
||||
"type": "string"
|
||||
},
|
||||
"deviceId": {
|
||||
"type": "string"
|
||||
},
|
||||
"duration": {
|
||||
"nullable": true,
|
||||
"type": "string"
|
||||
},
|
||||
"encodedVideoPath": {
|
||||
"nullable": true,
|
||||
"type": "string"
|
||||
},
|
||||
"exifInfo": {
|
||||
"$ref": "#/components/schemas/ExifEntity"
|
||||
},
|
||||
"faces": {
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/AssetFaceEntity"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"fileCreatedAt": {
|
||||
"format": "date-time",
|
||||
"type": "string"
|
||||
},
|
||||
"fileModifiedAt": {
|
||||
"format": "date-time",
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"isArchived": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"isFavorite": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"isReadOnly": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"isVisible": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"livePhotoVideo": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/AssetEntity"
|
||||
}
|
||||
],
|
||||
"nullable": true
|
||||
},
|
||||
"livePhotoVideoId": {
|
||||
"nullable": true,
|
||||
"type": "string"
|
||||
},
|
||||
"originalFileName": {
|
||||
"type": "string"
|
||||
},
|
||||
"originalPath": {
|
||||
"type": "string"
|
||||
},
|
||||
"owner": {
|
||||
"$ref": "#/components/schemas/UserEntity"
|
||||
},
|
||||
"ownerId": {
|
||||
"type": "string"
|
||||
},
|
||||
"resizePath": {
|
||||
"nullable": true,
|
||||
"type": "string"
|
||||
},
|
||||
"sharedLinks": {
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/SharedLinkEntity"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"sidecarPath": {
|
||||
"nullable": true,
|
||||
"type": "string"
|
||||
},
|
||||
"smartInfo": {
|
||||
"$ref": "#/components/schemas/SmartInfoEntity"
|
||||
},
|
||||
"tags": {
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/TagEntity"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"thumbhash": {
|
||||
"nullable": true,
|
||||
"type": "object"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"IMAGE",
|
||||
"VIDEO",
|
||||
"AUDIO",
|
||||
"OTHER"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"updatedAt": {
|
||||
"format": "date-time",
|
||||
"type": "string"
|
||||
},
|
||||
"webpPath": {
|
||||
"nullable": true,
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"deviceAssetId",
|
||||
"owner",
|
||||
"ownerId",
|
||||
"deviceId",
|
||||
"type",
|
||||
"originalPath",
|
||||
"resizePath",
|
||||
"webpPath",
|
||||
"thumbhash",
|
||||
"encodedVideoPath",
|
||||
"createdAt",
|
||||
"updatedAt",
|
||||
"fileCreatedAt",
|
||||
"fileModifiedAt",
|
||||
"isFavorite",
|
||||
"isArchived",
|
||||
"isReadOnly",
|
||||
"checksum",
|
||||
"duration",
|
||||
"isVisible",
|
||||
"livePhotoVideo",
|
||||
"livePhotoVideoId",
|
||||
"originalFileName",
|
||||
"sidecarPath",
|
||||
"tags",
|
||||
"sharedLinks",
|
||||
"faces"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"AssetFaceEntity": {
|
||||
"properties": {
|
||||
"asset": {
|
||||
"$ref": "#/components/schemas/AssetEntity"
|
||||
},
|
||||
"assetId": {
|
||||
"type": "string"
|
||||
},
|
||||
"boundingBoxX1": {
|
||||
"type": "number"
|
||||
},
|
||||
"boundingBoxX2": {
|
||||
"type": "number"
|
||||
},
|
||||
"boundingBoxY1": {
|
||||
"type": "number"
|
||||
},
|
||||
"boundingBoxY2": {
|
||||
"type": "number"
|
||||
},
|
||||
"embedding": {
|
||||
"items": {
|
||||
"type": "number"
|
||||
},
|
||||
"nullable": true,
|
||||
"type": "array"
|
||||
},
|
||||
"imageHeight": {
|
||||
"type": "number"
|
||||
},
|
||||
"imageWidth": {
|
||||
"type": "number"
|
||||
},
|
||||
"person": {
|
||||
"$ref": "#/components/schemas/PersonEntity"
|
||||
},
|
||||
"personId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"assetId",
|
||||
"personId",
|
||||
"embedding",
|
||||
"imageWidth",
|
||||
"imageHeight",
|
||||
"boundingBoxX1",
|
||||
"boundingBoxY1",
|
||||
"boundingBoxX2",
|
||||
"boundingBoxY2",
|
||||
"asset",
|
||||
"person"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"AssetFileUploadResponseDto": {
|
||||
"properties": {
|
||||
"duplicate": {
|
||||
@@ -5788,21 +5397,6 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"CreateRuleDto": {
|
||||
"properties": {
|
||||
"key": {
|
||||
"$ref": "#/components/schemas/RuleKey"
|
||||
},
|
||||
"value": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"key",
|
||||
"value"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"CreateTagDto": {
|
||||
"properties": {
|
||||
"name": {
|
||||
@@ -5984,142 +5578,6 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ExifEntity": {
|
||||
"properties": {
|
||||
"asset": {
|
||||
"$ref": "#/components/schemas/AssetEntity"
|
||||
},
|
||||
"assetId": {
|
||||
"type": "string"
|
||||
},
|
||||
"city": {
|
||||
"nullable": true,
|
||||
"type": "string"
|
||||
},
|
||||
"country": {
|
||||
"nullable": true,
|
||||
"type": "string"
|
||||
},
|
||||
"dateTimeOriginal": {
|
||||
"format": "date-time",
|
||||
"nullable": true,
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"description": "General info",
|
||||
"type": "string"
|
||||
},
|
||||
"exifImageHeight": {
|
||||
"nullable": true,
|
||||
"type": "number"
|
||||
},
|
||||
"exifImageWidth": {
|
||||
"nullable": true,
|
||||
"type": "number"
|
||||
},
|
||||
"exifTextSearchableColumn": {
|
||||
"type": "string"
|
||||
},
|
||||
"exposureTime": {
|
||||
"nullable": true,
|
||||
"type": "string"
|
||||
},
|
||||
"fNumber": {
|
||||
"nullable": true,
|
||||
"type": "number"
|
||||
},
|
||||
"fileSizeInByte": {
|
||||
"nullable": true,
|
||||
"type": "number"
|
||||
},
|
||||
"focalLength": {
|
||||
"nullable": true,
|
||||
"type": "number"
|
||||
},
|
||||
"fps": {
|
||||
"description": "Video info",
|
||||
"nullable": true,
|
||||
"type": "number"
|
||||
},
|
||||
"iso": {
|
||||
"nullable": true,
|
||||
"type": "number"
|
||||
},
|
||||
"latitude": {
|
||||
"nullable": true,
|
||||
"type": "number"
|
||||
},
|
||||
"lensModel": {
|
||||
"nullable": true,
|
||||
"type": "string"
|
||||
},
|
||||
"livePhotoCID": {
|
||||
"nullable": true,
|
||||
"type": "string"
|
||||
},
|
||||
"longitude": {
|
||||
"nullable": true,
|
||||
"type": "number"
|
||||
},
|
||||
"make": {
|
||||
"description": "Image info",
|
||||
"nullable": true,
|
||||
"type": "string"
|
||||
},
|
||||
"model": {
|
||||
"nullable": true,
|
||||
"type": "string"
|
||||
},
|
||||
"modifyDate": {
|
||||
"format": "date-time",
|
||||
"nullable": true,
|
||||
"type": "string"
|
||||
},
|
||||
"orientation": {
|
||||
"nullable": true,
|
||||
"type": "string"
|
||||
},
|
||||
"projectionType": {
|
||||
"nullable": true,
|
||||
"type": "string"
|
||||
},
|
||||
"state": {
|
||||
"nullable": true,
|
||||
"type": "string"
|
||||
},
|
||||
"timeZone": {
|
||||
"nullable": true,
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"assetId",
|
||||
"description",
|
||||
"exifImageWidth",
|
||||
"exifImageHeight",
|
||||
"fileSizeInByte",
|
||||
"orientation",
|
||||
"dateTimeOriginal",
|
||||
"modifyDate",
|
||||
"timeZone",
|
||||
"latitude",
|
||||
"longitude",
|
||||
"projectionType",
|
||||
"city",
|
||||
"livePhotoCID",
|
||||
"state",
|
||||
"country",
|
||||
"make",
|
||||
"model",
|
||||
"lensModel",
|
||||
"fNumber",
|
||||
"focalLength",
|
||||
"iso",
|
||||
"exposureTime",
|
||||
"exifTextSearchableColumn"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ExifResponseDto": {
|
||||
"properties": {
|
||||
"city": {
|
||||
@@ -6618,54 +6076,6 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"PersonEntity": {
|
||||
"properties": {
|
||||
"createdAt": {
|
||||
"format": "date-time",
|
||||
"type": "string"
|
||||
},
|
||||
"faces": {
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/AssetFaceEntity"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"isHidden": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"owner": {
|
||||
"$ref": "#/components/schemas/UserEntity"
|
||||
},
|
||||
"ownerId": {
|
||||
"type": "string"
|
||||
},
|
||||
"thumbnailPath": {
|
||||
"type": "string"
|
||||
},
|
||||
"updatedAt": {
|
||||
"format": "date-time",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"createdAt",
|
||||
"updatedAt",
|
||||
"ownerId",
|
||||
"owner",
|
||||
"name",
|
||||
"thumbnailPath",
|
||||
"faces",
|
||||
"isHidden"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"PersonResponseDto": {
|
||||
"properties": {
|
||||
"id": {
|
||||
@@ -6721,56 +6131,24 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"RuleEntity": {
|
||||
"properties": {
|
||||
"album": {
|
||||
"$ref": "#/components/schemas/AlbumEntity"
|
||||
},
|
||||
"albumId": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"key": {
|
||||
"enum": [
|
||||
"personId",
|
||||
"exifInfo.city",
|
||||
"asset.fileCreatedAt"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"ownerId": {
|
||||
"type": "string"
|
||||
},
|
||||
"user": {
|
||||
"$ref": "#/components/schemas/UserEntity"
|
||||
},
|
||||
"value": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"key",
|
||||
"value",
|
||||
"ownerId",
|
||||
"user",
|
||||
"albumId",
|
||||
"album"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"RuleKey": {
|
||||
"enum": [
|
||||
"personId",
|
||||
"exifInfo.city",
|
||||
"asset.fileCreatedAt"
|
||||
"person",
|
||||
"taken-after",
|
||||
"city",
|
||||
"state",
|
||||
"country",
|
||||
"make",
|
||||
"model",
|
||||
"location"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"RuleResponseDto": {
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"key": {
|
||||
"$ref": "#/components/schemas/RuleKey"
|
||||
},
|
||||
@@ -6778,11 +6156,12 @@
|
||||
"type": "string"
|
||||
},
|
||||
"value": {
|
||||
"type": "string"
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"key",
|
||||
"id",
|
||||
"value",
|
||||
"ownerId"
|
||||
],
|
||||
@@ -7152,80 +6531,6 @@
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"SharedLinkEntity": {
|
||||
"properties": {
|
||||
"album": {
|
||||
"$ref": "#/components/schemas/AlbumEntity"
|
||||
},
|
||||
"albumId": {
|
||||
"nullable": true,
|
||||
"type": "string"
|
||||
},
|
||||
"allowDownload": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"allowUpload": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"assets": {
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/AssetEntity"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"createdAt": {
|
||||
"format": "date-time",
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"nullable": true,
|
||||
"type": "string"
|
||||
},
|
||||
"expiresAt": {
|
||||
"format": "date-time",
|
||||
"nullable": true,
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"key": {
|
||||
"type": "object"
|
||||
},
|
||||
"showExif": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"ALBUM",
|
||||
"INDIVIDUAL"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"user": {
|
||||
"$ref": "#/components/schemas/UserEntity"
|
||||
},
|
||||
"userId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"description",
|
||||
"userId",
|
||||
"user",
|
||||
"key",
|
||||
"type",
|
||||
"createdAt",
|
||||
"expiresAt",
|
||||
"allowUpload",
|
||||
"allowDownload",
|
||||
"showExif",
|
||||
"assets",
|
||||
"albumId"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"SharedLinkResponseDto": {
|
||||
"properties": {
|
||||
"album": {
|
||||
@@ -7321,44 +6626,6 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"SmartInfoEntity": {
|
||||
"properties": {
|
||||
"asset": {
|
||||
"$ref": "#/components/schemas/AssetEntity"
|
||||
},
|
||||
"assetId": {
|
||||
"type": "string"
|
||||
},
|
||||
"clipEmbedding": {
|
||||
"items": {
|
||||
"type": "number"
|
||||
},
|
||||
"nullable": true,
|
||||
"type": "array"
|
||||
},
|
||||
"objects": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"nullable": true,
|
||||
"type": "array"
|
||||
},
|
||||
"tags": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"nullable": true,
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"assetId",
|
||||
"tags",
|
||||
"objects",
|
||||
"clipEmbedding"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"SmartInfoResponseDto": {
|
||||
"properties": {
|
||||
"objects": {
|
||||
@@ -7655,50 +6922,6 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"TagEntity": {
|
||||
"properties": {
|
||||
"assets": {
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/AssetEntity"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"renameTagId": {
|
||||
"nullable": true,
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"OBJECT",
|
||||
"FACE",
|
||||
"CUSTOM"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"user": {
|
||||
"$ref": "#/components/schemas/UserEntity"
|
||||
},
|
||||
"userId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"type",
|
||||
"name",
|
||||
"user",
|
||||
"userId",
|
||||
"renameTagId",
|
||||
"assets"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"TagResponseDto": {
|
||||
"properties": {
|
||||
"id": {
|
||||
@@ -7917,92 +7140,6 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"UserEntity": {
|
||||
"properties": {
|
||||
"assets": {
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/AssetEntity"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"createdAt": {
|
||||
"format": "date-time",
|
||||
"type": "string"
|
||||
},
|
||||
"deletedAt": {
|
||||
"format": "date-time",
|
||||
"nullable": true,
|
||||
"type": "string"
|
||||
},
|
||||
"email": {
|
||||
"type": "string"
|
||||
},
|
||||
"externalPath": {
|
||||
"nullable": true,
|
||||
"type": "string"
|
||||
},
|
||||
"firstName": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"isAdmin": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"lastName": {
|
||||
"type": "string"
|
||||
},
|
||||
"memoriesEnabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"oauthId": {
|
||||
"type": "string"
|
||||
},
|
||||
"password": {
|
||||
"type": "string"
|
||||
},
|
||||
"profileImagePath": {
|
||||
"type": "string"
|
||||
},
|
||||
"shouldChangePassword": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"storageLabel": {
|
||||
"nullable": true,
|
||||
"type": "string"
|
||||
},
|
||||
"tags": {
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/TagEntity"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"updatedAt": {
|
||||
"format": "date-time",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"firstName",
|
||||
"lastName",
|
||||
"isAdmin",
|
||||
"email",
|
||||
"storageLabel",
|
||||
"externalPath",
|
||||
"oauthId",
|
||||
"profileImagePath",
|
||||
"shouldChangePassword",
|
||||
"createdAt",
|
||||
"deletedAt",
|
||||
"updatedAt",
|
||||
"memoriesEnabled",
|
||||
"tags",
|
||||
"assets"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"UserResponseDto": {
|
||||
"properties": {
|
||||
"createdAt": {
|
||||
|
||||
@@ -19,6 +19,11 @@ export enum Permission {
|
||||
ALBUM_SHARE = 'album.share',
|
||||
ALBUM_DOWNLOAD = 'album.download',
|
||||
|
||||
RULE_READ = 'rule.read',
|
||||
RULE_CREATE = 'rule.create',
|
||||
RULE_UPDATE = 'rule.update',
|
||||
RULE_DELETE = 'rule.delete',
|
||||
|
||||
LIBRARY_READ = 'library.read',
|
||||
LIBRARY_DOWNLOAD = 'library.download',
|
||||
}
|
||||
@@ -156,6 +161,18 @@ export class AccessCore {
|
||||
case Permission.ALBUM_REMOVE_ASSET:
|
||||
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.LIBRARY_READ:
|
||||
return authUser.id === id || (await this.repository.library.hasPartnerAccess(authUser.id, id));
|
||||
|
||||
|
||||
@@ -14,6 +14,10 @@ export interface IAccessRepository {
|
||||
hasSharedLinkAccess(sharedLinkId: string, albumId: string): Promise<boolean>;
|
||||
};
|
||||
|
||||
rule: {
|
||||
hasOwnerAccess(userId: string, ruleId: string): Promise<boolean>;
|
||||
};
|
||||
|
||||
library: {
|
||||
hasPartnerAccess(userId: string, partnerId: string): Promise<boolean>;
|
||||
};
|
||||
|
||||
@@ -2,7 +2,7 @@ import { AlbumEntity } from '@app/infra/entities';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { AssetResponseDto, mapAsset } from '../asset';
|
||||
import { mapUser, UserResponseDto } from '../user';
|
||||
import { RuleResponseDto } from './dto/rule.dto';
|
||||
import { mapRule, RuleResponseDto } from './dto';
|
||||
|
||||
export class AlbumResponseDto {
|
||||
id!: string;
|
||||
@@ -26,13 +26,7 @@ export class AlbumResponseDto {
|
||||
}
|
||||
|
||||
export const mapAlbum = (entity: AlbumEntity, withAssets: boolean): AlbumResponseDto => {
|
||||
const sharedUsers: UserResponseDto[] = [];
|
||||
|
||||
entity.sharedUsers?.forEach((user) => {
|
||||
const userDto = mapUser(user);
|
||||
sharedUsers.push(userDto);
|
||||
});
|
||||
|
||||
const sharedUsers: UserResponseDto[] = (entity.sharedUsers || []).map(mapUser);
|
||||
const assets = entity.assets || [];
|
||||
|
||||
const hasSharedLink = entity.sharedLinks?.length > 0;
|
||||
@@ -54,7 +48,7 @@ export const mapAlbum = (entity: AlbumEntity, withAssets: boolean): AlbumRespons
|
||||
endDate: assets.at(-1)?.fileCreatedAt || undefined,
|
||||
assets: (withAssets ? assets : []).map((asset) => mapAsset(asset)),
|
||||
assetCount: entity.assets?.length || 0,
|
||||
rules: entity.rules?.map((rule) => ({ key: rule.key, value: rule.value, ownerId: rule.ownerId })) || [],
|
||||
rules: (entity.rules || []).map(mapRule),
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -185,6 +185,7 @@ describe(AlbumService.name, () => {
|
||||
endDate: undefined,
|
||||
hasSharedLink: false,
|
||||
updatedAt: expect.anything(),
|
||||
rules: [],
|
||||
});
|
||||
|
||||
expect(jobMock.queue).toHaveBeenCalledWith({
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { AlbumEntity, AssetEntity, RuleEntity, UserEntity } from '@app/infra/entities';
|
||||
import { AlbumEntity, AssetEntity, UserEntity } from '@app/infra/entities';
|
||||
import { BadRequestException, Inject, Injectable } from '@nestjs/common';
|
||||
import { AccessCore, IAccessRepository, Permission } from '../access';
|
||||
import { BulkIdErrorReason, BulkIdResponseDto, BulkIdsDto, IAssetRepository } from '../asset';
|
||||
@@ -13,8 +13,7 @@ import {
|
||||
mapAlbumWithoutAssets,
|
||||
} from './album-response.dto';
|
||||
import { IAlbumRepository } from './album.repository';
|
||||
import { AddUsersDto, AlbumInfoDto, CreateAlbumDto, CreateRuleDto, GetAlbumsDto, UpdateAlbumDto } from './dto';
|
||||
import { IRuleRepository } from './rule.repository';
|
||||
import { AddUsersDto, AlbumInfoDto, CreateAlbumDto, GetAlbumsDto, UpdateAlbumDto } from './dto';
|
||||
|
||||
@Injectable()
|
||||
export class AlbumService {
|
||||
@@ -25,7 +24,6 @@ export class AlbumService {
|
||||
@Inject(IAssetRepository) private assetRepository: IAssetRepository,
|
||||
@Inject(IJobRepository) private jobRepository: IJobRepository,
|
||||
@Inject(IUserRepository) private userRepository: IUserRepository,
|
||||
@Inject(IRuleRepository) private ruleRepository: IRuleRepository,
|
||||
) {
|
||||
this.access = new AccessCore(accessRepository);
|
||||
}
|
||||
@@ -104,7 +102,6 @@ export class AlbumService {
|
||||
sharedUsers: dto.sharedWithUserIds?.map((value) => ({ id: value } as UserEntity)) ?? [],
|
||||
assets: (dto.assetIds || []).map((id) => ({ id } as AssetEntity)),
|
||||
albumThumbnailAssetId: dto.assetIds?.[0] || null,
|
||||
rules: [],
|
||||
});
|
||||
|
||||
await this.jobRepository.queue({ name: JobName.SEARCH_INDEX_ALBUM, data: { ids: [album.id] } });
|
||||
@@ -288,39 +285,4 @@ export class AlbumService {
|
||||
}
|
||||
return album;
|
||||
}
|
||||
|
||||
async addRule(authUser: AuthUserDto, albumId: string, dto: CreateRuleDto) {
|
||||
await this.access.requirePermission(authUser, Permission.ALBUM_READ, albumId);
|
||||
|
||||
const album = await this.findOrFail(albumId);
|
||||
const user = await this.userRepository.get(authUser.id);
|
||||
|
||||
if (!user) {
|
||||
throw new BadRequestException('User not found');
|
||||
}
|
||||
|
||||
const rule = new RuleEntity();
|
||||
rule.key = dto.key;
|
||||
rule.value = dto.value;
|
||||
rule.album = album;
|
||||
rule.albumId = album.id;
|
||||
rule.user = user;
|
||||
rule.ownerId = user.id;
|
||||
|
||||
await this.ruleRepository.create(rule);
|
||||
|
||||
return await this.findOrFail(albumId);
|
||||
}
|
||||
|
||||
async removeRule(authUser: AuthUserDto, albumId: string, ruleId: string) {
|
||||
await this.access.requirePermission(authUser, Permission.ALBUM_READ, albumId);
|
||||
|
||||
const album = await this.findOrFail(albumId);
|
||||
const rule = album.rules.find((rule) => rule.id === ruleId);
|
||||
if (!rule) {
|
||||
throw new BadRequestException('Rule not found');
|
||||
}
|
||||
|
||||
await this.ruleRepository.delete(rule);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,4 +3,4 @@ export * from './album-create.dto';
|
||||
export * from './album-update.dto';
|
||||
export * from './album.dto';
|
||||
export * from './get-albums.dto';
|
||||
export * from './rule.dto';
|
||||
export * from '../../rule/rule.dto';
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
import { RuleKey } from '@app/infra/entities';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsNotEmpty } from 'class-validator';
|
||||
|
||||
export class CreateRuleDto {
|
||||
@ApiProperty({ enumName: 'RuleKey', enum: RuleKey })
|
||||
@IsNotEmpty()
|
||||
key!: RuleKey;
|
||||
|
||||
@IsNotEmpty()
|
||||
value!: string;
|
||||
}
|
||||
|
||||
export class RuleResponseDto {
|
||||
@ApiProperty({ enumName: 'RuleKey', enum: RuleKey })
|
||||
key!: RuleKey;
|
||||
value!: string;
|
||||
ownerId!: string;
|
||||
}
|
||||
@@ -2,4 +2,3 @@ export * from './album-response.dto';
|
||||
export * from './album.repository';
|
||||
export * from './album.service';
|
||||
export * from './dto';
|
||||
export * from './rule.repository';
|
||||
|
||||
@@ -15,6 +15,7 @@ export * from './media';
|
||||
export * from './metadata';
|
||||
export * from './partner';
|
||||
export * from './person';
|
||||
export * from './rule';
|
||||
export * from './search';
|
||||
export * from './server-info';
|
||||
export * from './shared-link';
|
||||
|
||||
@@ -95,6 +95,7 @@ describe(JobService.name, () => {
|
||||
[QueueName.THUMBNAIL_GENERATION]: expectedJobStatus,
|
||||
[QueueName.VIDEO_CONVERSION]: expectedJobStatus,
|
||||
[QueueName.RECOGNIZE_FACES]: expectedJobStatus,
|
||||
[QueueName.SMART_ALBUM]: expectedJobStatus,
|
||||
[QueueName.SIDECAR]: expectedJobStatus,
|
||||
});
|
||||
});
|
||||
@@ -224,6 +225,7 @@ describe(JobService.name, () => {
|
||||
[QueueName.RECOGNIZE_FACES]: { concurrency: 10 },
|
||||
[QueueName.SEARCH]: { concurrency: 10 },
|
||||
[QueueName.SIDECAR]: { concurrency: 10 },
|
||||
[QueueName.SMART_ALBUM]: { concurrency: 10 },
|
||||
[QueueName.STORAGE_TEMPLATE_MIGRATION]: { concurrency: 10 },
|
||||
[QueueName.THUMBNAIL_GENERATION]: { 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.RECOGNIZE_FACES, 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.THUMBNAIL_GENERATION, 10);
|
||||
expect(jobMock.setConcurrency).toHaveBeenCalledWith(QueueName.VIDEO_CONVERSION, 10);
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from './rule.dto';
|
||||
export * from './rule.repository';
|
||||
export * from './rule.service';
|
||||
@@ -0,0 +1,44 @@
|
||||
import { RuleEntity, RuleKey } from '@app/infra/entities';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsEnum, IsNotEmpty, IsOptional } from 'class-validator';
|
||||
import { ValidateUUID } from '../domain.util';
|
||||
|
||||
export class CreateRuleDto {
|
||||
@ValidateUUID()
|
||||
albumId!: string;
|
||||
|
||||
@ApiProperty({ enumName: 'RuleKey', enum: RuleKey })
|
||||
@IsEnum(RuleKey)
|
||||
key!: RuleKey;
|
||||
|
||||
@IsNotEmpty()
|
||||
value!: any;
|
||||
}
|
||||
|
||||
export class UpdateRuleDto {
|
||||
@ApiProperty({ enumName: 'RuleKey', enum: RuleKey })
|
||||
@IsOptional()
|
||||
@IsEnum(RuleKey)
|
||||
key?: RuleKey;
|
||||
|
||||
@IsOptional()
|
||||
@IsNotEmpty()
|
||||
value?: any;
|
||||
}
|
||||
|
||||
export class RuleResponseDto {
|
||||
id!: string;
|
||||
@ApiProperty({ enumName: 'RuleKey', enum: RuleKey })
|
||||
key!: RuleKey;
|
||||
value!: any;
|
||||
ownerId!: string;
|
||||
}
|
||||
|
||||
export const mapRule = (rule: RuleEntity): RuleResponseDto => {
|
||||
return {
|
||||
id: rule.id,
|
||||
key: rule.key,
|
||||
value: rule.value,
|
||||
ownerId: rule.ownerId,
|
||||
};
|
||||
};
|
||||
+3
-1
@@ -3,6 +3,8 @@ import { RuleEntity } from '@app/infra/entities';
|
||||
export const IRuleRepository = 'IRuleRepository';
|
||||
|
||||
export interface IRuleRepository {
|
||||
create(rule: RuleEntity): Promise<RuleEntity>;
|
||||
get(id: string): Promise<RuleEntity | null>;
|
||||
create(rule: Partial<RuleEntity>): Promise<RuleEntity>;
|
||||
update(rule: Partial<RuleEntity>): Promise<RuleEntity>;
|
||||
delete(rule: RuleEntity): Promise<RuleEntity>;
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
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: '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: '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', { value: 'Atlanta' })).rejects.toBeInstanceOf(
|
||||
BadRequestException,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,64 @@
|
||||
import { BadRequestException, Inject, Injectable } from '@nestjs/common';
|
||||
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);
|
||||
|
||||
// TODO additional validation on key and value
|
||||
|
||||
const rule = await this.repository.create({
|
||||
key: dto.key,
|
||||
value: dto.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);
|
||||
|
||||
// TODO additional validation on key and value
|
||||
|
||||
const rule = await this.repository.update({
|
||||
id,
|
||||
key: dto.key,
|
||||
value: dto.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;
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,7 @@ const updatedConfig = Object.freeze<SystemConfig>({
|
||||
[QueueName.RECOGNIZE_FACES]: { concurrency: 2 },
|
||||
[QueueName.SEARCH]: { concurrency: 5 },
|
||||
[QueueName.SIDECAR]: { concurrency: 5 },
|
||||
[QueueName.SMART_ALBUM]: { concurrency: 1 },
|
||||
[QueueName.STORAGE_TEMPLATE_MIGRATION]: { concurrency: 5 },
|
||||
[QueueName.THUMBNAIL_GENERATION]: { concurrency: 5 },
|
||||
[QueueName.VIDEO_CONVERSION]: { concurrency: 1 },
|
||||
|
||||
@@ -60,7 +60,6 @@ export class UserCore {
|
||||
dto.externalPath = null;
|
||||
}
|
||||
|
||||
console.log(dto.memoriesEnabled);
|
||||
return this.userRepository.update(id, dto);
|
||||
} catch (e) {
|
||||
Logger.error(e, 'Failed to update user info');
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
BulkIdResponseDto,
|
||||
BulkIdsDto,
|
||||
CreateAlbumDto as CreateDto,
|
||||
CreateRuleDto,
|
||||
GetAlbumsDto,
|
||||
UpdateAlbumDto as UpdateDto,
|
||||
} from '@app/domain';
|
||||
@@ -93,18 +92,4 @@ export class AlbumController {
|
||||
) {
|
||||
return this.service.removeUser(authUser, id, userId);
|
||||
}
|
||||
|
||||
@Post(':id/rule')
|
||||
createRule(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto, @Body() dto: CreateRuleDto) {
|
||||
return this.service.addRule(authUser, id, dto);
|
||||
}
|
||||
|
||||
@Delete(':id/rule/:ruleId')
|
||||
removeRule(
|
||||
@AuthUser() authUser: AuthUserDto,
|
||||
@Param() { id }: UUIDParamDto,
|
||||
@Param('ruleId', new ParseMeUUIDPipe({ version: '4' })) ruleId: string,
|
||||
) {
|
||||
return this.service.removeRule(authUser, id, ruleId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ export * from './job.controller';
|
||||
export * from './oauth.controller';
|
||||
export * from './partner.controller';
|
||||
export * from './person.controller';
|
||||
export * from './rule.controller';
|
||||
export * from './search.controller';
|
||||
export * from './server-info.controller';
|
||||
export * from './shared-link.controller';
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -1,23 +1,24 @@
|
||||
import { Column, Entity, Index, ManyToOne, PrimaryGeneratedColumn } from 'typeorm';
|
||||
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 {
|
||||
export class RuleEntity<T = RuleValue> {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id!: string;
|
||||
|
||||
@Column()
|
||||
key!: RuleKey;
|
||||
|
||||
@Column()
|
||||
value!: string;
|
||||
@Column({ type: 'varchar', transformer: JSON_TRANSFORMER })
|
||||
value!: T;
|
||||
|
||||
@Column()
|
||||
ownerId!: string;
|
||||
|
||||
@ManyToOne(() => UserEntity)
|
||||
user!: UserEntity;
|
||||
owner!: UserEntity;
|
||||
|
||||
@Column()
|
||||
albumId!: string;
|
||||
@@ -27,7 +28,38 @@ export class RuleEntity {
|
||||
}
|
||||
|
||||
export enum RuleKey {
|
||||
PERSON = 'personId',
|
||||
EXIF_CITY = 'exifInfo.city',
|
||||
DATE_AFTER = 'asset.fileCreatedAt',
|
||||
PERSON = 'person',
|
||||
TAKEN_AFTER = 'taken-after',
|
||||
CITY = 'city',
|
||||
STATE = 'state',
|
||||
COUNTRY = 'country',
|
||||
MAKE = 'make',
|
||||
MODEL = 'model',
|
||||
LOCATION = 'location',
|
||||
}
|
||||
|
||||
export type RuleValue = string | Date | RuleGeoValue;
|
||||
|
||||
export enum RuleValueType {
|
||||
UUID = 'uuid',
|
||||
STRING = 'string',
|
||||
DATE = 'date',
|
||||
GEO = 'geo',
|
||||
}
|
||||
|
||||
export interface RuleGeoValue {
|
||||
lat: number;
|
||||
long: 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.MAKE]: RuleValueType.STRING,
|
||||
[RuleKey.MODEL]: RuleValueType.STRING,
|
||||
[RuleKey.LOCATION]: RuleValueType.GEO,
|
||||
};
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { QueueName } from '@app/domain/job/job.constants';
|
||||
import { Column, Entity, PrimaryColumn } from 'typeorm';
|
||||
import { JSON_TRANSFORMER } from '../infra.util';
|
||||
|
||||
@Entity('system_config')
|
||||
export class SystemConfigEntity<T = SystemConfigValue> {
|
||||
@PrimaryColumn()
|
||||
key!: SystemConfigKey;
|
||||
|
||||
@Column({ type: 'varchar', nullable: true, transformer: { to: JSON.stringify, from: JSON.parse } })
|
||||
value!: T;
|
||||
@Column({ type: 'varchar', nullable: true, transformer: JSON_TRANSFORMER }) value!: T;
|
||||
}
|
||||
|
||||
export type SystemConfigValue = string | number | boolean;
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export const JSON_TRANSFORMER = { to: JSON.stringify, from: JSON.parse };
|
||||
@@ -1,13 +1,14 @@
|
||||
import { IAccessRepository } from '@app/domain';
|
||||
import { InjectRepository } from '@nestjs/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 {
|
||||
constructor(
|
||||
@InjectRepository(AssetEntity) private assetRepository: Repository<AssetEntity>,
|
||||
@InjectRepository(AlbumEntity) private albumRepository: Repository<AlbumEntity>,
|
||||
@InjectRepository(PartnerEntity) private partnerRepository: Repository<PartnerEntity>,
|
||||
@InjectRepository(RuleEntity) private ruleRepository: Repository<RuleEntity>,
|
||||
@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,
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -6,11 +6,24 @@ import { RuleEntity } from '../entities';
|
||||
export class RuleRepository implements IRuleRepository {
|
||||
constructor(@InjectRepository(RuleEntity) private repository: Repository<RuleEntity>) {}
|
||||
|
||||
create(rule: RuleEntity): Promise<RuleEntity> {
|
||||
return this.repository.save(rule);
|
||||
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 } });
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+1
@@ -9,6 +9,7 @@ export * from './file.stub';
|
||||
export * from './media.stub';
|
||||
export * from './partner.stub';
|
||||
export * from './person.stub';
|
||||
export * from './rule.stub';
|
||||
export * from './search.stub';
|
||||
export * from './shared-link.stub';
|
||||
export * from './system-config.stub';
|
||||
|
||||
Vendored
+15
@@ -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,
|
||||
}),
|
||||
};
|
||||
+2
@@ -80,6 +80,7 @@ const albumResponse: AlbumResponseDto = {
|
||||
hasSharedLink: false,
|
||||
assets: [],
|
||||
assetCount: 1,
|
||||
rules: [],
|
||||
};
|
||||
|
||||
export const sharedLinkStub = {
|
||||
@@ -222,6 +223,7 @@ export const sharedLinkStub = {
|
||||
sidecarPath: null,
|
||||
},
|
||||
],
|
||||
rules: [],
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -4,6 +4,7 @@ export type IAccessRepositoryMock = {
|
||||
asset: jest.Mocked<IAccessRepository['asset']>;
|
||||
album: jest.Mocked<IAccessRepository['album']>;
|
||||
library: jest.Mocked<IAccessRepository['library']>;
|
||||
rule: jest.Mocked<IAccessRepository['rule']>;
|
||||
};
|
||||
|
||||
export const newAccessRepositoryMock = (): IAccessRepositoryMock => {
|
||||
@@ -24,5 +25,9 @@ export const newAccessRepositoryMock = (): IAccessRepositoryMock => {
|
||||
library: {
|
||||
hasPartnerAccess: jest.fn(),
|
||||
},
|
||||
|
||||
rule: {
|
||||
hasOwnerAccess: jest.fn(),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -10,6 +10,7 @@ export * from './machine-learning.repository.mock';
|
||||
export * from './media.repository.mock';
|
||||
export * from './partner.repository.mock';
|
||||
export * from './person.repository.mock';
|
||||
export * from './rule.repository.mock';
|
||||
export * from './search.repository.mock';
|
||||
export * from './shared-link.repository.mock';
|
||||
export * from './smart-info.repository.mock';
|
||||
|
||||
@@ -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(),
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user