Compare commits

..

1 Commits

Author SHA1 Message Date
Alex
ee7e20706b fix(mobile): fetch non-archive for partners 2024-05-06 11:21:25 -05:00
1641 changed files with 43687 additions and 29990 deletions

View File

@@ -23,6 +23,7 @@ server/node_modules/
server/upload/
server/dist/
server/www/
server/test/assets/
web/node_modules/
web/coverage/

2
.gitattributes vendored
View File

@@ -2,6 +2,8 @@ mobile/openapi/**/*.md -diff -merge
mobile/openapi/**/*.md linguist-generated=true
mobile/openapi/**/*.dart -diff -merge
mobile/openapi/**/*.dart linguist-generated=true
mobile/openapi/.openapi-generator/FILES -diff -merge
mobile/openapi/.openapi-generator/FILES linguist-generated=true
mobile/lib/**/*.g.dart -diff -merge
mobile/lib/**/*.g.dart linguist-generated=true

5
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1,5 @@
# These are supported funding model platforms
github: immich-app
liberapay: alex.tran1502
custom: https://www.buymeacoffee.com/altran1502

View File

@@ -45,7 +45,7 @@ jobs:
uses: subosito/flutter-action@v2
with:
channel: 'stable'
flutter-version-file: ./mobile/pubspec.yaml
flutter-version: '3.19.3'
cache: true
- name: Create the Keystore

View File

@@ -35,7 +35,7 @@ jobs:
steps:
- name: Clean temporary images
if: "${{ env.TOKEN != '' }}"
uses: stumpylog/image-cleaner-action/ephemeral@v0.7.0
uses: stumpylog/image-cleaner-action/ephemeral@v0.6.0
with:
token: "${{ env.TOKEN }}"
owner: "immich-app"
@@ -64,7 +64,7 @@ jobs:
steps:
- name: Clean untagged images
if: "${{ env.TOKEN != '' }}"
uses: stumpylog/image-cleaner-action/untagged@v0.7.0
uses: stumpylog/image-cleaner-action/untagged@v0.6.0
with:
token: "${{ env.TOKEN }}"
owner: "immich-app"

View File

@@ -1,43 +0,0 @@
name: Docs build
on:
push:
branches: [main]
paths:
- "docs/**"
pull_request:
branches: [main]
paths:
- "docs/**"
release:
types: [published]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
build:
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./docs
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Run npm install
run: npm ci
- name: Check formatting
run: npm run format
- name: Run build
run: npm run build
- name: Upload build output
uses: actions/upload-artifact@v4
with:
name: docs-build-output
path: docs/build/
retention-days: 1

View File

@@ -1,189 +0,0 @@
name: Docs deploy
on:
workflow_run:
workflows: ["Docs build"]
types:
- completed
jobs:
checks:
runs-on: ubuntu-latest
outputs:
parameters: ${{ steps.parameters.outputs.result }}
steps:
- if: ${{ github.event.workflow_run.conclusion == 'failure' }}
run: echo 'The triggering workflow failed' && exit 1
- name: Determine deploy parameters
id: parameters
uses: actions/github-script@v7
with:
script: |
const eventType = context.payload.workflow_run.event;
const isFork = context.payload.workflow_run.repository.fork;
let parameters;
console.log({eventType, isFork});
if (eventType == "push") {
const branch = context.payload.workflow_run.head_branch;
console.log({branch});
const shouldDeploy = !isFork && branch == "main";
parameters = {
event: "branch",
name: "main",
shouldDeploy
};
} else if (eventType == "pull_request") {
let pull_number = context.payload.workflow_run.pull_requests[0]?.number;
if(!pull_number) {
const response = await github.rest.search.issuesAndPullRequests({q: 'repo:${{ github.repository }} is:pr sha:${{ github.event.workflow_run.head_sha }}',per_page: 1,})
const items = response.data.items
if (items.length < 1) {
throw new Error("No pull request found for the commit")
}
const pullRequestNumber = items[0].number
console.info("Pull request number is", pullRequestNumber)
pull_number = pullRequestNumber
}
const {data: pr} = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number
});
console.log({pull_number});
parameters = {
event: "pr",
name: `pr-${pull_number}`,
pr_number: pull_number,
shouldDeploy: true
};
} else if (eventType == "release") {
parameters = {
event: "release",
name: context.payload.workflow_run.head_branch,
shouldDeploy: !isFork
};
}
console.log(parameters);
return parameters;
deploy:
runs-on: ubuntu-latest
needs: checks
if: ${{ fromJson(needs.checks.outputs.parameters).shouldDeploy }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Load parameters
id: parameters
uses: actions/github-script@v7
with:
script: |
const json = `${{ needs.checks.outputs.parameters }}`;
const parameters = JSON.parse(json);
core.setOutput("event", parameters.event);
core.setOutput("name", parameters.name);
core.setOutput("shouldDeploy", parameters.shouldDeploy);
- run: |
echo "Starting docs deployment for ${{ steps.parameters.outputs.event }} ${{ steps.parameters.outputs.name }}"
- name: Download artifact
uses: actions/github-script@v7
with:
script: |
let allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({
owner: context.repo.owner,
repo: context.repo.repo,
run_id: context.payload.workflow_run.id,
});
let matchArtifact = allArtifacts.data.artifacts.filter((artifact) => {
return artifact.name == "docs-build-output"
})[0];
let download = await github.rest.actions.downloadArtifact({
owner: context.repo.owner,
repo: context.repo.repo,
artifact_id: matchArtifact.id,
archive_format: 'zip',
});
let fs = require('fs');
fs.writeFileSync(`${process.env.GITHUB_WORKSPACE}/docs-build-output.zip`, Buffer.from(download.data));
- name: Unzip artifact
run: unzip "${{ github.workspace }}/docs-build-output.zip" -d "${{ github.workspace }}/docs/build"
- name: Deploy Docs Subdomain
env:
TF_VAR_prefix_name: ${{ steps.parameters.outputs.name}}
TF_VAR_prefix_event_type: ${{ steps.parameters.outputs.event }}
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
TF_STATE_POSTGRES_CONN_STR: ${{ secrets.TF_STATE_POSTGRES_CONN_STR }}
uses: gruntwork-io/terragrunt-action@v2
with:
tg_version: "0.58.12"
tofu_version: "1.7.1"
tg_dir: "deployment/modules/cloudflare/docs"
tg_command: "apply"
- name: Deploy Docs Subdomain Output
id: docs-output
env:
TF_VAR_prefix_name: ${{ steps.parameters.outputs.name}}
TF_VAR_prefix_event_type: ${{ steps.parameters.outputs.event }}
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
TF_STATE_POSTGRES_CONN_STR: ${{ secrets.TF_STATE_POSTGRES_CONN_STR }}
uses: gruntwork-io/terragrunt-action@v2
with:
tg_version: "0.58.12"
tofu_version: "1.7.1"
tg_dir: "deployment/modules/cloudflare/docs"
tg_command: "output -json"
- name: Output Cleaning
id: clean
run: |
TG_OUT=$(echo '${{ steps.docs-output.outputs.tg_action_output }}' | sed 's|%0A|\n|g ; s|%3C|<|g' | jq -c .)
echo "output=$TG_OUT" >> $GITHUB_OUTPUT
- name: Publish to Cloudflare Pages
uses: cloudflare/pages-action@v1
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN_PAGES_UPLOAD }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
projectName: ${{ fromJson(steps.clean.outputs.output).pages_project_name.value }}
workingDirectory: "docs"
directory: "build"
branch: ${{ steps.parameters.outputs.name }}
wranglerVersion: '3'
- name: Deploy Docs Release Domain
if: ${{ steps.parameters.outputs.event == 'release' }}
env:
TF_VAR_prefix_name: ${{ steps.parameters.outputs.name}}
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
TF_STATE_POSTGRES_CONN_STR: ${{ secrets.TF_STATE_POSTGRES_CONN_STR }}
uses: gruntwork-io/terragrunt-action@v2
with:
tg_version: '0.58.12'
tofu_version: '1.7.1'
tg_dir: 'deployment/modules/cloudflare/docs-release'
tg_command: 'apply'
- name: Comment
uses: actions-cool/maintain-one-comment@v3
if: ${{ steps.parameters.outputs.event == 'pr' }}
with:
number: ${{ fromJson(needs.checks.outputs.parameters).pr_number }}
body: |
📖 Documentation deployed to [${{ fromJson(steps.clean.outputs.output).immich_app_branch_subdomain.value }}](https://${{ fromJson(steps.clean.outputs.output).immich_app_branch_subdomain.value }})
emojis: 'rocket'
body-include: '<!-- Docs PR URL -->'

View File

@@ -1,32 +0,0 @@
name: Docs destroy
on:
pull_request_target:
types: [closed]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Destroy Docs Subdomain
env:
TF_VAR_prefix_name: "pr-${{ github.event.number }}"
TF_VAR_prefix_event_type: "pr"
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
TF_STATE_POSTGRES_CONN_STR: ${{ secrets.TF_STATE_POSTGRES_CONN_STR }}
uses: gruntwork-io/terragrunt-action@v2
with:
tg_version: "0.58.12"
tofu_version: "1.7.1"
tg_dir: "deployment/modules/cloudflare/docs"
tg_command: "destroy"
- name: Comment
uses: actions-cool/maintain-one-comment@v3
with:
number: ${{ github.event.number }}
delete: true
body-include: '<!-- Docs PR URL -->'

View File

@@ -1,15 +0,0 @@
name: PR Conventional Commit Validation
on:
pull_request:
types: [opened, synchronize, reopened, edited]
jobs:
validate-pr-title:
runs-on: ubuntu-latest
steps:
- name: PR Conventional Commit Validation
uses: ytanikin/PRConventionalCommits@1.2.0
with:
task_types: '["feat","fix","docs","test","ci","refactor","perf","chore","revert"]'
add_label: 'false'

View File

@@ -22,8 +22,8 @@ jobs:
- name: Setup Flutter SDK
uses: subosito/flutter-action@v2
with:
channel: 'stable'
flutter-version-file: ./mobile/pubspec.yaml
channel: "stable"
flutter-version: "3.19.3"
- name: Install dependencies
run: dart pub get

View File

@@ -10,6 +10,28 @@ concurrency:
cancel-in-progress: true
jobs:
doc-tests:
name: Docs
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./docs
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Run npm install
run: npm ci
- name: Run formatter
run: npm run format
if: ${{ !cancelled() }}
- name: Run build
run: npm run build
if: ${{ !cancelled() }}
server-unit-tests:
name: Server
runs-on: ubuntu-latest
@@ -186,7 +208,7 @@ jobs:
uses: subosito/flutter-action@v2
with:
channel: 'stable'
flutter-version-file: ./mobile/pubspec.yaml
flutter-version: '3.19.3'
- name: Run tests
working-directory: ./mobile
run: flutter test -j 1
@@ -238,27 +260,16 @@ jobs:
name: OpenAPI Clients
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install server dependencies
run: npm --prefix=server ci
- name: Build the app
run: npm --prefix=server run build
- uses: actions/checkout@v4
- name: Run API generation
run: make open-api
- name: Find file changes
uses: tj-actions/verify-changed-files@v20
uses: tj-actions/verify-changed-files@v19
id: verify-changed-files
with:
files: |
mobile/openapi
open-api/typescript-sdk
open-api/immich-openapi-specs.json
- name: Verify files have not changed
if: steps.verify-changed-files.outputs.files_changed == 'true'
run: |
@@ -308,7 +319,7 @@ jobs:
run: npm run typeorm:migrations:generate ./src/migrations/TestMigration
- name: Find file changes
uses: tj-actions/verify-changed-files@v20
uses: tj-actions/verify-changed-files@v19
id: verify-changed-files
with:
files: |
@@ -321,12 +332,12 @@ jobs:
exit 1
- name: Run SQL generation
run: npm run sync:sql
run: npm run sql:generate
env:
DB_URL: postgres://postgres:postgres@localhost:5432/immich
- name: Find file changes
uses: tj-actions/verify-changed-files@v20
uses: tj-actions/verify-changed-files@v19
id: verify-changed-sql-files
with:
files: |

5
.gitignore vendored
View File

@@ -14,10 +14,7 @@ mobile/gradle.properties
mobile/openapi/pubspec.lock
mobile/*.jks
mobile/libisar.dylib
mobile/openapi/test
mobile/openapi/doc
mobile/openapi/.openapi-generator/FILES
open-api/typescript-sdk/build
mobile/android/fastlane/report.xml
mobile/ios/fastlane/report.xml
mobile/ios/fastlane/report.xml

2
.gitmodules vendored
View File

@@ -2,5 +2,5 @@
path = mobile/.isar
url = https://github.com/isar/isar
[submodule "server/test/assets"]
path = e2e/test-assets
path = server/test/assets
url = https://github.com/immich-app/test-assets

14
.vscode/settings.json vendored
View File

@@ -1,16 +1,6 @@
{
"editor.formatOnSave": true,
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.tabSize": 2,
"editor.formatOnSave": true
},
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.tabSize": 2,
"editor.formatOnSave": true
},
"[css]": {
"[javascript][typescript][css]": {
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.tabSize": 2,
"editor.formatOnSave": true
@@ -41,4 +31,4 @@
"explorer.fileNesting.patterns": {
"*.ts": "${capture}.spec.ts,${capture}.mock.ts"
}
}
}

View File

@@ -37,7 +37,7 @@ open-api-typescript:
cd ./open-api && bash ./bin/generate-open-api.sh typescript
sql:
npm --prefix server run sync:sql
npm --prefix server run sql:generate
attach-server:
docker exec -it docker_immich-server_1 sh

View File

@@ -18,7 +18,6 @@
</a>
<br/>
<p align="center">
<a href="readme_i18n/README_ca_ES.md">Català</a>
<a href="readme_i18n/README_es_ES.md">Español</a>
<a href="readme_i18n/README_fr_FR.md">Français</a>
@@ -31,7 +30,6 @@
<a href="readme_i18n/README_zh_CN.md">中文</a>
<a href="readme_i18n/README_ru_RU.md">Русский</a>
<a href="readme_i18n/README_pt_BR.md">Português Brasileiro</a>
<a href="readme_i18n/README_ar_JO.md">العربية</a>
</p>
## Disclaimer
@@ -72,7 +70,6 @@ Spec: Free-tier Oracle VM - Amsterdam - 2.4Ghz quad-core ARM64 CPU, 24GB RAM
```
## Activities
![Activities](https://repobeats.axiom.co/api/embed/9e86d9dc3ddd137161f2f6d2e758d7863b1789cb.svg "Repobeats analytics image")
## Features

View File

@@ -1 +1 @@
20.14
v20.12

View File

@@ -1,4 +1,4 @@
FROM node:20-alpine3.19@sha256:696ae41fb5880949a15ade7879a2deae93b3f0723f757bdb5b8a9e4a744ce27f as core
FROM node:20-alpine3.19@sha256:7a91aa397f2e2dfbfcdad2e2d72599f374e0b0172be1d86eeb73f1d33f36a4b2 as core
WORKDIR /usr/src/open-api/typescript-sdk
COPY open-api/typescript-sdk/package*.json open-api/typescript-sdk/tsconfig*.json ./

366
cli/package-lock.json generated
View File

@@ -31,7 +31,7 @@
"eslint": "^8.56.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-unicorn": "^53.0.0",
"eslint-plugin-unicorn": "^52.0.0",
"mock-fs": "^5.2.0",
"prettier": "^3.2.5",
"prettier-plugin-organize-imports": "^3.2.4",
@@ -47,14 +47,14 @@
},
"../open-api/typescript-sdk": {
"name": "@immich/sdk",
"version": "1.105.1",
"version": "1.103.1",
"dev": true,
"license": "GNU Affero General Public License version 3",
"dependencies": {
"@oazapfts/runtime": "^1.0.2"
"@oazapfts/runtime": "^1.0.3"
},
"devDependencies": {
"@types/node": "^20.12.13",
"@types/node": "^20.11.0",
"typescript": "^5.3.3"
}
},
@@ -174,9 +174,9 @@
}
},
"node_modules/@babel/helper-validator-identifier": {
"version": "7.24.5",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.5.tgz",
"integrity": "sha512-3q93SSKX2TWCG30M2G2kwaKeTYgEUp5Snjuj8qm729SObL6nbtUldAi37qbxkD5gg3xnBio+f9nqpSepGZMvxA==",
"version": "7.22.20",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz",
"integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==",
"dev": true,
"engines": {
"node": ">=6.9.0"
@@ -1113,6 +1113,12 @@
"integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==",
"dev": true
},
"node_modules/@types/json-schema": {
"version": "7.0.15",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
"dev": true
},
"node_modules/@types/lodash": {
"version": "4.17.0",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.0.tgz",
@@ -1138,11 +1144,10 @@
}
},
"node_modules/@types/node": {
"version": "20.12.13",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.13.tgz",
"integrity": "sha512-gBGeanV41c1L171rR7wjbMiEpEI/l5XFQdLLfhr/REwpgDy/4U8y89+i8kRiLzDyZdOkXh+cRaTetUnCYutoXA==",
"version": "20.12.7",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.7.tgz",
"integrity": "sha512-wq0cICSkRLVaf3UGLMGItu/PtdY7oaXaI/RVU+xliKVOtRna3PRY57ZDfztpDL0n11vfymMUnXv8QwYCO7L1wg==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~5.26.4"
}
@@ -1153,21 +1158,28 @@
"integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==",
"dev": true
},
"node_modules/@types/semver": {
"version": "7.5.8",
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz",
"integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==",
"dev": true
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "7.11.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.11.0.tgz",
"integrity": "sha512-P+qEahbgeHW4JQ/87FuItjBj8O3MYv5gELDzr8QaQ7fsll1gSMTYb6j87MYyxwf3DtD7uGFB9ShwgmCJB5KmaQ==",
"version": "7.7.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.7.1.tgz",
"integrity": "sha512-KwfdWXJBOviaBVhxO3p5TJiLpNuh2iyXyjmWN0f1nU87pwyvfS0EmjC6ukQVYVFJd/K1+0NWGPDXiyEyQorn0Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/regexpp": "^4.10.0",
"@typescript-eslint/scope-manager": "7.11.0",
"@typescript-eslint/type-utils": "7.11.0",
"@typescript-eslint/utils": "7.11.0",
"@typescript-eslint/visitor-keys": "7.11.0",
"@typescript-eslint/scope-manager": "7.7.1",
"@typescript-eslint/type-utils": "7.7.1",
"@typescript-eslint/utils": "7.7.1",
"@typescript-eslint/visitor-keys": "7.7.1",
"debug": "^4.3.4",
"graphemer": "^1.4.0",
"ignore": "^5.3.1",
"natural-compare": "^1.4.0",
"semver": "^7.6.0",
"ts-api-utils": "^1.3.0"
},
"engines": {
@@ -1188,16 +1200,15 @@
}
},
"node_modules/@typescript-eslint/parser": {
"version": "7.11.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.11.0.tgz",
"integrity": "sha512-yimw99teuaXVWsBcPO1Ais02kwJ1jmNA1KxE7ng0aT7ndr1pT1wqj0OJnsYVGKKlc4QJai86l/025L6z8CljOg==",
"version": "7.7.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.7.1.tgz",
"integrity": "sha512-vmPzBOOtz48F6JAGVS/kZYk4EkXao6iGrD838sp1w3NQQC0W8ry/q641KU4PrG7AKNAf56NOcR8GOpH8l9FPCw==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"@typescript-eslint/scope-manager": "7.11.0",
"@typescript-eslint/types": "7.11.0",
"@typescript-eslint/typescript-estree": "7.11.0",
"@typescript-eslint/visitor-keys": "7.11.0",
"@typescript-eslint/scope-manager": "7.7.1",
"@typescript-eslint/types": "7.7.1",
"@typescript-eslint/typescript-estree": "7.7.1",
"@typescript-eslint/visitor-keys": "7.7.1",
"debug": "^4.3.4"
},
"engines": {
@@ -1217,14 +1228,13 @@
}
},
"node_modules/@typescript-eslint/scope-manager": {
"version": "7.11.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.11.0.tgz",
"integrity": "sha512-27tGdVEiutD4POirLZX4YzT180vevUURJl4wJGmm6TrQoiYwuxTIY98PBp6L2oN+JQxzE0URvYlzJaBHIekXAw==",
"version": "7.7.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.7.1.tgz",
"integrity": "sha512-PytBif2SF+9SpEUKynYn5g1RHFddJUcyynGpztX3l/ik7KmZEv19WCMhUBkHXPU9es/VWGD3/zg3wg90+Dh2rA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "7.11.0",
"@typescript-eslint/visitor-keys": "7.11.0"
"@typescript-eslint/types": "7.7.1",
"@typescript-eslint/visitor-keys": "7.7.1"
},
"engines": {
"node": "^18.18.0 || >=20.0.0"
@@ -1235,14 +1245,13 @@
}
},
"node_modules/@typescript-eslint/type-utils": {
"version": "7.11.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.11.0.tgz",
"integrity": "sha512-WmppUEgYy+y1NTseNMJ6mCFxt03/7jTOy08bcg7bxJJdsM4nuhnchyBbE8vryveaJUf62noH7LodPSo5Z0WUCg==",
"version": "7.7.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.7.1.tgz",
"integrity": "sha512-ZksJLW3WF7o75zaBPScdW1Gbkwhd/lyeXGf1kQCxJaOeITscoSl0MjynVvCzuV5boUz/3fOI06Lz8La55mu29Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/typescript-estree": "7.11.0",
"@typescript-eslint/utils": "7.11.0",
"@typescript-eslint/typescript-estree": "7.7.1",
"@typescript-eslint/utils": "7.7.1",
"debug": "^4.3.4",
"ts-api-utils": "^1.3.0"
},
@@ -1263,11 +1272,10 @@
}
},
"node_modules/@typescript-eslint/types": {
"version": "7.11.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.11.0.tgz",
"integrity": "sha512-MPEsDRZTyCiXkD4vd3zywDCifi7tatc4K37KqTprCvaXptP7Xlpdw0NR2hRJTetG5TxbWDB79Ys4kLmHliEo/w==",
"version": "7.7.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.7.1.tgz",
"integrity": "sha512-AmPmnGW1ZLTpWa+/2omPrPfR7BcbUU4oha5VIbSbS1a1Tv966bklvLNXxp3mrbc+P2j4MNOTfDffNsk4o0c6/w==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^18.18.0 || >=20.0.0"
},
@@ -1277,14 +1285,13 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
"version": "7.11.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.11.0.tgz",
"integrity": "sha512-cxkhZ2C/iyi3/6U9EPc5y+a6csqHItndvN/CzbNXTNrsC3/ASoYQZEt9uMaEp+xFNjasqQyszp5TumAVKKvJeQ==",
"version": "7.7.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.7.1.tgz",
"integrity": "sha512-CXe0JHCXru8Fa36dteXqmH2YxngKJjkQLjxzoj6LYwzZ7qZvgsLSc+eqItCrqIop8Vl2UKoAi0StVWu97FQZIQ==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"@typescript-eslint/types": "7.11.0",
"@typescript-eslint/visitor-keys": "7.11.0",
"@typescript-eslint/types": "7.7.1",
"@typescript-eslint/visitor-keys": "7.7.1",
"debug": "^4.3.4",
"globby": "^11.1.0",
"is-glob": "^4.0.3",
@@ -1306,16 +1313,18 @@
}
},
"node_modules/@typescript-eslint/utils": {
"version": "7.11.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.11.0.tgz",
"integrity": "sha512-xlAWwPleNRHwF37AhrZurOxA1wyXowW4PqVXZVUNCLjB48CqdPJoJWkrpH2nij9Q3Lb7rtWindtoXwxjxlKKCA==",
"version": "7.7.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.7.1.tgz",
"integrity": "sha512-QUvBxPEaBXf41ZBbaidKICgVL8Hin0p6prQDu6bbetWo39BKbWJxRsErOzMNT1rXvTll+J7ChrbmMCXM9rsvOQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.4.0",
"@typescript-eslint/scope-manager": "7.11.0",
"@typescript-eslint/types": "7.11.0",
"@typescript-eslint/typescript-estree": "7.11.0"
"@types/json-schema": "^7.0.15",
"@types/semver": "^7.5.8",
"@typescript-eslint/scope-manager": "7.7.1",
"@typescript-eslint/types": "7.7.1",
"@typescript-eslint/typescript-estree": "7.7.1",
"semver": "^7.6.0"
},
"engines": {
"node": "^18.18.0 || >=20.0.0"
@@ -1329,13 +1338,12 @@
}
},
"node_modules/@typescript-eslint/visitor-keys": {
"version": "7.11.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.11.0.tgz",
"integrity": "sha512-7syYk4MzjxTEk0g/w3iqtgxnFQspDJfn6QKD36xMuuhTzjcxY7F8EmBLnALjVyaOF1/bVocu3bS/2/F7rXrveQ==",
"version": "7.7.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.7.1.tgz",
"integrity": "sha512-gBL3Eq25uADw1LQ9kVpf3hRM+DWzs0uZknHYK3hq4jcTPqVCClHGDnB6UUUV2SFeBeA4KWHWbbLqmbGcZ4FYbw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "7.11.0",
"@typescript-eslint/types": "7.7.1",
"eslint-visitor-keys": "^3.4.3"
},
"engines": {
@@ -1353,9 +1361,9 @@
"dev": true
},
"node_modules/@vitest/coverage-v8": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-1.6.0.tgz",
"integrity": "sha512-KvapcbMY/8GYIG0rlwwOKCVNRc0OL20rrhFkg/CHNzncV03TE2XWvO5w9uZYoxNiMEBacAJt3unSOiZ7svePew==",
"version": "1.5.2",
"resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-1.5.2.tgz",
"integrity": "sha512-QJqxRnbCwNtbbegK9E93rBmhN3dbfG1bC/o52Bqr0zGCYhQzwgwvrJBG7Q8vw3zilX6Ryy6oa/mkZku2lLJx1Q==",
"dev": true,
"dependencies": {
"@ampproject/remapping": "^2.2.1",
@@ -1376,17 +1384,17 @@
"url": "https://opencollective.com/vitest"
},
"peerDependencies": {
"vitest": "1.6.0"
"vitest": "1.5.2"
}
},
"node_modules/@vitest/expect": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.6.0.tgz",
"integrity": "sha512-ixEvFVQjycy/oNgHjqsL6AZCDduC+tflRluaHIzKIsdbzkLn2U/iBnVeJwB6HsIjQBdfMR8Z0tRxKUsvFJEeWQ==",
"version": "1.5.2",
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.5.2.tgz",
"integrity": "sha512-rf7MTD1WCoDlN3FfYJ9Llfp0PbdtOMZ3FIF0AVkDnKbp3oiMW1c8AmvRZBcqbAhDUAvF52e9zx4WQM1r3oraVA==",
"dev": true,
"dependencies": {
"@vitest/spy": "1.6.0",
"@vitest/utils": "1.6.0",
"@vitest/spy": "1.5.2",
"@vitest/utils": "1.5.2",
"chai": "^4.3.10"
},
"funding": {
@@ -1394,12 +1402,12 @@
}
},
"node_modules/@vitest/runner": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.6.0.tgz",
"integrity": "sha512-P4xgwPjwesuBiHisAVz/LSSZtDjOTPYZVmNAnpHHSR6ONrf8eCJOFRvUwdHn30F5M1fxhqtl7QZQUk2dprIXAg==",
"version": "1.5.2",
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.5.2.tgz",
"integrity": "sha512-7IJ7sJhMZrqx7HIEpv3WrMYcq8ZNz9L6alo81Y6f8hV5mIE6yVZsFoivLZmr0D777klm1ReqonE9LyChdcmw6g==",
"dev": true,
"dependencies": {
"@vitest/utils": "1.6.0",
"@vitest/utils": "1.5.2",
"p-limit": "^5.0.0",
"pathe": "^1.1.1"
},
@@ -1435,9 +1443,9 @@
}
},
"node_modules/@vitest/snapshot": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.6.0.tgz",
"integrity": "sha512-+Hx43f8Chus+DCmygqqfetcAZrDJwvTj0ymqjQq4CvmpKFSTVteEOBzCusu1x2tt4OJcvBflyHUE0DZSLgEMtQ==",
"version": "1.5.2",
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.5.2.tgz",
"integrity": "sha512-CTEp/lTYos8fuCc9+Z55Ga5NVPKUgExritjF5VY7heRFUfheoAqBneUlvXSUJHUZPjnPmyZA96yLRJDP1QATFQ==",
"dev": true,
"dependencies": {
"magic-string": "^0.30.5",
@@ -1449,9 +1457,9 @@
}
},
"node_modules/@vitest/spy": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.6.0.tgz",
"integrity": "sha512-leUTap6B/cqi/bQkXUu6bQV5TZPx7pmMBKBQiI0rJA8c3pB56ZsaTbREnF7CJfmvAS4V2cXIBAh/3rVwrrCYgw==",
"version": "1.5.2",
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.5.2.tgz",
"integrity": "sha512-xCcPvI8JpCtgikT9nLpHPL1/81AYqZy1GCy4+MCHBE7xi8jgsYkULpW5hrx5PGLgOQjUpb6fd15lqcriJ40tfQ==",
"dev": true,
"dependencies": {
"tinyspy": "^2.2.0"
@@ -1461,9 +1469,9 @@
}
},
"node_modules/@vitest/utils": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.6.0.tgz",
"integrity": "sha512-21cPiuGMoMZwiOHa2i4LXkMkMkCGzA+MVFV70jRwHo95dL4x/ts5GZhML1QWuy7yfp3WzK3lRvZi3JnXTYqrBw==",
"version": "1.5.2",
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.5.2.tgz",
"integrity": "sha512-sWOmyofuXLJ85VvXNsroZur7mOJGiQeM0JN3/0D1uU8U9bGFM69X1iqHaRXl6R8BwaLY6yPCogP257zxTzkUdA==",
"dev": true,
"dependencies": {
"diff-sequences": "^29.6.3",
@@ -1556,7 +1564,6 @@
"resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz",
"integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
@@ -1581,7 +1588,6 @@
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0"
}
@@ -1801,11 +1807,10 @@
"dev": true
},
"node_modules/commander": {
"version": "12.1.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz",
"integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==",
"version": "12.0.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-12.0.0.tgz",
"integrity": "sha512-MwVNWlYjDTtOjX5PiD7o5pK0UrFU/OYgcJfjjK4RaHZETNtjJqrZa9Y9ds88+A+f+d5lv+561eZ+yCKoS3gbAA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
}
@@ -1817,12 +1822,12 @@
"dev": true
},
"node_modules/core-js-compat": {
"version": "3.37.1",
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.37.1.tgz",
"integrity": "sha512-9TNiImhKvQqSUkOvk/mMRZzOANTiEVC7WaBNhHcKM7x+/5E1l5NvsysR19zuDQScE8k+kfQXWRN3AtS/eOSHpg==",
"version": "3.36.0",
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.36.0.tgz",
"integrity": "sha512-iV9Pd/PsgjNWBXeq8XRtWVSgz2tKAfhfvBs7qxYty+RlRd+OCksaWmOnc4JKrTc1cToXL1N0s3l/vwlxPtdElw==",
"dev": true,
"dependencies": {
"browserslist": "^4.23.0"
"browserslist": "^4.22.3"
},
"funding": {
"type": "opencollective",
@@ -1892,7 +1897,6 @@
"resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
"integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==",
"dev": true,
"license": "MIT",
"dependencies": {
"path-type": "^4.0.0"
},
@@ -2090,17 +2094,17 @@
}
},
"node_modules/eslint-plugin-unicorn": {
"version": "53.0.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-53.0.0.tgz",
"integrity": "sha512-kuTcNo9IwwUCfyHGwQFOK/HjJAYzbODHN3wP0PgqbW+jbXqpNWxNVpVhj2tO9SixBwuAdmal8rVcWKBxwFnGuw==",
"version": "52.0.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-52.0.0.tgz",
"integrity": "sha512-1Yzm7/m+0R4djH0tjDjfVei/ju2w3AzUGjG6q8JnuNIL5xIwsflyCooW5sfBvQp2pMYQFSWWCFONsjCax1EHng==",
"dev": true,
"dependencies": {
"@babel/helper-validator-identifier": "^7.24.5",
"@babel/helper-validator-identifier": "^7.22.20",
"@eslint-community/eslint-utils": "^4.4.0",
"@eslint/eslintrc": "^3.0.2",
"@eslint/eslintrc": "^2.1.4",
"ci-info": "^4.0.0",
"clean-regexp": "^1.0.0",
"core-js-compat": "^3.37.0",
"core-js-compat": "^3.34.0",
"esquery": "^1.5.0",
"indent-string": "^4.0.0",
"is-builtin-module": "^3.2.1",
@@ -2109,11 +2113,11 @@
"read-pkg-up": "^7.0.1",
"regexp-tree": "^0.1.27",
"regjsparser": "^0.10.0",
"semver": "^7.6.1",
"semver": "^7.5.4",
"strip-indent": "^3.0.0"
},
"engines": {
"node": ">=18.18"
"node": ">=16"
},
"funding": {
"url": "https://github.com/sindresorhus/eslint-plugin-unicorn?sponsor=1"
@@ -2122,92 +2126,6 @@
"eslint": ">=8.56.0"
}
},
"node_modules/eslint-plugin-unicorn/node_modules/@eslint/eslintrc": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.0.2.tgz",
"integrity": "sha512-wV19ZEGEMAC1eHgrS7UQPqsdEiCIbTKTasEfcXAigzoXICcqZSjBZEHlZwNVvKg6UBCjSlos84XiLqsRJnIcIg==",
"dev": true,
"dependencies": {
"ajv": "^6.12.4",
"debug": "^4.3.2",
"espree": "^10.0.1",
"globals": "^14.0.0",
"ignore": "^5.2.0",
"import-fresh": "^3.2.1",
"js-yaml": "^4.1.0",
"minimatch": "^3.1.2",
"strip-json-comments": "^3.1.1"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"url": "https://opencollective.com/eslint"
}
},
"node_modules/eslint-plugin-unicorn/node_modules/brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"dev": true,
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"node_modules/eslint-plugin-unicorn/node_modules/eslint-visitor-keys": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz",
"integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==",
"dev": true,
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"url": "https://opencollective.com/eslint"
}
},
"node_modules/eslint-plugin-unicorn/node_modules/espree": {
"version": "10.0.1",
"resolved": "https://registry.npmjs.org/espree/-/espree-10.0.1.tgz",
"integrity": "sha512-MWkrWZbJsL2UwnjxTX3gG8FneachS/Mwg7tdGXce011sJd5b0JG54vat5KHnfSBODZ3Wvzd2WnjxyzsRoVv+ww==",
"dev": true,
"dependencies": {
"acorn": "^8.11.3",
"acorn-jsx": "^5.3.2",
"eslint-visitor-keys": "^4.0.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"url": "https://opencollective.com/eslint"
}
},
"node_modules/eslint-plugin-unicorn/node_modules/globals": {
"version": "14.0.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz",
"integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==",
"dev": true,
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/eslint-plugin-unicorn/node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"dev": true,
"dependencies": {
"brace-expansion": "^1.1.7"
},
"engines": {
"node": "*"
}
},
"node_modules/eslint-scope": {
"version": "7.2.2",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz",
@@ -2548,7 +2466,6 @@
"resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz",
"integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==",
"dev": true,
"license": "MIT",
"dependencies": {
"array-union": "^2.1.0",
"dir-glob": "^3.0.1",
@@ -3052,7 +2969,6 @@
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz",
"integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^2.0.1"
},
@@ -3316,7 +3232,6 @@
"resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
"integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
@@ -3796,10 +3711,13 @@
}
},
"node_modules/semver": {
"version": "7.6.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz",
"integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==",
"version": "7.6.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz",
"integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==",
"dev": true,
"dependencies": {
"lru-cache": "^6.0.0"
},
"bin": {
"semver": "bin/semver.js"
},
@@ -3807,6 +3725,18 @@
"node": ">=10"
}
},
"node_modules/semver/node_modules/lru-cache": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
"dev": true,
"dependencies": {
"yallist": "^4.0.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@@ -3851,7 +3781,6 @@
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
"integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
@@ -4281,11 +4210,10 @@
}
},
"node_modules/vite": {
"version": "5.2.12",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.2.12.tgz",
"integrity": "sha512-/gC8GxzxMK5ntBwb48pR32GGhENnjtY30G4A0jemunsBkiEZFw60s8InGpN8gkhHEkjnRK1aSAxeQgwvFhUHAA==",
"version": "5.2.10",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.2.10.tgz",
"integrity": "sha512-PAzgUZbP7msvQvqdSD+ErD5qGnSFiGOoWmV5yAKUEI0kdhjbH6nMWVyZQC/hSc4aXwc0oJ9aEdIiF9Oje0JFCw==",
"dev": true,
"license": "MIT",
"dependencies": {
"esbuild": "^0.20.1",
"postcss": "^8.4.38",
@@ -4337,9 +4265,9 @@
}
},
"node_modules/vite-node": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.6.0.tgz",
"integrity": "sha512-de6HJgzC+TFzOu0NTC4RAIsyf/DY/ibWDYQUcuEA84EMHhcefTUGkjFHKKEJhQN4A+6I0u++kr3l36ZF2d7XRw==",
"version": "1.5.2",
"resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.5.2.tgz",
"integrity": "sha512-Y8p91kz9zU+bWtF7HGt6DVw2JbhyuB2RlZix3FPYAYmUyZ3n7iTp8eSyLyY6sxtPegvxQtmlTMhfPhUfCUF93A==",
"dev": true,
"dependencies": {
"cac": "^6.7.14",
@@ -4378,16 +4306,16 @@
}
},
"node_modules/vitest": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/vitest/-/vitest-1.6.0.tgz",
"integrity": "sha512-H5r/dN06swuFnzNFhq/dnz37bPXnq8xB2xB5JOVk8K09rUtoeNN+LHWkoQ0A/i3hvbUKKcCei9KpbxqHMLhLLA==",
"version": "1.5.2",
"resolved": "https://registry.npmjs.org/vitest/-/vitest-1.5.2.tgz",
"integrity": "sha512-l9gwIkq16ug3xY7BxHwcBQovLZG75zZL0PlsiYQbf76Rz6QGs54416UWMtC0jXeihvHvcHrf2ROEjkQRVpoZYw==",
"dev": true,
"dependencies": {
"@vitest/expect": "1.6.0",
"@vitest/runner": "1.6.0",
"@vitest/snapshot": "1.6.0",
"@vitest/spy": "1.6.0",
"@vitest/utils": "1.6.0",
"@vitest/expect": "1.5.2",
"@vitest/runner": "1.5.2",
"@vitest/snapshot": "1.5.2",
"@vitest/spy": "1.5.2",
"@vitest/utils": "1.5.2",
"acorn-walk": "^8.3.2",
"chai": "^4.3.10",
"debug": "^4.3.4",
@@ -4401,7 +4329,7 @@
"tinybench": "^2.5.1",
"tinypool": "^0.8.3",
"vite": "^5.0.0",
"vite-node": "1.6.0",
"vite-node": "1.5.2",
"why-is-node-running": "^2.2.2"
},
"bin": {
@@ -4416,8 +4344,8 @@
"peerDependencies": {
"@edge-runtime/vm": "*",
"@types/node": "^18.0.0 || >=20.0.0",
"@vitest/browser": "1.6.0",
"@vitest/ui": "1.6.0",
"@vitest/browser": "1.5.2",
"@vitest/ui": "1.5.2",
"happy-dom": "*",
"jsdom": "*"
},
@@ -4479,10 +4407,16 @@
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"dev": true
},
"node_modules/yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
"dev": true
},
"node_modules/yaml": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.2.tgz",
"integrity": "sha512-B3VqDZ+JAg1nZpaEmWtTXUlBneoGx6CPM9b0TENK6aoSu5t73dItudwdgmi6tHlIZZId4dZ9skcAQ2UbcyAeVA==",
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.1.tgz",
"integrity": "sha512-pIXzoImaqmfOrL7teGUBt/T7ZDnyeGBWyXQBvOVhLkWLN37GXv8NMLK406UY6dS51JfcQHsmcW5cJ441bHg6Lg==",
"dev": true,
"bin": {
"yaml": "bin.mjs"

View File

@@ -28,7 +28,7 @@
"eslint": "^8.56.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-unicorn": "^53.0.0",
"eslint-plugin-unicorn": "^52.0.0",
"mock-fs": "^5.2.0",
"prettier": "^3.2.5",
"prettier-plugin-organize-imports": "^3.2.4",
@@ -62,6 +62,6 @@
"lodash-es": "^4.17.21"
},
"volta": {
"node": "20.14.0"
"node": "20.12.2"
}
}

View File

@@ -1,8 +1,7 @@
import {
Action,
AssetBulkUploadCheckResult,
AssetMediaResponseDto,
AssetMediaStatus,
AssetFileUploadResponseDto,
addAssetsToAlbum,
checkBulkUpload,
createAlbum,
@@ -168,7 +167,7 @@ const uploadFiles = async (files: string[], { dryRun, concurrency }: UploadOptio
newAssets.push({ id: response.id, filepath });
if (response.status === AssetMediaStatus.Duplicate) {
if (response.duplicate) {
duplicateCount++;
duplicateSize += stats.size ?? 0;
} else {
@@ -193,7 +192,7 @@ const uploadFiles = async (files: string[], { dryRun, concurrency }: UploadOptio
return newAssets;
};
const uploadFile = async (input: string, stats: Stats): Promise<AssetMediaResponseDto> => {
const uploadFile = async (input: string, stats: Stats): Promise<AssetFileUploadResponseDto> => {
const { baseUrl, headers } = defaults;
const assetPath = path.parse(input);
@@ -226,7 +225,7 @@ const uploadFile = async (input: string, stats: Stats): Promise<AssetMediaRespon
formData.append('sidecarData', sidecarData);
}
const response = await fetch(`${baseUrl}/assets`, {
const response = await fetch(`${baseUrl}/asset/upload`, {
method: 'post',
redirect: 'error',
headers: headers as Record<string, string>,

View File

@@ -1,4 +1,4 @@
import { getMyUser } from '@immich/sdk';
import { getMyUserInfo } from '@immich/sdk';
import { existsSync } from 'node:fs';
import { mkdir, unlink } from 'node:fs/promises';
import { BaseOptions, connect, getAuthFilePath, logError, withError, writeAuthFile } from 'src/utils';
@@ -10,13 +10,13 @@ export const login = async (url: string, key: string, options: BaseOptions) => {
await connect(url, key);
const [error, user] = await withError(getMyUser());
const [error, userInfo] = await withError(getMyUserInfo());
if (error) {
logError(error, 'Failed to load user info');
process.exit(1);
}
console.log(`Logged in as ${user.email}`);
console.log(`Logged in as ${userInfo.email}`);
if (!existsSync(configDir)) {
// Create config folder if it doesn't exist

View File

@@ -1,4 +1,4 @@
import { getAssetStatistics, getMyUser, getServerVersion, getSupportedMediaTypes } from '@immich/sdk';
import { getAssetStatistics, getMyUserInfo, getServerVersion, getSupportedMediaTypes } from '@immich/sdk';
import { BaseOptions, authenticate } from 'src/utils';
export const serverInfo = async (options: BaseOptions) => {
@@ -8,7 +8,7 @@ export const serverInfo = async (options: BaseOptions) => {
getServerVersion(),
getSupportedMediaTypes(),
getAssetStatistics({}),
getMyUser(),
getMyUserInfo(),
]);
console.log(`Server Info (via ${userInfo.email})`);

View File

@@ -1,4 +1,4 @@
import { getMyUser, init, isHttpError } from '@immich/sdk';
import { defaults, getMyUserInfo, isHttpError } from '@immich/sdk';
import { glob } from 'fast-glob';
import { createHash } from 'node:crypto';
import { createReadStream } from 'node:fs';
@@ -46,9 +46,10 @@ export const connect = async (url: string, key: string) => {
// noop
}
init({ baseUrl: url, apiKey: key });
defaults.baseUrl = url;
defaults.headers = { 'x-api-key': key };
const [error] = await withError(getMyUser());
const [error] = await withError(getMyUserInfo());
if (isHttpError(error)) {
logError(error, 'Failed to connect to server');
process.exit(1);

37
cli/src/version.ts Normal file
View File

@@ -0,0 +1,37 @@
import { version } from '../package.json';
export interface ICliVersion {
major: number;
minor: number;
patch: number;
}
export class CliVersion implements ICliVersion {
constructor(
public readonly major: number,
public readonly minor: number,
public readonly patch: number,
) {}
toString() {
return `${this.major}.${this.minor}.${this.patch}`;
}
toJSON() {
const { major, minor, patch } = this;
return { major, minor, patch };
}
static fromString(version: string): CliVersion {
const regex = /v?(?<major>\d+)\.(?<minor>\d+)\.(?<patch>\d+)/i;
const matchResult = version.match(regex);
if (matchResult) {
const [, major, minor, patch] = matchResult.map(Number);
return new CliVersion(major, minor, patch);
} else {
throw new Error(`Invalid version format: ${version}`);
}
}
}
export const cliVersion = CliVersion.fromString(version);

38
deployment/.gitignore vendored
View File

@@ -1,38 +0,0 @@
# OpenTofu
# Local .terraform directories
**/.terraform/*
# .tfstate files
*.tfstate
*.tfstate.*
# Crash log files
crash.log
crash.*.log
# Ignore override files as they are usually used to override resources locally and so
# are not checked in
override.tf
override.tf.json
*_override.tf
*_override.tf.json
# Include override files you do wish to add to version control using negated pattern
# !example_override.tf
# Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan
# example: *tfplan*
# Ignore CLI configuration files
.terraformrc
terraform.rc
# Terragrunt
# terragrunt cache directories
**/.terragrunt-cache/*
# Terragrunt debug output file (when using `--terragrunt-debug` option)
# See: https://terragrunt.gruntwork.io/docs/reference/cli-options/#terragrunt-debug
terragrunt-debug.tfvars.json

View File

@@ -1,38 +0,0 @@
# This file is maintained automatically by "tofu init".
# Manual edits may be lost in future updates.
provider "registry.opentofu.org/cloudflare/cloudflare" {
version = "4.34.0"
constraints = "4.34.0"
hashes = [
"h1:+W0+Xe1AUh7yvHjDbgR9T7CY1UbBC3Y6U7Eo+ucLnJM=",
"h1:2+1lKObDDdFZRluvROF3RKtXD66CFT3PfnHOvR6CmfA=",
"h1:7vluN2wmw8D9nI11YwTgoGv3hGDXlkt8xqQ4L/JABeQ=",
"h1:B0Urm8ZKTJ8cXzSCtEpJ+o+LsD8MXaD6LU59qVbh50Q=",
"h1:FpGLCm5oF12FaRti3E4iQJlkVbdCC7toyGVuH8og7KY=",
"h1:FunTmrCMDy+rom7YskY0WiL5/Y164zFrrD9xnBxU5NY=",
"h1:GrxZhEb+5HzmHF/BvZBdGKBJy6Wyjme0+ABVDz/63to=",
"h1:J36dda2K42/oTfHuZ4jKkW5+nI6BTWFRUvo60P17NJg=",
"h1:Kq0Wyn+j6zoQeghMYixbnfnyP9ZSIEJbOCzMbaCiAQQ=",
"h1:TKxunXCiS/z105sN/kBNFwU6tIKD67JKJ3ZKjwzoCuI=",
"h1:TR0URKFQxsRO5/v7bKm5hkD/CTTjsG7aVGllL/Mf25c=",
"h1:V+3Qs0Reb6r+8p4XjE5ZFDWYrOIN0x5SwORz4wvHOJ4=",
"h1:mZB3Ui7V/lPQMQK53eBOjIHcrul74252dT06Kgn3J+s=",
"h1:wJwZrIXxoki8omXLJ7XA7B1KaSrtcLMJp090fRtFRAc=",
"zh:02aa46743c1585ada8faa7db23af68ea614053a506f88f05d1090ff5e0e68076",
"zh:1e1a545e83e6457a0e15357b23139bc288fb4fbd5e9a5ddfedc95a6a0216b08c",
"zh:29eef2621e0b1501f620e615bf73b1b90d5417d745e38af63634bc03250faf87",
"zh:3c20989d7e1e141882e6091384bf85fdc83f70f3d29e3e047c493a07de992095",
"zh:3d39619379ba29c7ffb15196f0ea72a04c84cfcdf4b39ac42ac4cf4c19f3eae2",
"zh:805f4a2774e9279c590b8214aabe6df9dcc22bb995df2530513f2f78c647ce75",
"zh:890df766e9b839623b1f0437355032a3c006226a6c200cd911e15ee1a9014e9f",
"zh:8af716f8655a57aa986861a8a7fa1d724594a284bd77c870eaea4db5f8b9732d",
"zh:a3d13c93b4e6ee6004782debaa9a17f990f2fe8ec8ba545c232818bb6064aba9",
"zh:bfa136acf82d3719473c0064446cc16d1b0303d98b06f55f503b7abeebceadb1",
"zh:ca6cf9254ae5436f2efbc01a0e3f7e4aa3c08b45182037b3eb3eb9539b2f7aec",
"zh:cba32d5de02674004e0a5955bd5222016d9991ca0553d4bd3bea517cd9def6ab",
"zh:d22c8cd527c6d0e84567f57be5911792e2fcd5969e3bba3747489f18bb16705b",
"zh:e4eeede9b3e72cdadd6cc252d4cbcf41baee6ecfd12bacd927e2dcbe733ab210",
"zh:facdaa787a69f86203cd3cc6922baea0b4a18bd9c36b0a8162e2e88ef6c90655",
]
}

View File

@@ -1,11 +0,0 @@
terraform {
backend "pg" {}
required_version = "~> 1.7"
required_providers {
cloudflare = {
source = "cloudflare/cloudflare"
version = "4.34.0"
}
}
}

View File

@@ -1,14 +0,0 @@
resource "cloudflare_pages_domain" "immich_app_release_domain" {
account_id = var.cloudflare_account_id
project_name = data.terraform_remote_state.cloudflare_account.outputs.immich_app_archive_pages_project_name
domain = "immich.app"
}
resource "cloudflare_record" "immich_app_release_domain" {
name = "immich.app"
proxied = true
ttl = 1
type = "CNAME"
value = data.terraform_remote_state.cloudflare_immich_app_docs.outputs.immich_app_branch_pages_hostname
zone_id = data.terraform_remote_state.cloudflare_account.outputs.immich_app_zone_id
}

View File

@@ -1,3 +0,0 @@
provider "cloudflare" {
api_token = data.terraform_remote_state.api_keys_state.outputs.terraform_key_cloudflare_docs
}

View File

@@ -1,27 +0,0 @@
data "terraform_remote_state" "api_keys_state" {
backend = "pg"
config = {
conn_str = var.tf_state_postgres_conn_str
schema_name = "prod_cloudflare_api_keys"
}
}
data "terraform_remote_state" "cloudflare_account" {
backend = "pg"
config = {
conn_str = var.tf_state_postgres_conn_str
schema_name = "prod_cloudflare_account"
}
}
data "terraform_remote_state" "cloudflare_immich_app_docs" {
backend = "pg"
config = {
conn_str = var.tf_state_postgres_conn_str
schema_name = "prod_cloudflare_immich_app_docs_${var.prefix_name}"
}
}

View File

@@ -1,20 +0,0 @@
terraform {
source = "."
extra_arguments custom_vars {
commands = get_terraform_commands_that_need_vars()
}
}
include {
path = find_in_parent_folders("state.hcl")
}
remote_state {
backend = "pg"
config = {
conn_str = get_env("TF_STATE_POSTGRES_CONN_STR")
schema_name = "prod_cloudflare_immich_app_docs_release"
}
}

View File

@@ -1,4 +0,0 @@
variable "cloudflare_account_id" {}
variable "tf_state_postgres_conn_str" {}
variable "prefix_name" {}

View File

@@ -1,38 +0,0 @@
# This file is maintained automatically by "tofu init".
# Manual edits may be lost in future updates.
provider "registry.opentofu.org/cloudflare/cloudflare" {
version = "4.34.0"
constraints = "4.34.0"
hashes = [
"h1:+W0+Xe1AUh7yvHjDbgR9T7CY1UbBC3Y6U7Eo+ucLnJM=",
"h1:2+1lKObDDdFZRluvROF3RKtXD66CFT3PfnHOvR6CmfA=",
"h1:7vluN2wmw8D9nI11YwTgoGv3hGDXlkt8xqQ4L/JABeQ=",
"h1:B0Urm8ZKTJ8cXzSCtEpJ+o+LsD8MXaD6LU59qVbh50Q=",
"h1:FpGLCm5oF12FaRti3E4iQJlkVbdCC7toyGVuH8og7KY=",
"h1:FunTmrCMDy+rom7YskY0WiL5/Y164zFrrD9xnBxU5NY=",
"h1:GrxZhEb+5HzmHF/BvZBdGKBJy6Wyjme0+ABVDz/63to=",
"h1:J36dda2K42/oTfHuZ4jKkW5+nI6BTWFRUvo60P17NJg=",
"h1:Kq0Wyn+j6zoQeghMYixbnfnyP9ZSIEJbOCzMbaCiAQQ=",
"h1:TKxunXCiS/z105sN/kBNFwU6tIKD67JKJ3ZKjwzoCuI=",
"h1:TR0URKFQxsRO5/v7bKm5hkD/CTTjsG7aVGllL/Mf25c=",
"h1:V+3Qs0Reb6r+8p4XjE5ZFDWYrOIN0x5SwORz4wvHOJ4=",
"h1:mZB3Ui7V/lPQMQK53eBOjIHcrul74252dT06Kgn3J+s=",
"h1:wJwZrIXxoki8omXLJ7XA7B1KaSrtcLMJp090fRtFRAc=",
"zh:02aa46743c1585ada8faa7db23af68ea614053a506f88f05d1090ff5e0e68076",
"zh:1e1a545e83e6457a0e15357b23139bc288fb4fbd5e9a5ddfedc95a6a0216b08c",
"zh:29eef2621e0b1501f620e615bf73b1b90d5417d745e38af63634bc03250faf87",
"zh:3c20989d7e1e141882e6091384bf85fdc83f70f3d29e3e047c493a07de992095",
"zh:3d39619379ba29c7ffb15196f0ea72a04c84cfcdf4b39ac42ac4cf4c19f3eae2",
"zh:805f4a2774e9279c590b8214aabe6df9dcc22bb995df2530513f2f78c647ce75",
"zh:890df766e9b839623b1f0437355032a3c006226a6c200cd911e15ee1a9014e9f",
"zh:8af716f8655a57aa986861a8a7fa1d724594a284bd77c870eaea4db5f8b9732d",
"zh:a3d13c93b4e6ee6004782debaa9a17f990f2fe8ec8ba545c232818bb6064aba9",
"zh:bfa136acf82d3719473c0064446cc16d1b0303d98b06f55f503b7abeebceadb1",
"zh:ca6cf9254ae5436f2efbc01a0e3f7e4aa3c08b45182037b3eb3eb9539b2f7aec",
"zh:cba32d5de02674004e0a5955bd5222016d9991ca0553d4bd3bea517cd9def6ab",
"zh:d22c8cd527c6d0e84567f57be5911792e2fcd5969e3bba3747489f18bb16705b",
"zh:e4eeede9b3e72cdadd6cc252d4cbcf41baee6ecfd12bacd927e2dcbe733ab210",
"zh:facdaa787a69f86203cd3cc6922baea0b4a18bd9c36b0a8162e2e88ef6c90655",
]
}

View File

@@ -1,11 +0,0 @@
terraform {
backend "pg" {}
required_version = "~> 1.7"
required_providers {
cloudflare = {
source = "cloudflare/cloudflare"
version = "4.34.0"
}
}
}

View File

@@ -1,26 +0,0 @@
resource "cloudflare_pages_domain" "immich_app_branch_domain" {
account_id = var.cloudflare_account_id
project_name = local.is_release ? data.terraform_remote_state.cloudflare_account.outputs.immich_app_archive_pages_project_name : data.terraform_remote_state.cloudflare_account.outputs.immich_app_preview_pages_project_name
domain = "${var.prefix_name}.${local.deploy_domain_prefix}.immich.app"
}
resource "cloudflare_record" "immich_app_branch_subdomain" {
name = "${var.prefix_name}.${local.deploy_domain_prefix}.immich.app"
proxied = true
ttl = 1
type = "CNAME"
value = "${replace(var.prefix_name, "/\\/|\\./", "-")}.${local.is_release ? data.terraform_remote_state.cloudflare_account.outputs.immich_app_archive_pages_project_subdomain : data.terraform_remote_state.cloudflare_account.outputs.immich_app_preview_pages_project_subdomain}"
zone_id = data.terraform_remote_state.cloudflare_account.outputs.immich_app_zone_id
}
output "immich_app_branch_subdomain" {
value = cloudflare_record.immich_app_branch_subdomain.hostname
}
output "immich_app_branch_pages_hostname" {
value = cloudflare_record.immich_app_branch_subdomain.value
}
output "pages_project_name" {
value = cloudflare_pages_domain.immich_app_branch_domain.project_name
}

View File

@@ -1,7 +0,0 @@
locals {
domain_name = "immich.app"
preview_prefix = contains(["branch", "pr"], var.prefix_event_type) ? "preview" : ""
archive_prefix = contains(["release"], var.prefix_event_type) ? "archive" : ""
deploy_domain_prefix = coalesce(local.preview_prefix, local.archive_prefix)
is_release = contains(["release"], var.prefix_event_type)
}

View File

@@ -1,3 +0,0 @@
provider "cloudflare" {
api_token = data.terraform_remote_state.api_keys_state.outputs.terraform_key_cloudflare_docs
}

View File

@@ -1,17 +0,0 @@
data "terraform_remote_state" "api_keys_state" {
backend = "pg"
config = {
conn_str = var.tf_state_postgres_conn_str
schema_name = "prod_cloudflare_api_keys"
}
}
data "terraform_remote_state" "cloudflare_account" {
backend = "pg"
config = {
conn_str = var.tf_state_postgres_conn_str
schema_name = "prod_cloudflare_account"
}
}

View File

@@ -1,24 +0,0 @@
terraform {
source = "."
extra_arguments custom_vars {
commands = get_terraform_commands_that_need_vars()
}
}
include {
path = find_in_parent_folders("state.hcl")
}
locals {
prefix_name = get_env("TF_VAR_prefix_name")
}
remote_state {
backend = "pg"
config = {
conn_str = get_env("TF_STATE_POSTGRES_CONN_STR")
schema_name = "prod_cloudflare_immich_app_docs_${local.prefix_name}"
}
}

View File

@@ -1,5 +0,0 @@
variable "cloudflare_account_id" {}
variable "tf_state_postgres_conn_str" {}
variable "prefix_name" {}
variable "prefix_event_type" {}

View File

@@ -1,20 +0,0 @@
locals {
cloudflare_account_id = get_env("CLOUDFLARE_ACCOUNT_ID")
cloudflare_api_token = get_env("CLOUDFLARE_API_TOKEN")
tf_state_postgres_conn_str = get_env("TF_STATE_POSTGRES_CONN_STR")
}
remote_state {
backend = "pg"
config = {
conn_str = local.tf_state_postgres_conn_str
}
}
inputs = {
cloudflare_account_id = local.cloudflare_account_id
cloudflare_api_token = local.cloudflare_api_token
tf_state_postgres_conn_str = local.tf_state_postgres_conn_str
}

View File

@@ -4,32 +4,32 @@
name: immich-dev
x-server-build: &server-common
image: immich-server-dev:latest
build:
context: ../
dockerfile: server/Dockerfile
target: dev
restart: always
volumes:
- ../server:/usr/src/app
- ../open-api:/usr/src/open-api
- ${UPLOAD_LOCATION}/photos:/usr/src/app/upload
- ${UPLOAD_LOCATION}/photos/upload:/usr/src/app/upload/upload
- /usr/src/app/node_modules
- /etc/localtime:/etc/localtime:ro
env_file:
- .env
ulimits:
nofile:
soft: 1048576
hard: 1048576
services:
immich-server:
container_name: immich_server
command: ['/usr/src/app/bin/immich-dev']
image: immich-server-dev:latest
# extends:
# file: hwaccel.transcoding.yml
# service: cpu # set to one of [nvenc, quicksync, rkmpp, vaapi, vaapi-wsl] for accelerated transcoding
build:
context: ../
dockerfile: server/Dockerfile
target: dev
restart: always
volumes:
- ../server:/usr/src/app
- ../open-api:/usr/src/open-api
- ${UPLOAD_LOCATION}/photos:/usr/src/app/upload
- ${UPLOAD_LOCATION}/photos/upload:/usr/src/app/upload/upload
- /usr/src/app/node_modules
- /etc/localtime:/etc/localtime:ro
env_file:
- .env
ulimits:
nofile:
soft: 1048576
hard: 1048576
command: ['/usr/src/app/bin/immich-dev', 'immich']
<<: *server-common
ports:
- 3001:3001
- 9230:9230
@@ -37,6 +37,19 @@ services:
- redis
- database
immich-microservices:
container_name: immich_microservices
command: ['/usr/src/app/bin/immich-dev', 'microservices']
<<: *server-common
# extends:
# file: hwaccel.transcoding.yml
# service: cpu # set to one of [nvenc, quicksync, rkmpp, vaapi, vaapi-wsl] for accelerated transcoding
ports:
- 9231:9230
depends_on:
- database
- immich-server
immich-web:
container_name: immich_web
image: immich-web-dev:latest
@@ -84,9 +97,7 @@ services:
redis:
container_name: immich_redis
image: redis:6.2-alpine@sha256:e31ca60b18f7e9b78b573d156702471d4eda038803c0b8e6f01559f350031e93
healthcheck:
test: redis-cli ping || exit 1
image: redis:6.2-alpine@sha256:84882e87b54734154586e5f8abd4dce69fe7311315e2fc6d67c29614c8de2672
database:
container_name: immich_postgres
@@ -97,18 +108,11 @@ services:
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_USER: ${DB_USERNAME}
POSTGRES_DB: ${DB_DATABASE_NAME}
POSTGRES_INITDB_ARGS: '--data-checksums'
volumes:
- ${UPLOAD_LOCATION}/postgres:/var/lib/postgresql/data
ports:
- 5432:5432
healthcheck:
test: pg_isready --dbname='${DB_DATABASE_NAME}' || exit 1; Chksum="$$(psql --dbname='${DB_DATABASE_NAME}' --username='${DB_USERNAME}' --tuples-only --no-align --command='SELECT SUM(checksum_failures) FROM pg_stat_database')"; echo "checksum failure count is $$Chksum"; [ "$$Chksum" = '0' ] || exit 1
interval: 5m
start_interval: 30s
start_period: 5m
command: ["postgres", "-c" ,"shared_preload_libraries=vectors.so", "-c", 'search_path="$$user", public, vectors', "-c", "logging_collector=on", "-c", "max_wal_size=2GB", "-c", "shared_buffers=512MB", "-c", "wal_compression=on"]
# set IMMICH_METRICS=true in .env to enable metrics
# immich-prometheus:
# container_name: immich_prometheus

View File

@@ -1,26 +1,39 @@
name: immich-prod
x-server-build: &server-common
image: immich-server:latest
build:
context: ../
dockerfile: server/Dockerfile
volumes:
- ${UPLOAD_LOCATION}/photos:/usr/src/app/upload
- /etc/localtime:/etc/localtime:ro
env_file:
- .env
restart: always
services:
immich-server:
container_name: immich_server
image: immich-server:latest
# extends:
# file: hwaccel.transcoding.yml
# service: cpu # set to one of [nvenc, quicksync, rkmpp, vaapi, vaapi-wsl] for accelerated transcoding
build:
context: ../
dockerfile: server/Dockerfile
volumes:
- ${UPLOAD_LOCATION}/photos:/usr/src/app/upload
- /etc/localtime:/etc/localtime:ro
env_file:
- .env
command: ['start.sh', 'immich']
<<: *server-common
ports:
- 2283:3001
depends_on:
- redis
- database
restart: always
immich-microservices:
container_name: immich_microservices
command: ['start.sh', 'microservices']
<<: *server-common
# extends:
# file: hwaccel.transcoding.yml
# service: cpu # set to one of [nvenc, quicksync, rkmpp, vaapi, vaapi-wsl] for accelerated transcoding
depends_on:
- redis
- database
- immich-server
immich-machine-learning:
container_name: immich_machine_learning
@@ -41,9 +54,7 @@ services:
redis:
container_name: immich_redis
image: redis:6.2-alpine@sha256:e31ca60b18f7e9b78b573d156702471d4eda038803c0b8e6f01559f350031e93
healthcheck:
test: redis-cli ping || exit 1
image: redis:6.2-alpine@sha256:84882e87b54734154586e5f8abd4dce69fe7311315e2fc6d67c29614c8de2672
restart: always
database:
@@ -55,25 +66,17 @@ services:
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_USER: ${DB_USERNAME}
POSTGRES_DB: ${DB_DATABASE_NAME}
POSTGRES_INITDB_ARGS: '--data-checksums'
volumes:
- ${UPLOAD_LOCATION}/postgres:/var/lib/postgresql/data
ports:
- 5432:5432
healthcheck:
test: pg_isready --dbname='${DB_DATABASE_NAME}' || exit 1; Chksum="$$(psql --dbname='${DB_DATABASE_NAME}' --username='${DB_USERNAME}' --tuples-only --no-align --command='SELECT SUM(checksum_failures) FROM pg_stat_database')"; echo "checksum failure count is $$Chksum"; [ "$$Chksum" = '0' ] || exit 1
interval: 5m
start_interval: 30s
start_period: 5m
command: ["postgres", "-c" ,"shared_preload_libraries=vectors.so", "-c", 'search_path="$$user", public, vectors', "-c", "logging_collector=on", "-c", "max_wal_size=2GB", "-c", "shared_buffers=512MB", "-c", "wal_compression=on"]
restart: always
# set IMMICH_METRICS=true in .env to enable metrics
immich-prometheus:
container_name: immich_prometheus
ports:
- 9090:9090
image: prom/prometheus@sha256:5c435642ca4d8427ca26f4901c11114023004709037880cd7860d5b7176aa731
image: prom/prometheus@sha256:4f6c47e39a9064028766e8c95890ed15690c30f00c4ba14e7ce6ae1ded0295b1
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
- prometheus-data:/prometheus
@@ -85,7 +88,7 @@ services:
command: ['./run.sh', '-disable-reporting']
ports:
- 3000:3000
image: grafana/grafana:11.0.0-ubuntu@sha256:02e99d1ee0b52dc9d3000c7b5314e7a07e0dfd69cc49bb3f8ce323491ed3406b
image: grafana/grafana:10.4.2-ubuntu@sha256:4f55071b556fb03f12b41423c98a185ed6695ed9ff2558e35805f0dd765fd958
volumes:
- grafana-data:/var/lib/grafana

View File

@@ -12,9 +12,7 @@ services:
immich-server:
container_name: immich_server
image: ghcr.io/immich-app/immich-server:${IMMICH_VERSION:-release}
# extends:
# file: hwaccel.transcoding.yml
# service: cpu # set to one of [nvenc, quicksync, rkmpp, vaapi, vaapi-wsl] for accelerated transcoding
command: ['start.sh', 'immich']
volumes:
- ${UPLOAD_LOCATION}:/usr/src/app/upload
- /etc/localtime:/etc/localtime:ro
@@ -27,6 +25,23 @@ services:
- database
restart: always
immich-microservices:
container_name: immich_microservices
image: ghcr.io/immich-app/immich-server:${IMMICH_VERSION:-release}
# extends: # uncomment this section for hardware acceleration - see https://immich.app/docs/features/hardware-transcoding
# file: hwaccel.transcoding.yml
# service: cpu # set to one of [nvenc, quicksync, rkmpp, vaapi, vaapi-wsl] for accelerated transcoding
command: ['start.sh', 'microservices']
volumes:
- ${UPLOAD_LOCATION}:/usr/src/app/upload
- /etc/localtime:/etc/localtime:ro
env_file:
- .env
depends_on:
- redis
- database
restart: always
immich-machine-learning:
container_name: immich_machine_learning
# For hardware acceleration, add one of -[armnn, cuda, openvino] to the image tag.
@@ -43,27 +58,18 @@ services:
redis:
container_name: immich_redis
image: docker.io/redis:6.2-alpine@sha256:e31ca60b18f7e9b78b573d156702471d4eda038803c0b8e6f01559f350031e93
healthcheck:
test: redis-cli ping || exit 1
image: registry.hub.docker.com/library/redis:6.2-alpine@sha256:84882e87b54734154586e5f8abd4dce69fe7311315e2fc6d67c29614c8de2672
restart: always
database:
container_name: immich_postgres
image: docker.io/tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:90724186f0a3517cf6914295b5ab410db9ce23190a2d9d0b9dd6463e3fa298f0
image: registry.hub.docker.com/tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:90724186f0a3517cf6914295b5ab410db9ce23190a2d9d0b9dd6463e3fa298f0
environment:
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_USER: ${DB_USERNAME}
POSTGRES_DB: ${DB_DATABASE_NAME}
POSTGRES_INITDB_ARGS: '--data-checksums'
volumes:
- ${DB_DATA_LOCATION}:/var/lib/postgresql/data
healthcheck:
test: pg_isready --dbname='${DB_DATABASE_NAME}' || exit 1; Chksum="$$(psql --dbname='${DB_DATABASE_NAME}' --username='${DB_USERNAME}' --tuples-only --no-align --command='SELECT SUM(checksum_failures) FROM pg_stat_database')"; echo "checksum failure count is $$Chksum"; [ "$$Chksum" = '0' ] || exit 1
interval: 5m
start_interval: 30s
start_period: 5m
command: ["postgres", "-c" ,"shared_preload_libraries=vectors.so", "-c", 'search_path="$$user", public, vectors', "-c", "logging_collector=on", "-c", "max_wal_size=2GB", "-c", "shared_buffers=512MB", "-c", "wal_compression=on"]
restart: always
volumes:

View File

@@ -5,9 +5,6 @@ UPLOAD_LOCATION=./library
# The location where your database files are stored
DB_DATA_LOCATION=./postgres
# To set a timezone, uncomment the next line and change Etc/UTC to a TZ identifier from this list: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List
# TZ=Etc/UTC
# The Immich version to use. You can pin this to a specific version like "v1.71.0"
IMMICH_VERSION=release

View File

@@ -1,7 +1,9 @@
version: "3.8"
# Configurations for hardware-accelerated machine learning
# If using Unraid or another platform that doesn't allow multiple Compose files,
# you can inline the config for a backend by copying its contents
# you can inline the config for a backend by copying its contents
# into the immich-machine-learning service in the docker-compose.yml file.
# See https://immich.app/docs/features/ml-hardware-acceleration for info on usage.
@@ -28,7 +30,7 @@ services:
openvino:
device_cgroup_rules:
- 'c 189:* rmw'
- "c 189:* rmw"
devices:
- /dev/dri:/dev/dri
volumes:

View File

@@ -1,3 +1,5 @@
version: "3.8"
# Configurations for hardware-accelerated transcoding
# If using Unraid or another platform that doesn't allow multiple Compose files,

View File

@@ -1 +1 @@
20.14
v20.12

View File

@@ -5,13 +5,13 @@ This website is built using [Docusaurus](https://docusaurus.io/), a modern stati
### Installation
```
$ npm install
$ yarn
```
### Local Development
```
$ npm run start
$ yarn start
```
This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server.
@@ -19,7 +19,7 @@ This command starts a local development server and opens up a browser window. Mo
### Build
```
$ npm run build
$ yarn build
```
This command generates static content into the `build` directory and can be served using any static contents hosting service.
@@ -29,13 +29,13 @@ This command generates static content into the `build` directory and can be serv
Using SSH:
```
$ USE_SSH=true npm run deploy
$ USE_SSH=true yarn deploy
```
Not using SSH:
```
$ GIT_USER=<Your GitHub username> npm run deploy
$ GIT_USER=<Your GitHub username> yarn deploy
```
If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch.

View File

@@ -67,7 +67,7 @@ Yes, with an [External Library](/docs/features/libraries.md).
### What happens to existing files after I choose a new [Storage Template](/docs/administration/storage-template.mdx)?
Template changes will only apply to _new_ assets. To retroactively apply the template to previously uploaded assets, run the Storage Migration Job, available on the [Jobs](/docs/administration/jobs-workers/#jobs) page.
Template changes will only apply to _new_ assets. To retroactively apply the template to previously uploaded assets, run the Storage Migration Job, available on the [Jobs](/docs/administration/jobs.md) page.
### Why are only photos and not videos being uploaded to Immich?
@@ -399,47 +399,8 @@ If it mentions SIGILL (note the lack of a K) or error code 132, it most likely m
If your version of Immich is below 1.92.0 and the crash occurs after logs about tracing or exporting a model, consider either upgrading or disabling the Tag Objects job.
## Database
### Why does Immich log migration errors on startup?
### Why am I getting database ownership errors?
If you get database errors such as `FATAL: data directory "/var/lib/postgresql/data" has wrong ownership` upon database startup, this is likely due to an issue with your filesystem.
NTFS and ex/FAT/32 filesystems are not supported. See [here](/docs/install/environment-variables#supported-filesystems) for more details.
### How can I verify the integrity of my database?
If you installed Immich using v1.104.0 or later, you likely have database checksums enabled by default. You can check this by running the following command.
A result of `on` means that checksums are enabled.
<details>
<summary>Check if checksums are enabled</summary>
```bash
docker exec -it immich_postgres psql --dbname=immich --username=<DB_USERNAME> --command="show data_checksums"
data_checksums
----------------
on
(1 row)
```
</details>
If checksums are enabled, you can check the status of the database with the following command. A normal result is all zeroes.
<details>
<summary>Check for database corruption</summary>
```bash
docker exec -it immich_postgres psql --dbname=immich --username=<DB_USERNAME> --command="SELECT datname, checksum_failures, checksum_last_failure FROM pg_stat_database WHERE datname IS NOT NULL"
datname | checksum_failures | checksum_last_failure
-----------+-------------------+-----------------------
postgres | 0 |
immich | 0 |
template1 | 0 |
template0 | 0 |
(4 rows)
```
</details>
Sometimes Immich logs errors such as "duplicate key value violates unique constraint" or "column (...) of relation (...) already exists". Because of Immich's container structure, this error can be seen when both immich and immich-microservices start at the same time and attempt to migrate or create the database structure. Since the database migration is run sequentially and inside of transactions, this error message does not cause harm to your installation of Immich and can safely be ignored. If needed, you can manually restart Immich by running `docker restart immich immich-microservices`.
[huggingface]: https://huggingface.co/immich-app

View File

@@ -17,10 +17,6 @@ Refer to the official [postgres documentation](https://www.postgresql.org/docs/c
The recommended way to backup and restore the Immich database is to use the `pg_dumpall` command. When restoring, you need to delete the `DB_DATA_LOCATION` folder (if it exists) to reset the database.
:::caution
It is not recommended to directly backup the `DB_DATA_LOCATION` folder. Doing so while the database is running can lead to a corrupted backup that cannot be restored.
:::
<Tabs>
<TabItem value="Linux system" label="Linux system" default>
@@ -55,7 +51,7 @@ docker compose pull # Update to latest version of Immich (if desired)
docker compose create # Create Docker containers for Immich apps without running them.
docker start immich_postgres # Start Postgres server
sleep 10 # Wait for Postgres server to start up
gc "C:\path\to\backup\dump.sql" | docker exec -i immich_postgres psql --username=postgres # Restore Backup
gc "C:\path\to\backup\dump.sql" | docker exec -i immich_postgres psql --username=postgres # Restore Backup
docker compose up -d # Start remainder of Immich apps
```

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

View File

@@ -1,55 +0,0 @@
# Jobs and Workers
## Workers
### Architecture
The `immich-server` container contains multiple workers:
- `api`: responds to API requests for data and files for the web and mobile app.
- `microservices`: handles most other work, such as thumbnail generation and video encoding, in the form of _jobs_. Simply put, a job is a request to process data in the background.
## Split workers
If you prefer to throttle or distribute the workers, you can do this using the [environment variables](/docs/install/environment-variables) to specify which container should pick up which tasks.
For example, for a simple setup with one container for the Web/API and one for all other microservices, you can do the following:
Copy the entire `immich-server` block as a new service and make the following changes to the **copy**:
```diff
- immich-server:
- container_name: immich_server
...
- ports:
- - 2283:3001
+ immich-microservices:
+ container_name: immich_microservices
```
Once you have two copies of the immich-server service, make the following chnages to each one. This will allow one container to only serve the web UI and API, and the other one to handle all other tasks.
```diff
services:
immich-server:
...
+ environment:
+ IMMICH_WORKERS_INCLUDE: 'api'
immich-microservices:
...
+ environment:
+ IMMICH_WORKERS_EXCLUDE: 'api'
```
## Jobs
When a new asset is uploaded it kicks off a series of jobs, which include metadata extraction, thumbnail generation, machine learning tasks, and storage template migration, if enabled. To view the status of a job navigate to the Administration -> Jobs page.
Additionally, some jobs run on a schedule, which is every night at midnight. This schedule, with the exception of [External Libraries](/docs/features/libraries) scanning, cannot be changed.
:::info
Storage Migration job can be run after changing the [Storage Template](/docs/administration/storage-template.mdx), in order to apply the change to the existing library.
:::
<img src={require('./img/admin-jobs.png').default} width="80%" title="Admin jobs" />

View File

@@ -0,0 +1,13 @@
# Jobs
The `immich-server` responds to API requests for data and files for the web and mobile app. To do this quickly and reliably, it offloads most other work to `immich-microservices` in the form of _jobs_. Simply put, a job is a request to process data in the background. Jobs are picked up automatically by microservices containers.
When a new asset is uploaded it kicks off a series of jobs, which include metadata extraction, thumbnail generation, machine learning tasks, and storage template migration, if enabled. To view the status of a job navigate to the Administration -> Jobs page.
Additionally, some jobs run on a schedule, which is every night at midnight. This schedule, with the exception of [External Libraries](/docs/features/libraries) scanning, cannot be changed.
:::info
Storage Migration job can be run after changing the [Storage Template](/docs/administration/storage-template.mdx), in order to apply the change to the existing library.
:::
<img src={require('./img/admin-jobs.png').default} width="80%" title="Admin jobs" />

View File

@@ -110,66 +110,8 @@ Immich has a route (`/api/oauth/mobile-redirect`) that is already configured to
## Example Configuration
<details>
<summary>Authentik Example</summary>
### Authentik Example
Here's an example of OAuth configured for Authentik:
Configuration of Authorised redirect URIs (Authentik OAuth2/OpenID Provider)
<img src={require('./img/authentik-redirect-uris-example.webp').default} width='70%' title="Authentik authorised redirect URIs" />
Configuration of OAuth in Immich System Settings
| Setting | Value |
| ---------------------------- | ---------------------------------------------------------------------------------- |
| Issuer URL | `https://example.immich.app/application/o/immich/.well-known/openid-configuration` |
| Client ID | AFCj2rM1f4rps**\*\*\*\***\***\*\*\*\***lCLEum6hH9... |
| Client Secret | 0v89FXkQOWO\***\*\*\*\*\***\*\*\***\*\*\*\*\***mprbvXD549HH6s1iw... |
| Scope | openid email profile |
| Signing Algorithm | RS256 |
| Storage Label Claim | preferred_username |
| Storage Quota Claim | immich_quota |
| Default Storage Quota (GiB) | 0 (0 for unlimited quota) |
| Button Text | Sign in with Authentik (optional) |
| Auto Register | Enabled (optional) |
| Auto Launch | Enabled (optional) |
| Mobile Redirect URI Override | Disable |
| Mobile Redirect URI | |
</details>
<details>
<summary>Google Example</summary>
### Google Example
Here's an example of OAuth configured for Google:
Configuration of Authorised redirect URIs (Google Console)
<img src={require('./img/google-redirect-uris-example.webp').default} width='50%' title="Google authorised redirect URIs" />
Configuration of OAuth in Immich System Settings
| Setting | Value |
| ---------------------------- | ------------------------------------------------------------------------------------------------------ |
| Issuer URL | [https://accounts.google.com](https://accounts.google.com) |
| Client ID | 7\***\*\*\*\*\*\*\***\*\*\***\*\*\*\*\*\*\***vuls.apps.googleusercontent.com |
| Client Secret | G\***\*\*\*\*\*\*\***\*\*\***\*\*\*\*\*\*\***OO |
| Scope | openid email profile |
| Signing Algorithm | RS256 |
| Storage Label Claim | preferred_username |
| Storage Quota Claim | immich_quota |
| Default Storage Quota (GiB) | 0 (0 for unlimited quota) |
| Button Text | Sign in with Google (optional) |
| Auto Register | Enabled (optional) |
| Auto Launch | Enabled |
| Mobile Redirect URI Override | Enabled (required) |
| Mobile Redirect URI | [https://demo.immich.app/api/oauth/mobile-redirect](https://demo.immich.app/api/oauth/mobile-redirect) |
</details>
![OAuth Settings](./img/oauth-settings.png)
[oidc]: https://openid.net/connect/

View File

@@ -5,7 +5,7 @@ While not officially recommended, it is possible to run Immich using a pre-exist
By default, Immich expects superuser permission on the Postgres database and requires certain extensions to be installed. This guide outlines the steps required to prepare a pre-existing Postgres server to be used by Immich.
:::tip
Running with a pre-existing Postgres server can unlock powerful administrative features, including logical replication and streaming write-ahead log backups using programs like pgBackRest or Barman.
Running with a pre-existing Postgres server can unlock powerful administrative features, including logical replication, data page checksums, and streaming write-ahead log backups using programs like pgBackRest or Barman.
:::
## Prerequisites

View File

@@ -18,7 +18,7 @@ In any other situation, there are 3 different options that can appear:
- MATCHES - These files are matched by their checksums.
- OFFLINE PATHS - These files are the result of manually deleting files from immich or a failed file move in the past (losing track of a file).
- OFFLINE PATHS - These files are the result of manually deleting files in the upload library or a failed file move in the past (losing track of a file).
- UNTRACKED FILES - These files are not tracked by the application. They can be the result of failed moves, interrupted uploads, or left behind due to a bug.

View File

@@ -77,6 +77,7 @@ immich-admin list-users
deletedAt: null,
updatedAt: 2023-09-21T15:42:28.129Z,
oauthId: '',
memoriesEnabled: true
}
]
```

View File

@@ -80,7 +80,7 @@ The Immich Microservices image uses the same `Dockerfile` as the Immich Server,
- Background jobs (file deletion, user deletion)
:::info
This list closely matches what is available on the [Administration > Jobs](/docs/administration/jobs-workers/#jobs) page, which provides some remote queue management capabilities.
This list closely matches what is available on the [Administration > Jobs](/docs/administration/jobs.md) page, which provides some remote queue management capabilities.
:::
### Machine Learning

View File

@@ -9,11 +9,6 @@ When contributing code through a pull request, please check the following:
- [ ] `npm run check:svelte` (Type checking via SvelteKit)
- [ ] `npm test` (unit tests)
## Documentation
- [ ] `npm run format` (formatting via Prettier)
- [ ] Update the `_redirects` file if you have renamed a page or removed it from the documentation.
:::tip AIO
Run all web checks with `npm run check:all`
:::

View File

@@ -22,8 +22,7 @@ You do not need to redo any transcoding jobs after enabling hardware acceleratio
- WSL2 does not support Quick Sync.
- Raspberry Pi is currently not supported.
- Two-pass mode is only supported for NVENC. Other APIs will ignore this setting.
- By default, only encoding is currently hardware accelerated. This means the CPU is still used for software decoding and tone-mapping.
- NVENC and RKMPP can be fully accelerated by enabling hardware decoding in the video transcoding settings.
- Only encoding is currently hardware accelerated, so the CPU is still used for software decoding and tone-mapping.
- Hardware dependent
- Codec support varies, but H.264 and HEVC are usually supported.
- Notably, NVIDIA and AMD GPUs do not support VP9 encoding.
@@ -34,7 +33,7 @@ You do not need to redo any transcoding jobs after enabling hardware acceleratio
#### NVENC
- You must have the official NVIDIA driver installed on the server.
- On Linux (except for WSL2), you also need to have [NVIDIA Container Toolkit][nvct] installed.
- On Linux (except for WSL2), you also need to have [NVIDIA Container Runtime][nvcr] installed.
#### QSV
@@ -66,7 +65,6 @@ For RKMPP to work:
3. Redeploy the `immich-microservices` container with these updated settings.
4. In the Admin page under `Video transcoding settings`, change the hardware acceleration setting to the appropriate option and save.
5. (Optional) If using a compatible backend, you may enable hardware decoding for optimal performance.
#### Single Compose File
@@ -124,7 +122,7 @@ Once this is done, you can continue to step 3 of "Basic Setup".
- While you can use VAAPI with NVIDIA and Intel devices, prefer the more specific APIs since they're more optimized for their respective devices
[hw-file]: https://github.com/immich-app/immich/releases/latest/download/hwaccel.transcoding.yml
[nvct]: https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html
[nvcr]: https://github.com/NVIDIA/nvidia-container-runtime/
[jellyfin-lp]: https://jellyfin.org/docs/general/administration/hardware-acceleration/intel/#configure-and-verify-lp-mode-on-linux
[jellyfin-kernel-bug]: https://jellyfin.org/docs/general/administration/hardware-acceleration/intel/#known-issues-and-limitations
[libmali-rockchip]: https://github.com/tsukumijima/libmali-rockchip/releases

View File

@@ -4,9 +4,13 @@
Immich supports the creation of libraries which is a top-level asset container. Currently, there are two types of libraries: traditional upload libraries that can sync with a mobile device, and external libraries, that keeps up to date with files on disk. Libraries are different from albums in that an asset can belong to multiple albums but only one library, and deleting a library deletes all assets contained within. As of August 2023, this is a new feature and libraries have a lot of potential for future development beyond what is documented here. This document attempts to describe the current state of libraries.
## The Upload Library
Immich comes preconfigured with an upload library for each user. All assets uploaded to Immich are added to this library. This library can be renamed, but not deleted. The upload library is the only library that can be synced with a mobile device. No items in an upload library is allowed to have the same sha1 hash as another item in the same library in order to prevent duplicates.
## External Libraries
External libraries tracks assets stored outside of Immich, i.e. in the file system. When the external library is scanned, Immich will read the metadata from the file and create an asset in the library for each image or video file. These items will then be shown in the main timeline, and they will look and behave like any other asset, including viewing on the map, adding to albums, etc.
External libraries tracks assets stored outside of immich, i.e. in the file system. Immich will only read data from the files, and will not modify them in any way. Therefore, the delete button is disabled for external assets. When the external library is scanned, immich will read the metadata from the file and create an asset in the library for each image or video file. These items will then be shown in the main timeline, and they will look and behave like any other asset, including viewing on the map, adding to albums, etc.
If a file is modified outside of Immich, the changes will not be reflected in immich until the library is scanned again. There are different ways to scan a library depending on the use case:
@@ -44,16 +48,16 @@ If the import paths are edited in a way that an external file is no longer in an
### Troubleshooting
Sometimes, an external library will not scan correctly. This can happen if Immich can't access the files. Here are some things to check:
Sometimes, an external library will not scan correctly. This can happen if immich_server or immich_microservices can't access the files. Here are some things to check:
- In the docker-compose file, are the volumes mounted correctly?
- Are the volumes also mounted to any worker containers?
- Are the volumes identical between the `server` and `microservices` container?
- Are the import paths set correctly, and do they match the path set in docker-compose file?
- Make sure you don't use symlinks in your import libraries, and that you aren't linking across docker mounts.
- Are the permissions set correctly?
- Make sure you are using forward slashes (`/`) and not backward slashes.
To validate that Immich can reach your external library, start a shell inside the container. Run `docker exec -it immich_server bash` to a bash shell. If your import path is `/data/import/photos`, check it with `ls /data/import/photos`. Do the same check for the same in any microservices containers.
To validate that Immich can reach your external library, start a shell inside the container. Run `docker exec -it immich_microservices /bin/bash` to a bash shell. If your import path is `/data/import/photos`, check it with `ls /data/import/photos`. Do the same check for the `immich_server` container. If you cannot access this directory in both the `microservices` and `server` containers, Immich won't be able to import files.
### Exclusion Patterns
@@ -98,7 +102,7 @@ First, we need to plan how we want to organize the libraries. The christmas trip
### Mount Docker Volumes
The `immich-server` container will need access to the gallery. Modify your docker compose file as follows
`immich-server` and `immich-microservices` containers will need access to the gallery. Modify your docker compose file as follows
```diff title="docker-compose.yml"
immich-server:
@@ -107,6 +111,15 @@ The `immich-server` container will need access to the gallery. Modify your docke
+ - /mnt/nas/christmas-trip:/mnt/media/christmas-trip:ro
+ - /home/user/old-pics:/mnt/media/old-pics:ro
+ - /mnt/media/videos:/mnt/media/videos:ro
+ - "C:/Users/user_name/Desktop/my media:/mnt/media/my-media:ro" # import path in Windows system.
immich-microservices:
volumes:
- ${UPLOAD_LOCATION}:/usr/src/app/upload
+ - /mnt/nas/christmas-trip:/mnt/media/christmas-trip:ro
+ - /home/user/old-pics:/mnt/media/old-pics:ro
+ - /mnt/media/videos:/mnt/media/videos:ro
+ - "C:/Users/user_name/Desktop/my media:/mnt/media/my-media:ro" # import path in Windows system.
```

View File

@@ -38,7 +38,7 @@ You do not need to redo any machine learning jobs after enabling hardware accele
- The GPU must have compute capability 5.2 or greater.
- The server must have the official NVIDIA driver installed.
- The installed driver must be >= 535 (it must support CUDA 12.2).
- On Linux (except for WSL2), you also need to have [NVIDIA Container Toolkit][nvct] installed.
- On Linux (except for WSL2), you also need to have [NVIDIA Container Runtime][nvcr] installed.
#### OpenVINO
@@ -95,11 +95,11 @@ immich-machine-learning:
Once this is done, you can redeploy the `immich-machine-learning` container.
:::info
You can confirm the device is being recognized and used by checking its utilization (via `nvtop` for CUDA, `intel_gpu_top` for OpenVINO, etc.). You can also enable debug logging by setting `IMMICH_LOG_LEVEL=debug` in the `.env` file and restarting the `immich-machine-learning` container. When a Smart Search or Face Detection job begins, you should see a log for `Available ORT providers` containing the relevant provider. In the case of ARM NN, the absence of a `Could not load ANN shared libraries` log entry means it loaded successfully.
You can confirm the device is being recognized and used by checking its utilization (via `nvtop` for CUDA, `intel_gpu_top` for OpenVINO, etc.). You can also enable debug logging by setting `LOG_LEVEL=debug` in the `.env` file and restarting the `immich-machine-learning` container. When a Smart Search or Face Detection job begins, you should see a log for `Available ORT providers` containing the relevant provider. In the case of ARM NN, the absence of a `Could not load ANN shared libraries` log entry means it loaded successfully.
:::
[hw-file]: https://github.com/immich-app/immich/releases/latest/download/hwaccel.ml.yml
[nvct]: https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html
[nvcr]: https://github.com/NVIDIA/nvidia-container-runtime/
## Tips

View File

@@ -9,7 +9,7 @@ It is important to remember to update the backup settings after following the gu
In our `.env` file, we will define variables that will help us in the future when we want to move to a more advanced server in the future
```diff title=".env"
# You can find documentation for all the supported env variables [here](/docs/install/environment-variables)
# You can find documentation for all the supported env variables at https://immich.app/docs/install/environment-variables
# Custom location where your uploaded, thumbnails, and transcoded video files are stored
- UPLOAD_LOCATION=./library

View File

@@ -1,16 +0,0 @@
# Create Custom Map Styles for Immich Using Maptiler
You may decide that you'd like to modify the style document which is used to draw the maps in Immich. This can be done easily using Maptiler, if you do not want to write an entire JSON document by hand.
## Steps
1. Create a free account at https://cloud.maptiler.com
2. Once logged in, you can either create a brand new map by clicking on **New Map**, selecting a starter map, and then clicking **Customize**, OR by selecting a **Standard Map** and customizing it from there.
3. The **editor** interface is self-explanatory. You can change colors, remove visible layers, or add optional layers (e.g., administrative, topo, hydro, etc.) in the composer.
4. Once you have your map composed, click on **Save** at the top right. Give it a unique name to save it to your account.
5. Next, **Publish** your style using the **Publish** button at the top right. This will deploy it to production, which means it is able to be exposed over the Internet. Maptiler will present an interactive side-by-side map with the original and your changes prior to publication.<br/>![Maptiler Publication Settings](img/immich_map_styles_publish.png)
6. Maptiler will warn you that changing the map will change it across all apps using the map. Since no apps are using the map yet, this is okay.
7. Clicking on the name of your new map at the top left will bring you to the item's **details** page. From here, copy the link to the JSON style under **Use vector style**. This link will automatically contain your personal API key to Maptiler.
8. In **Immich**, navigate to **Administration --> Settings --> Map & GPS Settings** and expand the **Map Settings** subsection.
9. Paste the link to your JSON style in either the **Light Style** or **Dark Style**. (You can add different styles which will help make the map style more appropriate depending on whether you set **Immich** to Light or Dark mode.
10. Save your selections. Reload the map, and enjoy your custom map style!

View File

@@ -2,52 +2,48 @@
A short guide on connecting [pgAdmin](https://www.pgadmin.org/) to Immich.
:::note
In order to connect to the database the immich_postgres container **must be running**.
The passwords and usernames used below match the ones specified in the example `.env` file. If changed, please use actual values instead.
**Optional:** To connect to the database **outside** of your Docker's network:
- Expose port 5432 in your `docker-compose.yml` file.
- Edit the PostgreSQL [`pg_hba.conf`](https://www.postgresql.org/docs/current/auth-pg-hba-conf.html) file.
- Make sure your firewall does not block access to port 5432.
Note that exposing the database port increases the risk of getting attacked by hackers.
Make sure to remove the binding port after finishing the database's tasks.
:::
## 1. Install pgAdmin
Add a file `docker-compose-pgadmin.yml` next to your `docker-compose.yml` with the following content:
```
name: immich
services:
pgadmin:
image: dpage/pgadmin4
container_name: pgadmin4_container
restart: always
ports:
- "8888:80"
environment:
PGADMIN_DEFAULT_EMAIL: user-name@domain-name.com
PGADMIN_DEFAULT_PASSWORD: strong-password
volumes:
- pgadmin-data:/var/lib/pgadmin
volumes:
pgadmin-data:
```
Change the values of `PGADMIN_DEFAULT_EMAIL` and `PGADMIN_DEFAULT_PASSWORD` in this file.
Run `docker compose -f docker-compose.yml -f docker-compose-pgadmin.yml up` to start immich along with `pgAdmin`.
Download and install [pgAdmin](https://www.pgadmin.org/download/) following the official documentation.
## 2. Add a Server
Open [localhost:8888](http://localhost:8888) and login with the default credentials from above.
Open pgAdmin and click "Add New Server".
Right click on `Servers` and click on `Register >> Server..` then enter the values below in the `Connection` tab.
<img src={require('./img/add-new-server-option.png').default} width="50%" title="new server option" />
<img src={require('./img/pgadmin-add-new-server.png').default} width="50%" title="new server option" />
## 3. Enter Connection Details
:::note
The parameters used here match those specified in the example `.env` file. If you have changed your `.env` file, you'll need to adjust accordingly.
:::
| Name | Value |
| -------------------- | ----------- |
| Host name/address | `localhost` |
| Port | `5432` |
| Maintenance database | `immich` |
| Username | `postgres` |
| Password | `postgres` |
| Name | Value |
| -------------------- | ----------------- |
| Host name/address | `immich_postgres` |
| Port | `5432` |
| Maintenance database | `immich` |
| Username | `postgres` |
| Password | `postgres` |
<img src={require('./img/Connection-Pgadmin.png').default} width="75%" title="Connection" />
## 4. Save Connection
Click on "Save" to connect to the Immich database.
:::tip
View [Database Queries](/docs/guides/database-queries/) for common database queries.
:::

View File

@@ -96,7 +96,7 @@ SELECT * FROM "users";
## System Config
```sql title="Custom settings"
SELECT "key", "value" FROM "system_metadata" WHERE "key" = 'system-config';
SELECT "key", "value" FROM "system_config";
```
(Only used when not using the [config file](/docs/install/config-file))

View File

@@ -11,8 +11,9 @@ docker ps -a # see a list of running and stopped containers
```bash
docker exec -it <id or name> <command> # attach to a container with a command
docker exec -it immich_server bash
docker exec -it immich_machine_learning bash
docker exec -it immich_server sh
docker exec -it immich_microservices sh
docker exec -it immich_machine_learning sh
```
## Logs
@@ -21,6 +22,7 @@ docker exec -it immich_machine_learning bash
docker logs <id or name> # see the logs for a specific container (by id or name)
docker logs immich_server
docker logs immich_microservices
docker logs immich_machine_learning
```

View File

@@ -6,13 +6,15 @@ in a directory on the same machine.
# Mount the directory into the containers.
Edit `docker-compose.yml` to add two new mount points in the section `immich-server:` under `volumes:`
Edit `docker-compose.yml` to add two new mount points under `volumes:`
```diff
immich-server:
volumes:
+ - ${EXTERNAL_PATH}:/usr/src/app/external
```
immich-server:
volumes:
- ${EXTERNAL_PATH}:/usr/src/app/external
```
Be sure to add exactly the same line to both `immich-server:` and `immich-microservices:`.
Edit `.env` to define `EXTERNAL_PATH`, substituting in the correct path for your computer:

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

View File

@@ -32,7 +32,7 @@ def upload(file):
}
response = requests.post(
f'{BASE_URL}/assets', headers=headers, data=data, files=files)
f'{BASE_URL}/asset/upload', headers=headers, data=data, files=files)
print(response.json())
# {'id': 'ef96f635-61c7-4639-9e60-61a11c4bbfba', 'duplicate': False}

View File

@@ -7,7 +7,7 @@ To alleviate [performance issues on low-memory systems](/docs/FAQ.mdx#why-is-imm
- Start the container by running `docker compose up -d`.
:::info
Starting with version v1.93.0 face detection work and face recognize were split. From now on face detection is done in the immich_machine_learning container, but facial recognition is done in the `microservices` worker.
Starting with version v1.93.0 face detection work and face recognize were split. From now on face detection is done in the immich_machine_learning service, but facial recognition is done in the immich_microservices service.
:::
:::note
@@ -15,7 +15,7 @@ The [hwaccel.ml.yml](https://github.com/immich-app/immich/releases/latest/downlo
:::
```yaml
name: immich_remote_ml
version: '3.8'
services:
immich-machine-learning:

View File

@@ -77,10 +77,6 @@ The default configuration looks like this:
"enabled": true,
"modelName": "ViT-B-32__openai"
},
"duplicateDetection": {
"enabled": false,
"maxDistance": 0.03
},
"facialRecognition": {
"enabled": true,
"modelName": "buffalo_l",
@@ -157,6 +153,9 @@ The default configuration looks like this:
"server": {
"externalDomain": "",
"loginPageMessage": ""
},
"user": {
"deleteDelay": 7
}
}
```

View File

@@ -17,37 +17,29 @@ If this should not work, try running `docker compose up -d --force-recreate`.
## Docker Compose
| Variable | Description | Default | Containers |
| :----------------- | :------------------------------ | :-------: | :----------------------- |
| `IMMICH_VERSION` | Image tags | `release` | server, machine learning |
| `UPLOAD_LOCATION` | Host Path for uploads | | server |
| `DB_DATA_LOCATION` | Host Path for Postgres database | | database |
| Variable | Description | Default | Services |
| :----------------- | :------------------------------ | :-------: | :-------------------------------------- |
| `IMMICH_VERSION` | Image tags | `release` | server, microservices, machine learning |
| `UPLOAD_LOCATION` | Host Path for uploads | | server, microservices |
| `DB_DATA_LOCATION` | Host Path for Postgres database | | database |
:::tip
These environment variables are used by the `docker-compose.yml` file and do **NOT** affect the containers directly.
:::
### Supported filesystems
The Immich Postgres database (`DB_DATA_LOCATION`) must be located on a filesystem that supports user/group
ownership and permissions (EXT2/3/4, ZFS, APFS, BTRFS, XFS, etc.). It will not work on any filesystem formatted in NTFS or ex/FAT/32.
It will not work in WSL (Windows Subsystem for Linux) when using a mounted host directory (commonly under `/mnt`).
If this is an issue, you can change the bind mount to a Docker volume instead.
Regardless of filesystem, it is not recommended to use a network share for your database location due to performance and possible data loss issues.
## General
| Variable | Description | Default | Containers | Workers |
| :------------------------------ | :---------------------------------------------- | :----------------------: | :----------------------- | :----------------- |
| `TZ` | Timezone | | server | microservices |
| `IMMICH_ENV` | Environment (production, development) | `production` | server, machine learning | api, microservices |
| `IMMICH_LOG_LEVEL` | Log Level (verbose, debug, log, warn, error) | `log` | server, machine learning | api, microservices |
| `IMMICH_MEDIA_LOCATION` | Media Location | `./upload`<sup>\*1</sup> | server | api, microservices |
| `IMMICH_CONFIG_FILE` | Path to config file | | server | api, microservices |
| `IMMICH_WEB_ROOT` | Path of root index.html | `/usr/src/app/www` | server | api |
| `IMMICH_REVERSE_GEOCODING_ROOT` | Path of reverse geocoding dump directory | `/usr/src/resources` | server | microservices |
| `NO_COLOR` | Set to `true` to disable color-coded log output | `false` | server, machine learning | |
| Variable | Description | Default | Services |
| :------------------------------ | :------------------------------------------- | :----------------------: | :-------------------------------------- |
| `TZ` | Timezone | | microservices |
| `NODE_ENV` | Environment (production, development) | `production` | server, microservices, machine learning |
| `LOG_LEVEL` | Log Level (verbose, debug, log, warn, error) | `log` | server, microservices, machine learning |
| `IMMICH_MEDIA_LOCATION` | Media Location | `./upload`<sup>\*1</sup> | server, microservices |
| `IMMICH_CONFIG_FILE` | Path to config file | | server, microservices |
| `IMMICH_WEB_ROOT` | Path of root index.html | `/usr/src/app/www` | server |
| `IMMICH_REVERSE_GEOCODING_ROOT` | Path of reverse geocoding dump directory | `/usr/src/resources` | microservices |
\*1: With the default `WORKDIR` of `/usr/src/app`, this path will resolve to `/usr/src/app/upload`.
It only need to be set if the Immich deployment method is changing.
@@ -55,39 +47,31 @@ It only need to be set if the Immich deployment method is changing.
:::tip
`TZ` should be set to a `TZ identifier` from [this list][tz-list]. For example, `TZ="Etc/UTC"`.
`TZ` is used by `exiftool` as a fallback in case the timezone cannot be determined from the image metadata. It is also used for logfile timestamps and cron job execution.
:::
## Workers
| Variable | Description | Default | Containers |
| :----------------------- | :--------------------------------------------------------------------------------------------------- | :-----: | :--------- |
| `IMMICH_WORKERS_INCLUDE` | Only run these workers. | | server |
| `IMMICH_WORKERS_EXCLUDE` | Do not run these workers. Matches against default workers, or `IMMICH_WORKERS_INCLUDE` if specified. | | server |
:::info
Information on the current workers can be found [here](/docs/administration/jobs-workers).
`TZ` is only used by `exiftool`, which is present in the microservices container, as a fallback in case the timezone cannot be determined from the image metadata.
:::
## Ports
| Variable | Description | Default |
| :------------ | :------------- | :----------------------------------------: |
| `IMMICH_HOST` | Listening host | `0.0.0.0` |
| `IMMICH_PORT` | Listening port | `3001` (server), `3003` (machine learning) |
| Variable | Description | Default | Services |
| :---------------------- | :-------------------- | :-------: | :-------------------- |
| `HOST` | Host | `0.0.0.0` | server, microservices |
| `SERVER_PORT` | Server Port | `3001` | server |
| `MICROSERVICES_PORT` | Microservices Port | `3002` | microservices |
| `MACHINE_LEARNING_HOST` | Machine Learning Host | `0.0.0.0` | machine learning |
| `MACHINE_LEARNING_PORT` | Machine Learning Port | `3003` | machine learning |
## Database
| Variable | Description | Default | Containers |
| :---------------------------------- | :----------------------------------------------------------------------- | :----------: | :----------------------------- |
| `DB_URL` | Database URL | | server |
| `DB_HOSTNAME` | Database Host | `database` | server |
| `DB_PORT` | Database Port | `5432` | server |
| `DB_USERNAME` | Database User | `postgres` | server, database<sup>\*1</sup> |
| `DB_PASSWORD` | Database Password | `postgres` | server, database<sup>\*1</sup> |
| `DB_DATABASE_NAME` | Database Name | `immich` | server, database<sup>\*1</sup> |
| `DB_VECTOR_EXTENSION`<sup>\*2</sup> | Database Vector Extension (one of [`pgvector`, `pgvecto.rs`]) | `pgvecto.rs` | server |
| `DB_SKIP_MIGRATIONS` | Whether to skip running migrations on startup (one of [`true`, `false`]) | `false` | server |
| Variable | Description | Default | Services |
| :---------------------------------- | :----------------------------------------------------------------------- | :----------: | :-------------------------------------------- |
| `DB_URL` | Database URL | | server, microservices |
| `DB_HOSTNAME` | Database Host | `database` | server, microservices |
| `DB_PORT` | Database Port | `5432` | server, microservices |
| `DB_USERNAME` | Database User | `postgres` | server, microservices, database<sup>\*1</sup> |
| `DB_PASSWORD` | Database Password | `postgres` | server, microservices, database<sup>\*1</sup> |
| `DB_DATABASE_NAME` | Database Name | `immich` | server, microservices, database<sup>\*1</sup> |
| `DB_VECTOR_EXTENSION`<sup>\*2</sup> | Database Vector Extension (one of [`pgvector`, `pgvecto.rs`]) | `pgvecto.rs` | server, microservices |
| `DB_SKIP_MIGRATIONS` | Whether to skip running migrations on startup (one of [`true`, `false`]) | `false` | server, microservices |
\*1: The values of `DB_USERNAME`, `DB_PASSWORD`, and `DB_DATABASE_NAME` are passed to the Postgres container as the variables `POSTGRES_USER`, `POSTGRES_PASSWORD`, and `POSTGRES_DB` in `docker-compose.yml`.
@@ -95,34 +79,30 @@ Information on the current workers can be found [here](/docs/administration/jobs
:::info
All `DB_` variables must be provided to all Immich workers, including `api` and `microservices`.
`DB_URL` must be in the format `postgresql://immichdbusername:immichdbpassword@postgreshost:postgresport/immichdatabasename`.
You can require SSL by adding `?sslmode=require` to the end of the `DB_URL` string, or require SSL and skip certificate verification by adding `?sslmode=require&sslmode=no-verify`.
When `DB_URL` is defined, the `DB_HOSTNAME`, `DB_PORT`, `DB_USERNAME`, `DB_PASSWORD` and `DB_DATABASE_NAME` database variables are ignored.
:::
## Redis
| Variable | Description | Default | Containers |
| :--------------- | :------------- | :-----: | :--------- |
| `REDIS_URL` | Redis URL | | server |
| `REDIS_SOCKET` | Redis Socket | | server |
| `REDIS_HOSTNAME` | Redis Host | `redis` | server |
| `REDIS_PORT` | Redis Port | `6379` | server |
| `REDIS_USERNAME` | Redis Username | | server |
| `REDIS_PASSWORD` | Redis Password | | server |
| `REDIS_DBINDEX` | Redis DB Index | `0` | server |
| Variable | Description | Default | Services |
| :--------------- | :------------- | :-----: | :-------------------- |
| `REDIS_URL` | Redis URL | | server, microservices |
| `REDIS_HOSTNAME` | Redis Host | `redis` | server, microservices |
| `REDIS_PORT` | Redis Port | `6379` | server, microservices |
| `REDIS_DBINDEX` | Redis DB Index | `0` | server, microservices |
| `REDIS_USERNAME` | Redis Username | | server, microservices |
| `REDIS_PASSWORD` | Redis Password | | server, microservices |
| `REDIS_SOCKET` | Redis Socket | | server, microservices |
:::info
All `REDIS_` variables must be provided to all Immich workers, including `api` and `microservices`.
`REDIS_URL` must start with `ioredis://` and then include a `base64` encoded JSON string for the configuration.
More info can be found in the upstream [ioredis][redis-api] documentation.
When `REDIS_URL` or `REDIS_SOCKET` are defined, the `REDIS_HOSTNAME`, `REDIS_PORT`, `REDIS_USERNAME`, `REDIS_PASSWORD`, and `REDIS_DBINDEX` variables are ignored.
- When `REDIS_URL` is defined, the other redis (`REDIS_*`) variables are ignored.
- When `REDIS_SOCKET` is defined, the other redis (`REDIS_*`) variables are ignored.
:::
Redis (Sentinel) URL example JSON before encoding:
@@ -154,7 +134,7 @@ Redis (Sentinel) URL example JSON before encoding:
## Machine Learning
| Variable | Description | Default | Containers |
| Variable | Description | Default | Services |
| :----------------------------------------------- | :------------------------------------------------------------------- | :-----------------: | :--------------- |
| `MACHINE_LEARNING_MODEL_TTL` | Inactivity time (s) before a model is unloaded (disabled if \<= 0) | `300` | machine learning |
| `MACHINE_LEARNING_MODEL_TTL_POLL_S` | Interval (s) between checks for the model TTL (disabled if \<= 0) | `10` | machine learning |
@@ -179,13 +159,13 @@ Other machine learning parameters can be tuned from the admin UI.
## Prometheus
| Variable | Description | Default | Containers | Workers |
| :----------------------------- | :-------------------------------------------------------------------------------------------- | :-----: | :--------- | :----------------- |
| `IMMICH_METRICS`<sup>\*1</sup> | Toggle all metrics (one of [`true`, `false`]) | | server | api, microservices |
| `IMMICH_API_METRICS` | Toggle metrics for endpoints and response times (one of [`true`, `false`]) | | server | api, microservices |
| `IMMICH_HOST_METRICS` | Toggle metrics for CPU and memory utilization for host and process (one of [`true`, `false`]) | | server | api, microservices |
| `IMMICH_IO_METRICS` | Toggle metrics for database queries, image processing, etc. (one of [`true`, `false`]) | | server | api, microservices |
| `IMMICH_JOB_METRICS` | Toggle metrics for jobs and queues (one of [`true`, `false`]) | | server | api, microservices |
| Variable | Description | Default | Services |
| :----------------------------- | :-------------------------------------------------------------------------------------------- | :-----: | :-------------------- |
| `IMMICH_METRICS`<sup>\*1</sup> | Toggle all metrics (one of [`true`, `false`]) | | server, microservices |
| `IMMICH_API_METRICS` | Toggle metrics for endpoints and response times (one of [`true`, `false`]) | | server, microservices |
| `IMMICH_HOST_METRICS` | Toggle metrics for CPU and memory utilization for host and process (one of [`true`, `false`]) | | server, microservices |
| `IMMICH_IO_METRICS` | Toggle metrics for database queries, image processing, etc. (one of [`true`, `false`]) | | server, microservices |
| `IMMICH_JOB_METRICS` | Toggle metrics for jobs and queues (one of [`true`, `false`]) | | server, microservices |
\*1: Overridden for a metric group when its corresponding environmental variable is set.

View File

@@ -8,7 +8,7 @@ Install Immich using Portainer's Stack feature.
1. Go to "**Stacks**" in the left sidebar.
2. Click on "**Add stack**".
3. Give the stack a name (i.e. immich), and select "**Web Editor**" as the build method.
3. Give the stack a name (i.e. Immich), and select "**Web Editor**" as the build method.
4. Copy the content of the `docker-compose.yml` file from the [GitHub repository](https://github.com/immich-app/immich/releases/latest/download/docker-compose.yml).
5. Replace `.env` with `stack.env` for all containers that need to use environment variables in the web editor.
@@ -38,9 +38,8 @@ style={{border: '1px solid #ddd'}}
alt="Dot Env Example"
/>
- Change the default `DB_PASSWORD`, and add custom database connection information if necessary.
- Change `DB_DATA_LOCATION` to a folder where the database will be saved to disk.
- Change `UPLOAD_LOCATION` to a folder where media (uploaded and generated) will be stored.
- Populate custom database information if necessary.
- Populate `UPLOAD_LOCATION` with your preferred location for storing backup assets.
11. Click on "**Deploy the stack**".

View File

@@ -15,15 +15,12 @@ Hardware and software requirements for Immich
Immich requires the command `docker compose` - the similarly named `docker-compose` is [deprecated](https://docs.docker.com/compose/migrate/) and is no longer compatible with Immich.
:::
:::info Podman
You can also use Podman to run the application. However, additional configuration might be required.
:::
## Hardware
- **OS**: Recommended Linux operating system (Ubuntu, Debian, etc).
- Windows is supported with [Docker Desktop on Windows](https://docs.docker.com/desktop/install/windows-install/) or [WSL 2](https://docs.docker.com/desktop/wsl/).
- macOS is supported with [Docker Desktop on Mac](https://docs.docker.com/desktop/install/mac-install/).
- **RAM**: Minimum 4GB, recommended 6GB.
- **CPU**: Minimum 2 cores, recommended 4 cores.
- **Storage**: Recommended Unix-compatible filesystem (EXT4, ZFS, APFS, etc.) with support for user/group ownership and permissions.
- This can present an issue for Windows users. See [here](/docs/install/environment-variables#supported-filesystems)
for more details and alternatives.
- The generation of thumbnails and transcoded video can increase the size of the photo library by 10-20% on average.
- Network shares are supported for the storage of image and video assets only.
- **OS**: Preferred unix-based operating system (Ubuntu, Debian, MacOS, etc). Windows works too, with [Docker Desktop on Windows](https://docs.docker.com/desktop/install/windows-install/)
- **RAM**: At least 4GB, preferred 6GB.
- **CPU**: At least 2 cores, preferred 4 cores.

View File

@@ -77,9 +77,13 @@ const config = {
},
announcementBar: {
id: 'site_announcement_immich',
content: `⚠️ The project is under <strong>very active</strong> development. Expect bugs and changes. Do not use it as <strong>the only way</strong> to store your photos and videos!`,
backgroundColor: '#593f00',
textColor: '#ffefc9',
content:
'Immich joins Futo! Read about the announcement <a href="/blog/2024/immich-core-team-goes-fulltime">here</a>.',
backgroundColor: '#4250af',
textColor: 'white',
// content: `⚠️ The project is under <strong>very active</strong> development. Expect bugs and changes. Do not use it as <strong>the only way</strong> to store your photos and videos!`,
// backgroundColor: '#593f00',
// textColor: '#ffefc9',
isCloseable: false,
},
docs: {
@@ -100,9 +104,9 @@ const config = {
label: 'Docs',
},
{
to: '/roadmap',
to: '/milestones',
position: 'right',
label: 'Roadmap',
label: 'Milestones',
},
{
to: '/docs/api',
@@ -119,11 +123,6 @@ const config = {
label: 'GitHub',
position: 'right',
},
{
href: 'https://discord.gg/D8JsnBEuKb',
label: 'Discord',
position: 'right',
},
],
},
footer: {

1279
docs/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -56,6 +56,6 @@
"node": ">=20"
},
"volta": {
"node": "20.14.0"
"node": "20.12.2"
}
}

View File

@@ -10,17 +10,17 @@ interface CommunityGuidesProps {
const guides: CommunityGuidesProps[] = [
{
title: 'Cloudflare Tunnels with SSO/OAuth',
description: `Setting up Cloudflare Tunnels and a SaaS App for Immich.`,
description: `Setting up Cloudflare Tunnels and a SaaS App for immich.`,
url: 'https://github.com/immich-app/immich/discussions/8299',
},
{
title: 'Database backup in TrueNAS',
description: `Create a database backup with pgAdmin in TrueNAS.`,
title: 'Database backup in Truenas',
description: `Create a database backup with pgAdmin in Truenas.`,
url: 'https://github.com/immich-app/immich/discussions/8809',
},
{
title: 'Unraid backup scripts',
description: `Back up your assets in Unraid with a pre-prepared script.`,
description: `Back up your assets in Unarid with a pre-prepared script.`,
url: 'https://github.com/immich-app/immich/discussions/8416',
},
{
@@ -33,11 +33,6 @@ const guides: CommunityGuidesProps[] = [
description: 'Documentation for simple podman setup using quadlets.',
url: 'https://github.com/tbelway/immich-podman-quadlets/blob/main/docs/install/podman-quadlet.md',
},
{
title: 'Google Photos import + albums',
description: 'Import your Google Photos files into Immich and add your albums',
url: 'https://github.com/immich-app/immich/discussions/1340',
},
];
function CommunityGuide({ title, description, url }: CommunityGuidesProps): JSX.Element {

View File

@@ -10,7 +10,7 @@ interface CommunityProjectProps {
const projects: CommunityProjectProps[] = [
{
title: 'immich-go',
description: `An alternative to the immich-CLI that doesn't depend on nodejs. It specializes in importing Google Photos Takeout archives.`,
description: `An alternative to the immich-CLI command that doesn't depend on nodejs installation. It tries its best for importing google photos takeout archives.`,
url: 'https://github.com/simulot/immich-go',
},
{
@@ -33,11 +33,6 @@ const projects: CommunityProjectProps[] = [
description: 'A Python script to create albums based on the folder structure of an external library.',
url: 'https://github.com/Salvoxia/immich-folder-album-creator',
},
{
title: 'Immich-Tools',
description: 'Provides scripts for handling problems on the repair page.',
url: 'https://github.com/clumsyCoder00/Immich-Tools',
},
{
title: 'Lightroom Publisher: mi.Immich.Publisher',
description: 'Lightroom plugin to publish photos from Lightroom collections to Immich albums.',

View File

@@ -1,23 +1,28 @@
import useIsBrowser from '@docusaurus/useIsBrowser';
import { mdiCheckboxBlankCircle, mdiCheckboxMarkedCircle } from '@mdi/js';
import Icon from '@mdi/react';
import React from 'react';
import Icon from '@mdi/react';
import { mdiCheckboxMarkedCircleOutline } from '@mdi/js';
import useIsBrowser from '@docusaurus/useIsBrowser';
export type Item = {
export interface Item {
icon: string;
iconColor: string;
title: string;
description?: string;
link?: { url: string; text: string };
done?: false;
getDateLabel: (language: string) => string;
};
release?: string;
tag?: string;
date: Date;
dateType: DateType;
}
export enum DateType {
RELEASE = 'Release Date',
DATE = 'Date',
}
interface Props {
items: Item[];
}
export function Timeline({ items }: Props): JSX.Element {
export default function Timeline({ items }: Props): JSX.Element {
const isBrowser = useIsBrowser();
return (
@@ -25,15 +30,21 @@ export function Timeline({ items }: Props): JSX.Element {
{items.map((item, index) => {
const isFirst = index === 0;
const isLast = index === items.length - 1;
const done = item.done ?? true;
const dateLabel = item.getDateLabel(isBrowser ? navigator.language : 'en-US');
const timelineIcon = done ? mdiCheckboxMarkedCircle : mdiCheckboxBlankCircle;
const cardIcon = item.icon;
const classNames: string[] = [];
if (isFirst) {
classNames.push('');
}
if (isLast) {
classNames.push('rounded rounded-b-full');
}
return (
<li key={index} className={`flex min-h-24 w-[700px] max-w-[90vw] ${done ? '' : 'italic'}`}>
<li key={index} className="flex min-h-24 w-[700px] max-w-[90vw]">
<div className="md:flex justify-start w-36 mr-8 items-center dark:text-immich-dark-primary text-immich-primary hidden">
{dateLabel}
{isBrowser ? item.date.toLocaleDateString(navigator.language) : ''}
</div>
<div className={`${isFirst && 'relative top-[50%]'} ${isLast && 'relative bottom-[50%]'}`}>
<div
@@ -43,32 +54,33 @@ export function Timeline({ items }: Props): JSX.Element {
></div>
</div>
<div className="z-10 flex items-center bg-immich-primary dark:bg-immich-dark-primary border-2 border-solid rounded-full dark:text-black text-white relative top-[50%] left-[-3px] translate-y-[-50%] translate-x-[-50%] w-8 h-8 shadow-lg ">
{<Icon path={timelineIcon} size={1.25} />}
<Icon path={mdiCheckboxMarkedCircleOutline} size={1.25} />
</div>
<section className=" dark:bg-immich-dark-gray bg-immich-gray dark:border-0 border-gray-200 border border-solid rounded-2xl flex flex-row w-full gap-2 p-4 md:ml-4 my-2 hover:bg-immich-primary/10 dark:hover:bg-immich-dark-primary/10 transition-all">
<div className="flex flex-col flex-grow justify-between gap-2">
<div className="flex gap-2 items-center">
{cardIcon === 'immich' ? (
<img src="img/immich-logo.svg" height="30" />
) : (
<Icon path={cardIcon} size={1} color={item.iconColor} />
)}
<p className="m-0 mt-1 text-lg items-start flex gap-2 place-items-center content-center">
<span>{item.title}</span>
</p>
</div>
<p className="m-0 text-sm text-gray-600 dark:text-gray-300">{item.description}</p>
</div>
<div className="flex flex-col justify-between place-items-end">
<section className=" dark:bg-immich-dark-gray bg-immich-gray dark:border-0 border-gray-200 border border-solid rounded-2xl flex flex-col w-full gap-2 p-4 md:ml-4 my-2 hover:bg-immich-primary/10 dark:hover:bg-immich-dark-primary/10 transition-all">
<div className="m-0 text-lg flex w-full items-center justify-between gap-2">
<p className="m-0 items-start flex gap-2">
<Icon path={item.icon} size={1} />
<span>{item.title}</span>
</p>
<span className="dark:text-immich-dark-primary text-immich-primary">
{item.link && (
<a href={item.link.url} target="_blank" rel="noopener">
[{item.link.text}]
{item.tag ? (
<a
href={`https://github.com/immich-app/immich/releases/tag/${item.tag}`}
target="_blank"
rel="noopener"
>
[{item.release ?? item.tag}]{' '}
</a>
) : (
item.release && <span>[{item.release}]</span>
)}
</span>
<div className="md:hidden text-sm text-right">{dateLabel}</div>
</div>
<div className="md:hidden text-xs">
{`${item.dateType} - ${isBrowser ? item.date.toLocaleDateString(navigator.language) : ''}`}
</div>
<p className="m-0 text-sm text-gray-600 dark:text-gray-300">{item.description}</p>
</section>
</li>
);

View File

@@ -15,10 +15,6 @@ button {
font-family: 'Overpass', sans-serif;
}
img {
border-radius: 15px;
}
/* You can override the default Infima variables here. */
:root {
--ifm-color-primary: #4250af;
@@ -46,7 +42,7 @@ img {
}
div[class^='announcementBar_'] {
min-height: 2rem;
height: 2rem;
}
.navbar__brand .navbar__title {

View File

@@ -33,13 +33,6 @@ function HomepageHeader() {
>
Demo portal
</Link>
<Link
className="flex place-items-center place-content-center py-3 px-8 border bg-immich-dark-primary dark:bg-immich-primary rounded-full hover:no-underline text-immich-primary dark:text-immich-dark-bg font-bold uppercase"
to="https://discord.gg/D8JsnBEuKb"
>
Discord
</Link>
</div>
<img src="/img/immich-screenshots.png" alt="screenshots" width={'70%'} />
<div className="flex flex-col sm:flex-row place-items-center place-content-center mt-4 gap-1">

File diff suppressed because it is too large Load Diff

View File

@@ -28,4 +28,3 @@
/docs/features/search /docs/features/smart-search 301
/docs/guides/api-album-sync /docs/community-projects 301
/docs/guides/remove-offline-files /docs/community-projects 301
/milestones /roadmap 301

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -1 +1 @@
20.14
v20.12

View File

@@ -2,32 +2,40 @@ version: '3.8'
name: immich-e2e
x-server-build: &server-common
image: immich-server:latest
build:
context: ../
dockerfile: server/Dockerfile
environment:
- DB_HOSTNAME=database
- DB_USERNAME=postgres
- DB_PASSWORD=postgres
- DB_DATABASE_NAME=immich
- IMMICH_MACHINE_LEARNING_ENABLED=false
- IMMICH_METRICS=true
volumes:
- upload:/usr/src/app/upload
- ../server/test/assets:/data/assets
depends_on:
- redis
- database
services:
immich-server:
container_name: immich-e2e-server
command: ['./start.sh']
image: immich-server:latest
build:
context: ../
dockerfile: server/Dockerfile
environment:
- DB_HOSTNAME=database
- DB_USERNAME=postgres
- DB_PASSWORD=postgres
- DB_DATABASE_NAME=immich
- IMMICH_MACHINE_LEARNING_ENABLED=false
- IMMICH_METRICS=true
volumes:
- upload:/usr/src/app/upload
- ./test-assets:/test-assets
depends_on:
- redis
- database
command: ['./start.sh', 'immich']
<<: *server-common
ports:
- 2283:3001
immich-microservices:
container_name: immich-e2e-microservices
command: ['./start.sh', 'microservices']
<<: *server-common
redis:
image: redis:6.2-alpine@sha256:e31ca60b18f7e9b78b573d156702471d4eda038803c0b8e6f01559f350031e93
image: redis:6.2-alpine@sha256:84882e87b54734154586e5f8abd4dce69fe7311315e2fc6d67c29614c8de2672
database:
image: tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:90724186f0a3517cf6914295b5ab410db9ce23190a2d9d0b9dd6463e3fa298f0

432
e2e/package-lock.json generated
View File

@@ -1,17 +1,17 @@
{
"name": "immich-e2e",
"version": "1.105.1",
"version": "1.103.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "immich-e2e",
"version": "1.105.1",
"version": "1.103.1",
"license": "GNU Affero General Public License version 3",
"devDependencies": {
"@immich/cli": "file:../cli",
"@immich/sdk": "file:../open-api/typescript-sdk",
"@playwright/test": "^1.44.1",
"@playwright/test": "^1.41.2",
"@types/luxon": "^3.4.2",
"@types/node": "^20.11.17",
"@types/pg": "^8.11.0",
@@ -23,8 +23,8 @@
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-unicorn": "^53.0.0",
"exiftool-vendored": "^26.0.0",
"eslint-plugin-unicorn": "^52.0.0",
"exiftool-vendored": "^24.5.0",
"luxon": "^3.4.4",
"pg": "^8.11.3",
"pngjs": "^7.0.0",
@@ -65,7 +65,7 @@
"eslint": "^8.56.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-unicorn": "^53.0.0",
"eslint-plugin-unicorn": "^52.0.0",
"mock-fs": "^5.2.0",
"prettier": "^3.2.5",
"prettier-plugin-organize-imports": "^3.2.4",
@@ -81,14 +81,14 @@
},
"../open-api/typescript-sdk": {
"name": "@immich/sdk",
"version": "1.105.1",
"version": "1.103.1",
"dev": true,
"license": "GNU Affero General Public License version 3",
"dependencies": {
"@oazapfts/runtime": "^1.0.2"
"@oazapfts/runtime": "^1.0.3"
},
"devDependencies": {
"@types/node": "^20.12.13",
"@types/node": "^20.11.0",
"typescript": "^5.3.3"
}
},
@@ -208,9 +208,9 @@
}
},
"node_modules/@babel/helper-validator-identifier": {
"version": "7.24.5",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.5.tgz",
"integrity": "sha512-3q93SSKX2TWCG30M2G2kwaKeTYgEUp5Snjuj8qm729SObL6nbtUldAi37qbxkD5gg3xnBio+f9nqpSepGZMvxA==",
"version": "7.22.20",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz",
"integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==",
"dev": true,
"engines": {
"node": ">=6.9.0"
@@ -953,9 +953,9 @@
}
},
"node_modules/@photostructure/tz-lookup": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/@photostructure/tz-lookup/-/tz-lookup-10.0.0.tgz",
"integrity": "sha512-8ZAjoj/irCuvUlyEinQ/HB6A8hP3bD1dgTOZvfl1b9nAwqniutFDHOQRcGM6Crea68bOwPj010f0Z4KkmuLHEA==",
"version": "9.0.2",
"resolved": "https://registry.npmjs.org/@photostructure/tz-lookup/-/tz-lookup-9.0.2.tgz",
"integrity": "sha512-H8+tTt7ilJNkFyb+QgPnLEGUjQzGwiMb9n7lwRZNBgSKL3VZs9AkjI1E//FcwPjNafwAH932U92+xTqJiF3Bbw==",
"dev": true
},
"node_modules/@pkgr/core": {
@@ -971,12 +971,12 @@
}
},
"node_modules/@playwright/test": {
"version": "1.44.1",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.44.1.tgz",
"integrity": "sha512-1hZ4TNvD5z9VuhNJ/walIjvMVvYkZKf71axoF/uiAqpntQJXpG64dlXhoDXE3OczPuTuvjf/M5KWFg5VAVUS3Q==",
"version": "1.43.1",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.43.1.tgz",
"integrity": "sha512-HgtQzFgNEEo4TE22K/X7sYTYNqEMMTZmFS8kTq6m8hXj+m1D8TgwgIbumHddJa9h4yl4GkKb8/bgAl2+g7eDgA==",
"dev": true,
"dependencies": {
"playwright": "1.44.1"
"playwright": "1.43.1"
},
"bin": {
"playwright": "cli.js"
@@ -1217,6 +1217,12 @@
"integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==",
"dev": true
},
"node_modules/@types/json-schema": {
"version": "7.0.15",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
"dev": true
},
"node_modules/@types/luxon": {
"version": "3.4.2",
"resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.4.2.tgz",
@@ -1230,11 +1236,10 @@
"dev": true
},
"node_modules/@types/node": {
"version": "20.12.13",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.13.tgz",
"integrity": "sha512-gBGeanV41c1L171rR7wjbMiEpEI/l5XFQdLLfhr/REwpgDy/4U8y89+i8kRiLzDyZdOkXh+cRaTetUnCYutoXA==",
"version": "20.12.7",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.7.tgz",
"integrity": "sha512-wq0cICSkRLVaf3UGLMGItu/PtdY7oaXaI/RVU+xliKVOtRna3PRY57ZDfztpDL0n11vfymMUnXv8QwYCO7L1wg==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~5.26.4"
}
@@ -1246,9 +1251,9 @@
"dev": true
},
"node_modules/@types/pg": {
"version": "8.11.6",
"resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.11.6.tgz",
"integrity": "sha512-/2WmmBXHLsfRqzfHW7BNZ8SbYzE8OSk7i3WjFYvfgRHj7S1xj+16Je5fUKv3lVdVzk/zn9TXOqf+avFCFIE0yQ==",
"version": "8.11.5",
"resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.11.5.tgz",
"integrity": "sha512-2xMjVviMxneZHDHX5p5S6tsRRs7TpDHeeK7kTTMe/kAC/mRRNjWHjZg0rkiY+e17jXSZV3zJYDxXV8Cy72/Vuw==",
"dev": true,
"dependencies": {
"@types/node": "*",
@@ -1314,14 +1319,20 @@
}
},
"node_modules/@types/pngjs": {
"version": "6.0.5",
"resolved": "https://registry.npmjs.org/@types/pngjs/-/pngjs-6.0.5.tgz",
"integrity": "sha512-0k5eKfrA83JOZPppLtS2C7OUtyNAl2wKNxfyYl9Q5g9lPkgBl/9hNyAu6HuEH2J4XmIv2znEpkDd0SaZVxW6iQ==",
"version": "6.0.4",
"resolved": "https://registry.npmjs.org/@types/pngjs/-/pngjs-6.0.4.tgz",
"integrity": "sha512-atAK9xLKOnxiuArxcHovmnOUUGBZOQ3f0vCf43FnoKs6XnqiambT1kkJWmdo71IR+BoXSh+CueeFR0GfH3dTlQ==",
"dev": true,
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/semver": {
"version": "7.5.8",
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz",
"integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==",
"dev": true
},
"node_modules/@types/superagent": {
"version": "8.1.3",
"resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-8.1.3.tgz",
@@ -1344,20 +1355,21 @@
}
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "7.11.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.11.0.tgz",
"integrity": "sha512-P+qEahbgeHW4JQ/87FuItjBj8O3MYv5gELDzr8QaQ7fsll1gSMTYb6j87MYyxwf3DtD7uGFB9ShwgmCJB5KmaQ==",
"version": "7.7.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.7.1.tgz",
"integrity": "sha512-KwfdWXJBOviaBVhxO3p5TJiLpNuh2iyXyjmWN0f1nU87pwyvfS0EmjC6ukQVYVFJd/K1+0NWGPDXiyEyQorn0Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/regexpp": "^4.10.0",
"@typescript-eslint/scope-manager": "7.11.0",
"@typescript-eslint/type-utils": "7.11.0",
"@typescript-eslint/utils": "7.11.0",
"@typescript-eslint/visitor-keys": "7.11.0",
"@typescript-eslint/scope-manager": "7.7.1",
"@typescript-eslint/type-utils": "7.7.1",
"@typescript-eslint/utils": "7.7.1",
"@typescript-eslint/visitor-keys": "7.7.1",
"debug": "^4.3.4",
"graphemer": "^1.4.0",
"ignore": "^5.3.1",
"natural-compare": "^1.4.0",
"semver": "^7.6.0",
"ts-api-utils": "^1.3.0"
},
"engines": {
@@ -1378,16 +1390,15 @@
}
},
"node_modules/@typescript-eslint/parser": {
"version": "7.11.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.11.0.tgz",
"integrity": "sha512-yimw99teuaXVWsBcPO1Ais02kwJ1jmNA1KxE7ng0aT7ndr1pT1wqj0OJnsYVGKKlc4QJai86l/025L6z8CljOg==",
"version": "7.7.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.7.1.tgz",
"integrity": "sha512-vmPzBOOtz48F6JAGVS/kZYk4EkXao6iGrD838sp1w3NQQC0W8ry/q641KU4PrG7AKNAf56NOcR8GOpH8l9FPCw==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"@typescript-eslint/scope-manager": "7.11.0",
"@typescript-eslint/types": "7.11.0",
"@typescript-eslint/typescript-estree": "7.11.0",
"@typescript-eslint/visitor-keys": "7.11.0",
"@typescript-eslint/scope-manager": "7.7.1",
"@typescript-eslint/types": "7.7.1",
"@typescript-eslint/typescript-estree": "7.7.1",
"@typescript-eslint/visitor-keys": "7.7.1",
"debug": "^4.3.4"
},
"engines": {
@@ -1407,14 +1418,13 @@
}
},
"node_modules/@typescript-eslint/scope-manager": {
"version": "7.11.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.11.0.tgz",
"integrity": "sha512-27tGdVEiutD4POirLZX4YzT180vevUURJl4wJGmm6TrQoiYwuxTIY98PBp6L2oN+JQxzE0URvYlzJaBHIekXAw==",
"version": "7.7.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.7.1.tgz",
"integrity": "sha512-PytBif2SF+9SpEUKynYn5g1RHFddJUcyynGpztX3l/ik7KmZEv19WCMhUBkHXPU9es/VWGD3/zg3wg90+Dh2rA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "7.11.0",
"@typescript-eslint/visitor-keys": "7.11.0"
"@typescript-eslint/types": "7.7.1",
"@typescript-eslint/visitor-keys": "7.7.1"
},
"engines": {
"node": "^18.18.0 || >=20.0.0"
@@ -1425,14 +1435,13 @@
}
},
"node_modules/@typescript-eslint/type-utils": {
"version": "7.11.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.11.0.tgz",
"integrity": "sha512-WmppUEgYy+y1NTseNMJ6mCFxt03/7jTOy08bcg7bxJJdsM4nuhnchyBbE8vryveaJUf62noH7LodPSo5Z0WUCg==",
"version": "7.7.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.7.1.tgz",
"integrity": "sha512-ZksJLW3WF7o75zaBPScdW1Gbkwhd/lyeXGf1kQCxJaOeITscoSl0MjynVvCzuV5boUz/3fOI06Lz8La55mu29Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/typescript-estree": "7.11.0",
"@typescript-eslint/utils": "7.11.0",
"@typescript-eslint/typescript-estree": "7.7.1",
"@typescript-eslint/utils": "7.7.1",
"debug": "^4.3.4",
"ts-api-utils": "^1.3.0"
},
@@ -1453,11 +1462,10 @@
}
},
"node_modules/@typescript-eslint/types": {
"version": "7.11.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.11.0.tgz",
"integrity": "sha512-MPEsDRZTyCiXkD4vd3zywDCifi7tatc4K37KqTprCvaXptP7Xlpdw0NR2hRJTetG5TxbWDB79Ys4kLmHliEo/w==",
"version": "7.7.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.7.1.tgz",
"integrity": "sha512-AmPmnGW1ZLTpWa+/2omPrPfR7BcbUU4oha5VIbSbS1a1Tv966bklvLNXxp3mrbc+P2j4MNOTfDffNsk4o0c6/w==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^18.18.0 || >=20.0.0"
},
@@ -1467,14 +1475,13 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
"version": "7.11.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.11.0.tgz",
"integrity": "sha512-cxkhZ2C/iyi3/6U9EPc5y+a6csqHItndvN/CzbNXTNrsC3/ASoYQZEt9uMaEp+xFNjasqQyszp5TumAVKKvJeQ==",
"version": "7.7.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.7.1.tgz",
"integrity": "sha512-CXe0JHCXru8Fa36dteXqmH2YxngKJjkQLjxzoj6LYwzZ7qZvgsLSc+eqItCrqIop8Vl2UKoAi0StVWu97FQZIQ==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"@typescript-eslint/types": "7.11.0",
"@typescript-eslint/visitor-keys": "7.11.0",
"@typescript-eslint/types": "7.7.1",
"@typescript-eslint/visitor-keys": "7.7.1",
"debug": "^4.3.4",
"globby": "^11.1.0",
"is-glob": "^4.0.3",
@@ -1500,7 +1507,6 @@
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0"
}
@@ -1510,7 +1516,6 @@
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz",
"integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^2.0.1"
},
@@ -1522,16 +1527,18 @@
}
},
"node_modules/@typescript-eslint/utils": {
"version": "7.11.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.11.0.tgz",
"integrity": "sha512-xlAWwPleNRHwF37AhrZurOxA1wyXowW4PqVXZVUNCLjB48CqdPJoJWkrpH2nij9Q3Lb7rtWindtoXwxjxlKKCA==",
"version": "7.7.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.7.1.tgz",
"integrity": "sha512-QUvBxPEaBXf41ZBbaidKICgVL8Hin0p6prQDu6bbetWo39BKbWJxRsErOzMNT1rXvTll+J7ChrbmMCXM9rsvOQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.4.0",
"@typescript-eslint/scope-manager": "7.11.0",
"@typescript-eslint/types": "7.11.0",
"@typescript-eslint/typescript-estree": "7.11.0"
"@types/json-schema": "^7.0.15",
"@types/semver": "^7.5.8",
"@typescript-eslint/scope-manager": "7.7.1",
"@typescript-eslint/types": "7.7.1",
"@typescript-eslint/typescript-estree": "7.7.1",
"semver": "^7.6.0"
},
"engines": {
"node": "^18.18.0 || >=20.0.0"
@@ -1545,13 +1552,12 @@
}
},
"node_modules/@typescript-eslint/visitor-keys": {
"version": "7.11.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.11.0.tgz",
"integrity": "sha512-7syYk4MzjxTEk0g/w3iqtgxnFQspDJfn6QKD36xMuuhTzjcxY7F8EmBLnALjVyaOF1/bVocu3bS/2/F7rXrveQ==",
"version": "7.7.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.7.1.tgz",
"integrity": "sha512-gBL3Eq25uADw1LQ9kVpf3hRM+DWzs0uZknHYK3hq4jcTPqVCClHGDnB6UUUV2SFeBeA4KWHWbbLqmbGcZ4FYbw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "7.11.0",
"@typescript-eslint/types": "7.7.1",
"eslint-visitor-keys": "^3.4.3"
},
"engines": {
@@ -1569,9 +1575,9 @@
"dev": true
},
"node_modules/@vitest/coverage-v8": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-1.6.0.tgz",
"integrity": "sha512-KvapcbMY/8GYIG0rlwwOKCVNRc0OL20rrhFkg/CHNzncV03TE2XWvO5w9uZYoxNiMEBacAJt3unSOiZ7svePew==",
"version": "1.5.2",
"resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-1.5.2.tgz",
"integrity": "sha512-QJqxRnbCwNtbbegK9E93rBmhN3dbfG1bC/o52Bqr0zGCYhQzwgwvrJBG7Q8vw3zilX6Ryy6oa/mkZku2lLJx1Q==",
"dev": true,
"dependencies": {
"@ampproject/remapping": "^2.2.1",
@@ -1592,17 +1598,17 @@
"url": "https://opencollective.com/vitest"
},
"peerDependencies": {
"vitest": "1.6.0"
"vitest": "1.5.2"
}
},
"node_modules/@vitest/expect": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.6.0.tgz",
"integrity": "sha512-ixEvFVQjycy/oNgHjqsL6AZCDduC+tflRluaHIzKIsdbzkLn2U/iBnVeJwB6HsIjQBdfMR8Z0tRxKUsvFJEeWQ==",
"version": "1.5.2",
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.5.2.tgz",
"integrity": "sha512-rf7MTD1WCoDlN3FfYJ9Llfp0PbdtOMZ3FIF0AVkDnKbp3oiMW1c8AmvRZBcqbAhDUAvF52e9zx4WQM1r3oraVA==",
"dev": true,
"dependencies": {
"@vitest/spy": "1.6.0",
"@vitest/utils": "1.6.0",
"@vitest/spy": "1.5.2",
"@vitest/utils": "1.5.2",
"chai": "^4.3.10"
},
"funding": {
@@ -1610,12 +1616,12 @@
}
},
"node_modules/@vitest/runner": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.6.0.tgz",
"integrity": "sha512-P4xgwPjwesuBiHisAVz/LSSZtDjOTPYZVmNAnpHHSR6ONrf8eCJOFRvUwdHn30F5M1fxhqtl7QZQUk2dprIXAg==",
"version": "1.5.2",
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.5.2.tgz",
"integrity": "sha512-7IJ7sJhMZrqx7HIEpv3WrMYcq8ZNz9L6alo81Y6f8hV5mIE6yVZsFoivLZmr0D777klm1ReqonE9LyChdcmw6g==",
"dev": true,
"dependencies": {
"@vitest/utils": "1.6.0",
"@vitest/utils": "1.5.2",
"p-limit": "^5.0.0",
"pathe": "^1.1.1"
},
@@ -1624,9 +1630,9 @@
}
},
"node_modules/@vitest/snapshot": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.6.0.tgz",
"integrity": "sha512-+Hx43f8Chus+DCmygqqfetcAZrDJwvTj0ymqjQq4CvmpKFSTVteEOBzCusu1x2tt4OJcvBflyHUE0DZSLgEMtQ==",
"version": "1.5.2",
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.5.2.tgz",
"integrity": "sha512-CTEp/lTYos8fuCc9+Z55Ga5NVPKUgExritjF5VY7heRFUfheoAqBneUlvXSUJHUZPjnPmyZA96yLRJDP1QATFQ==",
"dev": true,
"dependencies": {
"magic-string": "^0.30.5",
@@ -1638,9 +1644,9 @@
}
},
"node_modules/@vitest/spy": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.6.0.tgz",
"integrity": "sha512-leUTap6B/cqi/bQkXUu6bQV5TZPx7pmMBKBQiI0rJA8c3pB56ZsaTbREnF7CJfmvAS4V2cXIBAh/3rVwrrCYgw==",
"version": "1.5.2",
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.5.2.tgz",
"integrity": "sha512-xCcPvI8JpCtgikT9nLpHPL1/81AYqZy1GCy4+MCHBE7xi8jgsYkULpW5hrx5PGLgOQjUpb6fd15lqcriJ40tfQ==",
"dev": true,
"dependencies": {
"tinyspy": "^2.2.0"
@@ -1650,9 +1656,9 @@
}
},
"node_modules/@vitest/utils": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.6.0.tgz",
"integrity": "sha512-21cPiuGMoMZwiOHa2i4LXkMkMkCGzA+MVFV70jRwHo95dL4x/ts5GZhML1QWuy7yfp3WzK3lRvZi3JnXTYqrBw==",
"version": "1.5.2",
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.5.2.tgz",
"integrity": "sha512-sWOmyofuXLJ85VvXNsroZur7mOJGiQeM0JN3/0D1uU8U9bGFM69X1iqHaRXl6R8BwaLY6yPCogP257zxTzkUdA==",
"dev": true,
"dependencies": {
"diff-sequences": "^29.6.3",
@@ -1779,7 +1785,6 @@
"resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz",
"integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
@@ -1831,13 +1836,12 @@
}
},
"node_modules/braces": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
"dev": true,
"license": "MIT",
"dependencies": {
"fill-range": "^7.1.1"
"fill-range": "^7.0.1"
},
"engines": {
"node": ">=8"
@@ -2117,12 +2121,12 @@
"dev": true
},
"node_modules/core-js-compat": {
"version": "3.37.1",
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.37.1.tgz",
"integrity": "sha512-9TNiImhKvQqSUkOvk/mMRZzOANTiEVC7WaBNhHcKM7x+/5E1l5NvsysR19zuDQScE8k+kfQXWRN3AtS/eOSHpg==",
"version": "3.36.0",
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.36.0.tgz",
"integrity": "sha512-iV9Pd/PsgjNWBXeq8XRtWVSgz2tKAfhfvBs7qxYty+RlRd+OCksaWmOnc4JKrTc1cToXL1N0s3l/vwlxPtdElw==",
"dev": true,
"dependencies": {
"browserslist": "^4.23.0"
"browserslist": "^4.22.3"
},
"funding": {
"type": "opencollective",
@@ -2243,7 +2247,6 @@
"resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
"integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==",
"dev": true,
"license": "MIT",
"dependencies": {
"path-type": "^4.0.0"
},
@@ -2484,17 +2487,17 @@
}
},
"node_modules/eslint-plugin-unicorn": {
"version": "53.0.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-53.0.0.tgz",
"integrity": "sha512-kuTcNo9IwwUCfyHGwQFOK/HjJAYzbODHN3wP0PgqbW+jbXqpNWxNVpVhj2tO9SixBwuAdmal8rVcWKBxwFnGuw==",
"version": "52.0.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-52.0.0.tgz",
"integrity": "sha512-1Yzm7/m+0R4djH0tjDjfVei/ju2w3AzUGjG6q8JnuNIL5xIwsflyCooW5sfBvQp2pMYQFSWWCFONsjCax1EHng==",
"dev": true,
"dependencies": {
"@babel/helper-validator-identifier": "^7.24.5",
"@babel/helper-validator-identifier": "^7.22.20",
"@eslint-community/eslint-utils": "^4.4.0",
"@eslint/eslintrc": "^3.0.2",
"@eslint/eslintrc": "^2.1.4",
"ci-info": "^4.0.0",
"clean-regexp": "^1.0.0",
"core-js-compat": "^3.37.0",
"core-js-compat": "^3.34.0",
"esquery": "^1.5.0",
"indent-string": "^4.0.0",
"is-builtin-module": "^3.2.1",
@@ -2503,11 +2506,11 @@
"read-pkg-up": "^7.0.1",
"regexp-tree": "^0.1.27",
"regjsparser": "^0.10.0",
"semver": "^7.6.1",
"semver": "^7.5.4",
"strip-indent": "^3.0.0"
},
"engines": {
"node": ">=18.18"
"node": ">=16"
},
"funding": {
"url": "https://github.com/sindresorhus/eslint-plugin-unicorn?sponsor=1"
@@ -2516,70 +2519,6 @@
"eslint": ">=8.56.0"
}
},
"node_modules/eslint-plugin-unicorn/node_modules/@eslint/eslintrc": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.0.2.tgz",
"integrity": "sha512-wV19ZEGEMAC1eHgrS7UQPqsdEiCIbTKTasEfcXAigzoXICcqZSjBZEHlZwNVvKg6UBCjSlos84XiLqsRJnIcIg==",
"dev": true,
"dependencies": {
"ajv": "^6.12.4",
"debug": "^4.3.2",
"espree": "^10.0.1",
"globals": "^14.0.0",
"ignore": "^5.2.0",
"import-fresh": "^3.2.1",
"js-yaml": "^4.1.0",
"minimatch": "^3.1.2",
"strip-json-comments": "^3.1.1"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"url": "https://opencollective.com/eslint"
}
},
"node_modules/eslint-plugin-unicorn/node_modules/eslint-visitor-keys": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz",
"integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==",
"dev": true,
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"url": "https://opencollective.com/eslint"
}
},
"node_modules/eslint-plugin-unicorn/node_modules/espree": {
"version": "10.0.1",
"resolved": "https://registry.npmjs.org/espree/-/espree-10.0.1.tgz",
"integrity": "sha512-MWkrWZbJsL2UwnjxTX3gG8FneachS/Mwg7tdGXce011sJd5b0JG54vat5KHnfSBODZ3Wvzd2WnjxyzsRoVv+ww==",
"dev": true,
"dependencies": {
"acorn": "^8.11.3",
"acorn-jsx": "^5.3.2",
"eslint-visitor-keys": "^4.0.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"url": "https://opencollective.com/eslint"
}
},
"node_modules/eslint-plugin-unicorn/node_modules/globals": {
"version": "14.0.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz",
"integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==",
"dev": true,
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/eslint-scope": {
"version": "7.2.2",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz",
@@ -2700,40 +2639,37 @@
}
},
"node_modules/exiftool-vendored": {
"version": "26.1.0",
"resolved": "https://registry.npmjs.org/exiftool-vendored/-/exiftool-vendored-26.1.0.tgz",
"integrity": "sha512-Bhy2Ia86Agt3+PbJJhWeVMqJNXl74XJ0Oygef5F5uCL13fTxlmF8dECHiChyx8bBc3sxIw+2Q3ehWunJh3bs6w==",
"version": "24.6.0",
"resolved": "https://registry.npmjs.org/exiftool-vendored/-/exiftool-vendored-24.6.0.tgz",
"integrity": "sha512-jGjsoeYmR9VUrlZn0j1wcxMVi5y8C7A4FAa4vm3/l7ThT8d0f+jRcBqtdjaf+P5Ds/F4OgUq+ee/fRVhLy2DrA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@photostructure/tz-lookup": "^10.0.0",
"@photostructure/tz-lookup": "^9.0.2",
"@types/luxon": "^3.4.2",
"batch-cluster": "^13.0.0",
"he": "^1.2.0",
"luxon": "^3.4.4"
},
"optionalDependencies": {
"exiftool-vendored.exe": "12.85.0",
"exiftool-vendored.pl": "12.85.0"
"exiftool-vendored.exe": "12.78.0",
"exiftool-vendored.pl": "12.78.0"
}
},
"node_modules/exiftool-vendored.exe": {
"version": "12.85.0",
"resolved": "https://registry.npmjs.org/exiftool-vendored.exe/-/exiftool-vendored.exe-12.85.0.tgz",
"integrity": "sha512-rWsKVp9oXsS79S3bfCNXKeEo4av0xcd7slk/TfPpCa5pojg8ZVXSVfPZMAAlhOuK63YXrKN/e3jRNReeGP+2Gw==",
"version": "12.78.0",
"resolved": "https://registry.npmjs.org/exiftool-vendored.exe/-/exiftool-vendored.exe-12.78.0.tgz",
"integrity": "sha512-eMN7L67sb89xi8sN7INPg19uwa1KibG2oOyGcfOvB47h+1hzmGgivVu/SZIMeOToVIbLRwUl+AFwLYSTNXsJEg==",
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/exiftool-vendored.pl": {
"version": "12.85.0",
"resolved": "https://registry.npmjs.org/exiftool-vendored.pl/-/exiftool-vendored.pl-12.85.0.tgz",
"integrity": "sha512-AelZQCCfl0a0g7PYx90TqbNGlSu2zDbRfCTjGw6bBBYnJF0NUfUWVhTpa8XGe2lHx1KYikH8AkJaey3esAxMAg==",
"version": "12.78.0",
"resolved": "https://registry.npmjs.org/exiftool-vendored.pl/-/exiftool-vendored.pl-12.78.0.tgz",
"integrity": "sha512-K8j9NgxRpTFskFuXEl0AGsc692yYyThe4i3SXgx7xc0fu/vwD2c7tRGljkEtvaweYnMmfrF4DhCpuTu0aux6sg==",
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"!win32"
@@ -2756,7 +2692,6 @@
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz",
"integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==",
"dev": true,
"license": "MIT",
"dependencies": {
"@nodelib/fs.stat": "^2.0.2",
"@nodelib/fs.walk": "^1.2.3",
@@ -2773,7 +2708,6 @@
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
"dev": true,
"license": "ISC",
"dependencies": {
"is-glob": "^4.0.1"
},
@@ -2821,11 +2755,10 @@
}
},
"node_modules/fill-range": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"to-regex-range": "^5.0.1"
},
@@ -3068,7 +3001,6 @@
"resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz",
"integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==",
"dev": true,
"license": "MIT",
"dependencies": {
"array-union": "^2.1.0",
"dir-glob": "^3.0.1",
@@ -3344,7 +3276,6 @@
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.12.0"
}
@@ -3560,6 +3491,18 @@
"get-func-name": "^2.0.1"
}
},
"node_modules/lru-cache": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
"dev": true,
"dependencies": {
"yallist": "^4.0.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/luxon": {
"version": "3.4.4",
"resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.4.tgz",
@@ -3618,7 +3561,6 @@
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
"integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 8"
}
@@ -3633,13 +3575,12 @@
}
},
"node_modules/micromatch": {
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz",
"integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==",
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz",
"integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==",
"dev": true,
"license": "MIT",
"dependencies": {
"braces": "^3.0.3",
"braces": "^3.0.2",
"picomatch": "^2.3.1"
},
"engines": {
@@ -4106,7 +4047,6 @@
"resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
"integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
@@ -4235,7 +4175,6 @@
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8.6"
},
@@ -4255,12 +4194,12 @@
}
},
"node_modules/playwright": {
"version": "1.44.1",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.44.1.tgz",
"integrity": "sha512-qr/0UJ5CFAtloI3avF95Y0L1xQo6r3LQArLIg/z/PoGJ6xa+EwzrwO5lpNr/09STxdHuUoP2mvuELJS+hLdtgg==",
"version": "1.43.1",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.43.1.tgz",
"integrity": "sha512-V7SoH0ai2kNt1Md9E3Gwas5B9m8KR2GVvwZnAI6Pg0m3sh7UvgiYhRrhsziCmqMJNouPckiOhk8T+9bSAK0VIA==",
"dev": true,
"dependencies": {
"playwright-core": "1.44.1"
"playwright-core": "1.43.1"
},
"bin": {
"playwright": "cli.js"
@@ -4273,9 +4212,9 @@
}
},
"node_modules/playwright-core": {
"version": "1.44.1",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.44.1.tgz",
"integrity": "sha512-wh0JWtYTrhv1+OSsLPgFzGzt67Y7BE/ZS3jEqgGBlp2ppp1ZDj8c+9IARNW4dwf1poq5MgHreEM2KV/GuR4cFA==",
"version": "1.43.1",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.43.1.tgz",
"integrity": "sha512-EI36Mto2Vrx6VF7rm708qSnesVQKbxEWvPrfA1IPY6HgczBplDx7ENtx+K2n4kJ41sLLkuGfmb0ZLSSXlDhqPg==",
"dev": true,
"bin": {
"playwright-core": "cli.js"
@@ -4771,10 +4710,13 @@
]
},
"node_modules/semver": {
"version": "7.6.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz",
"integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==",
"version": "7.6.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz",
"integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==",
"dev": true,
"dependencies": {
"lru-cache": "^6.0.0"
},
"bin": {
"semver": "bin/semver.js"
},
@@ -4867,7 +4809,6 @@
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
"integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
@@ -5194,7 +5135,6 @@
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"is-number": "^7.0.0"
},
@@ -5354,9 +5294,9 @@
}
},
"node_modules/vite": {
"version": "5.2.11",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.2.11.tgz",
"integrity": "sha512-HndV31LWW05i1BLPMUCE1B9E9GFbOu1MbenhS58FuK6owSO5qHm7GiCotrNY1YE5rMeQSFBGmT5ZaLEjFizgiQ==",
"version": "5.2.10",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.2.10.tgz",
"integrity": "sha512-PAzgUZbP7msvQvqdSD+ErD5qGnSFiGOoWmV5yAKUEI0kdhjbH6nMWVyZQC/hSc4aXwc0oJ9aEdIiF9Oje0JFCw==",
"dev": true,
"dependencies": {
"esbuild": "^0.20.1",
@@ -5409,9 +5349,9 @@
}
},
"node_modules/vite-node": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.6.0.tgz",
"integrity": "sha512-de6HJgzC+TFzOu0NTC4RAIsyf/DY/ibWDYQUcuEA84EMHhcefTUGkjFHKKEJhQN4A+6I0u++kr3l36ZF2d7XRw==",
"version": "1.5.2",
"resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.5.2.tgz",
"integrity": "sha512-Y8p91kz9zU+bWtF7HGt6DVw2JbhyuB2RlZix3FPYAYmUyZ3n7iTp8eSyLyY6sxtPegvxQtmlTMhfPhUfCUF93A==",
"dev": true,
"dependencies": {
"cac": "^6.7.14",
@@ -5445,16 +5385,16 @@
}
},
"node_modules/vitest": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/vitest/-/vitest-1.6.0.tgz",
"integrity": "sha512-H5r/dN06swuFnzNFhq/dnz37bPXnq8xB2xB5JOVk8K09rUtoeNN+LHWkoQ0A/i3hvbUKKcCei9KpbxqHMLhLLA==",
"version": "1.5.2",
"resolved": "https://registry.npmjs.org/vitest/-/vitest-1.5.2.tgz",
"integrity": "sha512-l9gwIkq16ug3xY7BxHwcBQovLZG75zZL0PlsiYQbf76Rz6QGs54416UWMtC0jXeihvHvcHrf2ROEjkQRVpoZYw==",
"dev": true,
"dependencies": {
"@vitest/expect": "1.6.0",
"@vitest/runner": "1.6.0",
"@vitest/snapshot": "1.6.0",
"@vitest/spy": "1.6.0",
"@vitest/utils": "1.6.0",
"@vitest/expect": "1.5.2",
"@vitest/runner": "1.5.2",
"@vitest/snapshot": "1.5.2",
"@vitest/spy": "1.5.2",
"@vitest/utils": "1.5.2",
"acorn-walk": "^8.3.2",
"chai": "^4.3.10",
"debug": "^4.3.4",
@@ -5468,7 +5408,7 @@
"tinybench": "^2.5.1",
"tinypool": "^0.8.3",
"vite": "^5.0.0",
"vite-node": "1.6.0",
"vite-node": "1.5.2",
"why-is-node-running": "^2.2.2"
},
"bin": {
@@ -5483,8 +5423,8 @@
"peerDependencies": {
"@edge-runtime/vm": "*",
"@types/node": "^18.0.0 || >=20.0.0",
"@vitest/browser": "1.6.0",
"@vitest/ui": "1.6.0",
"@vitest/browser": "1.5.2",
"@vitest/ui": "1.5.2",
"happy-dom": "*",
"jsdom": "*"
},

View File

@@ -1,6 +1,6 @@
{
"name": "immich-e2e",
"version": "1.105.1",
"version": "1.103.1",
"description": "",
"main": "index.js",
"type": "module",
@@ -21,7 +21,7 @@
"devDependencies": {
"@immich/cli": "file:../cli",
"@immich/sdk": "file:../open-api/typescript-sdk",
"@playwright/test": "^1.44.1",
"@playwright/test": "^1.41.2",
"@types/luxon": "^3.4.2",
"@types/node": "^20.11.17",
"@types/pg": "^8.11.0",
@@ -33,8 +33,8 @@
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-unicorn": "^53.0.0",
"exiftool-vendored": "^26.0.0",
"eslint-plugin-unicorn": "^52.0.0",
"exiftool-vendored": "^24.5.0",
"luxon": "^3.4.4",
"pg": "^8.11.3",
"pngjs": "^7.0.0",
@@ -47,6 +47,6 @@
"vitest": "^1.3.0"
},
"volta": {
"node": "20.14.0"
"node": "20.12.2"
}
}

View File

@@ -1,8 +1,7 @@
import {
ActivityCreateDto,
AlbumResponseDto,
AlbumUserRole,
AssetMediaResponseDto,
AssetFileUploadResponseDto,
LoginResponseDto,
ReactionType,
createActivity as create,
@@ -14,10 +13,10 @@ import { app, asBearerAuth, utils } from 'src/utils';
import request from 'supertest';
import { beforeAll, beforeEach, describe, expect, it } from 'vitest';
describe('/activities', () => {
describe('/activity', () => {
let admin: LoginResponseDto;
let nonOwner: LoginResponseDto;
let asset: AssetMediaResponseDto;
let asset: AssetFileUploadResponseDto;
let album: AlbumResponseDto;
const createActivity = (dto: ActivityCreateDto, accessToken?: string) =>
@@ -34,7 +33,7 @@ describe('/activities', () => {
createAlbumDto: {
albumName: 'Album 1',
assetIds: [asset.id],
albumUsers: [{ userId: nonOwner.userId, role: AlbumUserRole.Editor }],
sharedWithUserIds: [nonOwner.userId],
},
},
{ headers: asBearerAuth(admin.accessToken) },
@@ -45,24 +44,22 @@ describe('/activities', () => {
await utils.resetDatabase(['activity']);
});
describe('GET /activities', () => {
describe('GET /activity', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get('/activities');
const { status, body } = await request(app).get('/activity');
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should require an albumId', async () => {
const { status, body } = await request(app)
.get('/activities')
.set('Authorization', `Bearer ${admin.accessToken}`);
const { status, body } = await request(app).get('/activity').set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toEqual(400);
expect(body).toEqual(errorDto.badRequest(expect.arrayContaining(['albumId must be a UUID'])));
});
it('should reject an invalid albumId', async () => {
const { status, body } = await request(app)
.get('/activities')
.get('/activity')
.query({ albumId: uuidDto.invalid })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toEqual(400);
@@ -71,7 +68,7 @@ describe('/activities', () => {
it('should reject an invalid assetId', async () => {
const { status, body } = await request(app)
.get('/activities')
.get('/activity')
.query({ albumId: uuidDto.notFound, assetId: uuidDto.invalid })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toEqual(400);
@@ -80,7 +77,7 @@ describe('/activities', () => {
it('should start off empty', async () => {
const { status, body } = await request(app)
.get('/activities')
.get('/activity')
.query({ albumId: album.id })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(body).toEqual([]);
@@ -104,7 +101,7 @@ describe('/activities', () => {
]);
const { status, body } = await request(app)
.get('/activities')
.get('/activity')
.query({ albumId: album.id })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toEqual(200);
@@ -123,7 +120,7 @@ describe('/activities', () => {
]);
const { status, body } = await request(app)
.get('/activities')
.get('/activity')
.query({ albumId: album.id, type: 'comment' })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toEqual(200);
@@ -142,7 +139,7 @@ describe('/activities', () => {
]);
const { status, body } = await request(app)
.get('/activities')
.get('/activity')
.query({ albumId: album.id, type: 'like' })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toEqual(200);
@@ -154,7 +151,7 @@ describe('/activities', () => {
const reaction = await createActivity({ albumId: album.id, type: ReactionType.Like });
const response1 = await request(app)
.get('/activities')
.get('/activity')
.query({ albumId: album.id, userId: uuidDto.notFound })
.set('Authorization', `Bearer ${admin.accessToken}`);
@@ -162,7 +159,7 @@ describe('/activities', () => {
expect(response1.body.length).toBe(0);
const response2 = await request(app)
.get('/activities')
.get('/activity')
.query({ albumId: album.id, userId: admin.userId })
.set('Authorization', `Bearer ${admin.accessToken}`);
@@ -182,7 +179,7 @@ describe('/activities', () => {
]);
const { status, body } = await request(app)
.get('/activities')
.get('/activity')
.query({ albumId: album.id, assetId: asset.id })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toEqual(200);
@@ -191,16 +188,16 @@ describe('/activities', () => {
});
});
describe('POST /activities', () => {
describe('POST /activity', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).post('/activities');
const { status, body } = await request(app).post('/activity');
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should require an albumId', async () => {
const { status, body } = await request(app)
.post('/activities')
.post('/activity')
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ albumId: uuidDto.invalid });
expect(status).toEqual(400);
@@ -209,7 +206,7 @@ describe('/activities', () => {
it('should require a comment when type is comment', async () => {
const { status, body } = await request(app)
.post('/activities')
.post('/activity')
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ albumId: uuidDto.notFound, type: 'comment', comment: null });
expect(status).toEqual(400);
@@ -218,7 +215,7 @@ describe('/activities', () => {
it('should add a comment to an album', async () => {
const { status, body } = await request(app)
.post('/activities')
.post('/activity')
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({
albumId: album.id,
@@ -238,7 +235,7 @@ describe('/activities', () => {
it('should add a like to an album', async () => {
const { status, body } = await request(app)
.post('/activities')
.post('/activity')
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ albumId: album.id, type: 'like' });
expect(status).toEqual(201);
@@ -255,7 +252,7 @@ describe('/activities', () => {
it('should return a 200 for a duplicate like on the album', async () => {
const reaction = await createActivity({ albumId: album.id, type: ReactionType.Like });
const { status, body } = await request(app)
.post('/activities')
.post('/activity')
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ albumId: album.id, type: 'like' });
expect(status).toEqual(200);
@@ -269,7 +266,7 @@ describe('/activities', () => {
type: ReactionType.Like,
});
const { status, body } = await request(app)
.post('/activities')
.post('/activity')
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ albumId: album.id, type: 'like' });
expect(status).toEqual(201);
@@ -278,7 +275,7 @@ describe('/activities', () => {
it('should add a comment to an asset', async () => {
const { status, body } = await request(app)
.post('/activities')
.post('/activity')
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({
albumId: album.id,
@@ -299,7 +296,7 @@ describe('/activities', () => {
it('should add a like to an asset', async () => {
const { status, body } = await request(app)
.post('/activities')
.post('/activity')
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ albumId: album.id, assetId: asset.id, type: 'like' });
expect(status).toEqual(201);
@@ -321,7 +318,7 @@ describe('/activities', () => {
});
const { status, body } = await request(app)
.post('/activities')
.post('/activity')
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ albumId: album.id, assetId: asset.id, type: 'like' });
expect(status).toEqual(200);
@@ -329,16 +326,16 @@ describe('/activities', () => {
});
});
describe('DELETE /activities/:id', () => {
describe('DELETE /activity/:id', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).delete(`/activities/${uuidDto.notFound}`);
const { status, body } = await request(app).delete(`/activity/${uuidDto.notFound}`);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should require a valid uuid', async () => {
const { status, body } = await request(app)
.delete(`/activities/${uuidDto.invalid}`)
.delete(`/activity/${uuidDto.invalid}`)
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest(['id must be a UUID']));
@@ -351,7 +348,7 @@ describe('/activities', () => {
comment: 'This is a test comment',
});
const { status } = await request(app)
.delete(`/activities/${reaction.id}`)
.delete(`/activity/${reaction.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toEqual(204);
});
@@ -362,7 +359,7 @@ describe('/activities', () => {
type: ReactionType.Like,
});
const { status } = await request(app)
.delete(`/activities/${reaction.id}`)
.delete(`/activity/${reaction.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toEqual(204);
});
@@ -375,7 +372,7 @@ describe('/activities', () => {
});
const { status } = await request(app)
.delete(`/activities/${reaction.id}`)
.delete(`/activity/${reaction.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toEqual(204);
@@ -389,7 +386,7 @@ describe('/activities', () => {
});
const { status, body } = await request(app)
.delete(`/activities/${reaction.id}`)
.delete(`/activity/${reaction.id}`)
.set('Authorization', `Bearer ${nonOwner.accessToken}`);
expect(status).toBe(400);
@@ -407,7 +404,7 @@ describe('/activities', () => {
);
const { status } = await request(app)
.delete(`/activities/${reaction.id}`)
.delete(`/activity/${reaction.id}`)
.set('Authorization', `Bearer ${nonOwner.accessToken}`);
expect(status).toBe(204);

View File

@@ -2,9 +2,9 @@ import {
addAssetsToAlbum,
AlbumResponseDto,
AlbumUserRole,
AssetMediaResponseDto,
AssetFileUploadResponseDto,
AssetOrder,
deleteUserAdmin,
deleteUser,
getAlbumInfo,
LoginResponseDto,
SharedLinkType,
@@ -23,11 +23,11 @@ const user2SharedUser = 'user2SharedUser';
const user2SharedLink = 'user2SharedLink';
const user2NotShared = 'user2NotShared';
describe('/albums', () => {
describe('/album', () => {
let admin: LoginResponseDto;
let user1: LoginResponseDto;
let user1Asset1: AssetMediaResponseDto;
let user1Asset2: AssetMediaResponseDto;
let user1Asset1: AssetFileUploadResponseDto;
let user1Asset2: AssetFileUploadResponseDto;
let user1Albums: AlbumResponseDto[];
let user2: LoginResponseDto;
let user2Albums: AlbumResponseDto[];
@@ -49,50 +49,72 @@ describe('/albums', () => {
utils.createAsset(user1.accessToken),
]);
user1Albums = await Promise.all([
const albums = await Promise.all([
// user 1
/* 0 */
utils.createAlbum(user1.accessToken, {
albumName: user1SharedEditorUser,
albumUsers: [{ userId: user2.userId, role: AlbumUserRole.Editor }],
sharedWithUserIds: [user2.userId],
assetIds: [user1Asset1.id],
}),
/* 1 */
utils.createAlbum(user1.accessToken, {
albumName: user1SharedLink,
assetIds: [user1Asset1.id],
}),
/* 2 */
utils.createAlbum(user1.accessToken, {
albumName: user1NotShared,
assetIds: [user1Asset1.id, user1Asset2.id],
}),
// user 2
/* 3 */
utils.createAlbum(user2.accessToken, {
albumName: user2SharedUser,
sharedWithUserIds: [user1.userId, user3.userId],
}),
/* 4 */
utils.createAlbum(user2.accessToken, { albumName: user2SharedLink }),
/* 5 */
utils.createAlbum(user2.accessToken, { albumName: user2NotShared }),
// user 3
/* 6 */
utils.createAlbum(user3.accessToken, {
albumName: 'Deleted',
sharedWithUserIds: [user1.userId],
}),
// user1 shared with an editor
/* 7 */
utils.createAlbum(user1.accessToken, {
albumName: user1SharedViewerUser,
albumUsers: [{ userId: user2.userId, role: AlbumUserRole.Viewer }],
sharedWithUserIds: [user2.userId],
assetIds: [user1Asset1.id],
}),
]);
user2Albums = await Promise.all([
utils.createAlbum(user2.accessToken, {
albumName: user2SharedUser,
albumUsers: [
{ userId: user1.userId, role: AlbumUserRole.Editor },
{ userId: user3.userId, role: AlbumUserRole.Editor },
],
}),
utils.createAlbum(user2.accessToken, { albumName: user2SharedLink }),
utils.createAlbum(user2.accessToken, { albumName: user2NotShared }),
]);
await utils.createAlbum(user3.accessToken, {
albumName: 'Deleted',
albumUsers: [{ userId: user1.userId, role: AlbumUserRole.Editor }],
// Make viewer
await utils.updateAlbumUser(user1.accessToken, {
id: albums[7].id,
userId: user2.userId,
updateAlbumUserDto: { role: AlbumUserRole.Viewer },
});
albums[0].albumUsers[0].role = AlbumUserRole.Editor;
albums[3].albumUsers[0].role = AlbumUserRole.Editor;
albums[6].albumUsers[0].role = AlbumUserRole.Editor;
await addAssetsToAlbum(
{ id: user2Albums[0].id, bulkIdsDto: { ids: [user1Asset1.id] } },
{ id: albums[3].id, bulkIdsDto: { ids: [user1Asset1.id] } },
{ headers: asBearerAuth(user1.accessToken) },
);
user2Albums[0] = await getAlbumInfo({ id: user2Albums[0].id }, { headers: asBearerAuth(user2.accessToken) });
albums[3] = await getAlbumInfo({ id: albums[3].id }, { headers: asBearerAuth(user2.accessToken) });
user1Albums = [...albums.slice(0, 3), albums[7]];
user2Albums = albums.slice(3, 6);
await Promise.all([
// add shared link to user1SharedLink album
@@ -107,19 +129,19 @@ describe('/albums', () => {
}),
]);
await deleteUserAdmin({ id: user3.userId, userAdminDeleteDto: {} }, { headers: asBearerAuth(admin.accessToken) });
await deleteUser({ id: user3.userId, deleteUserDto: {} }, { headers: asBearerAuth(admin.accessToken) });
});
describe('GET /albums', () => {
describe('GET /album', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get('/albums');
const { status, body } = await request(app).get('/album');
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should reject an invalid shared param', async () => {
const { status, body } = await request(app)
.get('/albums?shared=invalid')
.get('/album?shared=invalid')
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toEqual(400);
expect(body).toEqual(errorDto.badRequest(['shared must be a boolean value']));
@@ -127,7 +149,7 @@ describe('/albums', () => {
it('should reject an invalid assetId param', async () => {
const { status, body } = await request(app)
.get('/albums?assetId=invalid')
.get('/album?assetId=invalid')
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toEqual(400);
expect(body).toEqual(errorDto.badRequest(['assetId must be a UUID']));
@@ -135,7 +157,7 @@ describe('/albums', () => {
it("should not show other users' favorites", async () => {
const { status, body } = await request(app)
.get(`/albums/${user1Albums[0].id}?withoutAssets=false`)
.get(`/album/${user1Albums[0].id}?withoutAssets=false`)
.set('Authorization', `Bearer ${user2.accessToken}`);
expect(status).toEqual(200);
expect(body).toEqual({
@@ -146,7 +168,7 @@ describe('/albums', () => {
it('should not return shared albums with a deleted owner', async () => {
const { status, body } = await request(app)
.get('/albums?shared=true')
.get('/album?shared=true')
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200);
@@ -178,7 +200,7 @@ describe('/albums', () => {
});
it('should return the album collection including owned and shared', async () => {
const { status, body } = await request(app).get('/albums').set('Authorization', `Bearer ${user1.accessToken}`);
const { status, body } = await request(app).get('/album').set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200);
expect(body).toHaveLength(4);
expect(body).toEqual(
@@ -209,7 +231,7 @@ describe('/albums', () => {
it('should return the album collection filtered by shared', async () => {
const { status, body } = await request(app)
.get('/albums?shared=true')
.get('/album?shared=true')
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200);
expect(body).toHaveLength(4);
@@ -241,7 +263,7 @@ describe('/albums', () => {
it('should return the album collection filtered by NOT shared', async () => {
const { status, body } = await request(app)
.get('/albums?shared=false')
.get('/album?shared=false')
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200);
expect(body).toHaveLength(1);
@@ -258,7 +280,7 @@ describe('/albums', () => {
it('should return the album collection filtered by assetId', async () => {
const { status, body } = await request(app)
.get(`/albums?assetId=${user1Asset2.id}`)
.get(`/album?assetId=${user1Asset2.id}`)
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200);
expect(body).toHaveLength(1);
@@ -266,7 +288,7 @@ describe('/albums', () => {
it('should return the album collection filtered by assetId and ignores shared=true', async () => {
const { status, body } = await request(app)
.get(`/albums?shared=true&assetId=${user1Asset1.id}`)
.get(`/album?shared=true&assetId=${user1Asset1.id}`)
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200);
expect(body).toHaveLength(5);
@@ -274,23 +296,23 @@ describe('/albums', () => {
it('should return the album collection filtered by assetId and ignores shared=false', async () => {
const { status, body } = await request(app)
.get(`/albums?shared=false&assetId=${user1Asset1.id}`)
.get(`/album?shared=false&assetId=${user1Asset1.id}`)
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200);
expect(body).toHaveLength(5);
});
});
describe('GET /albums/:id', () => {
describe('GET /album/:id', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get(`/albums/${user1Albums[0].id}`);
const { status, body } = await request(app).get(`/album/${user1Albums[0].id}`);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should return album info for own album', async () => {
const { status, body } = await request(app)
.get(`/albums/${user1Albums[0].id}?withoutAssets=false`)
.get(`/album/${user1Albums[0].id}?withoutAssets=false`)
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200);
@@ -302,7 +324,7 @@ describe('/albums', () => {
it('should return album info for shared album (editor)', async () => {
const { status, body } = await request(app)
.get(`/albums/${user2Albums[0].id}?withoutAssets=false`)
.get(`/album/${user2Albums[0].id}?withoutAssets=false`)
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200);
@@ -311,7 +333,7 @@ describe('/albums', () => {
it('should return album info for shared album (viewer)', async () => {
const { status, body } = await request(app)
.get(`/albums/${user1Albums[3].id}?withoutAssets=false`)
.get(`/album/${user1Albums[3].id}?withoutAssets=false`)
.set('Authorization', `Bearer ${user2.accessToken}`);
expect(status).toBe(200);
@@ -320,7 +342,7 @@ describe('/albums', () => {
it('should return album info with assets when withoutAssets is undefined', async () => {
const { status, body } = await request(app)
.get(`/albums/${user1Albums[0].id}`)
.get(`/album/${user1Albums[0].id}`)
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200);
@@ -332,7 +354,7 @@ describe('/albums', () => {
it('should return album info without assets when withoutAssets is true', async () => {
const { status, body } = await request(app)
.get(`/albums/${user1Albums[0].id}?withoutAssets=true`)
.get(`/album/${user1Albums[0].id}?withoutAssets=true`)
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200);
@@ -344,16 +366,16 @@ describe('/albums', () => {
});
});
describe('GET /albums/count', () => {
describe('GET /album/count', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get('/albums/count');
const { status, body } = await request(app).get('/album/count');
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should return total count of albums the user has access to', async () => {
const { status, body } = await request(app)
.get('/albums/count')
.get('/album/count')
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200);
@@ -361,16 +383,16 @@ describe('/albums', () => {
});
});
describe('POST /albums', () => {
describe('POST /album', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).post('/albums').send({ albumName: 'New album' });
const { status, body } = await request(app).post('/album').send({ albumName: 'New album' });
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should create an album', async () => {
const { status, body } = await request(app)
.post('/albums')
.post('/album')
.send({ albumName: 'New album' })
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(201);
@@ -383,6 +405,7 @@ describe('/albums', () => {
description: '',
albumThumbnailAssetId: null,
shared: false,
sharedUsers: [],
albumUsers: [],
hasSharedLink: false,
assets: [],
@@ -394,9 +417,9 @@ describe('/albums', () => {
});
});
describe('PUT /albums/:id/assets', () => {
describe('PUT /album/:id/assets', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).put(`/albums/${user1Albums[0].id}/assets`);
const { status, body } = await request(app).put(`/album/${user1Albums[0].id}/assets`);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
@@ -404,7 +427,7 @@ describe('/albums', () => {
it('should be able to add own asset to own album', async () => {
const asset = await utils.createAsset(user1.accessToken);
const { status, body } = await request(app)
.put(`/albums/${user1Albums[0].id}/assets`)
.put(`/album/${user1Albums[0].id}/assets`)
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({ ids: [asset.id] });
@@ -415,7 +438,7 @@ describe('/albums', () => {
it('should be able to add own asset to shared album', async () => {
const asset = await utils.createAsset(user1.accessToken);
const { status, body } = await request(app)
.put(`/albums/${user2Albums[0].id}/assets`)
.put(`/album/${user2Albums[0].id}/assets`)
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({ ids: [asset.id] });
@@ -426,33 +449,19 @@ describe('/albums', () => {
it('should not be able to add assets to album as a viewer', async () => {
const asset = await utils.createAsset(user2.accessToken);
const { status, body } = await request(app)
.put(`/albums/${user1Albums[3].id}/assets`)
.put(`/album/${user1Albums[3].id}/assets`)
.set('Authorization', `Bearer ${user2.accessToken}`)
.send({ ids: [asset.id] });
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest('Not found or no album.addAsset access'));
});
it('should add duplicate assets only once', async () => {
const asset = await utils.createAsset(user1.accessToken);
const { status, body } = await request(app)
.put(`/albums/${user1Albums[0].id}/assets`)
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({ ids: [asset.id, asset.id] });
expect(status).toBe(200);
expect(body).toEqual([
expect.objectContaining({ id: asset.id, success: true }),
expect.objectContaining({ id: asset.id, success: false, error: 'duplicate' }),
]);
});
});
describe('PATCH /albums/:id', () => {
describe('PATCH /album/:id', () => {
it('should require authentication', async () => {
const { status, body } = await request(app)
.patch(`/albums/${uuidDto.notFound}`)
.patch(`/album/${uuidDto.notFound}`)
.send({ albumName: 'New album name' });
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
@@ -463,7 +472,7 @@ describe('/albums', () => {
albumName: 'New album',
});
const { status, body } = await request(app)
.patch(`/albums/${album.id}`)
.patch(`/album/${album.id}`)
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({
albumName: 'New album name',
@@ -480,7 +489,7 @@ describe('/albums', () => {
it('should not be able to update as a viewer', async () => {
const { status, body } = await request(app)
.patch(`/albums/${user1Albums[3].id}`)
.patch(`/album/${user1Albums[3].id}`)
.set('Authorization', `Bearer ${user2.accessToken}`)
.send({ albumName: 'New album name' });
@@ -490,7 +499,7 @@ describe('/albums', () => {
it('should not be able to update as an editor', async () => {
const { status, body } = await request(app)
.patch(`/albums/${user1Albums[0].id}`)
.patch(`/album/${user1Albums[0].id}`)
.set('Authorization', `Bearer ${user2.accessToken}`)
.send({ albumName: 'New album name' });
@@ -499,10 +508,10 @@ describe('/albums', () => {
});
});
describe('DELETE /albums/:id/assets', () => {
describe('DELETE /album/:id/assets', () => {
it('should require authentication', async () => {
const { status, body } = await request(app)
.delete(`/albums/${user1Albums[0].id}/assets`)
.delete(`/album/${user1Albums[0].id}/assets`)
.send({ ids: [user1Asset1.id] });
expect(status).toBe(401);
@@ -511,7 +520,7 @@ describe('/albums', () => {
it('should not be able to remove foreign asset from own album', async () => {
const { status, body } = await request(app)
.delete(`/albums/${user2Albums[0].id}/assets`)
.delete(`/album/${user2Albums[0].id}/assets`)
.set('Authorization', `Bearer ${user2.accessToken}`)
.send({ ids: [user1Asset1.id] });
@@ -527,7 +536,7 @@ describe('/albums', () => {
it('should not be able to remove foreign asset from foreign album', async () => {
const { status, body } = await request(app)
.delete(`/albums/${user1Albums[0].id}/assets`)
.delete(`/album/${user1Albums[0].id}/assets`)
.set('Authorization', `Bearer ${user2.accessToken}`)
.send({ ids: [user1Asset1.id] });
@@ -543,7 +552,7 @@ describe('/albums', () => {
it('should be able to remove own asset from own album', async () => {
const { status, body } = await request(app)
.delete(`/albums/${user1Albums[0].id}/assets`)
.delete(`/album/${user1Albums[0].id}/assets`)
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({ ids: [user1Asset1.id] });
@@ -553,7 +562,7 @@ describe('/albums', () => {
it('should be able to remove own asset from shared album', async () => {
const { status, body } = await request(app)
.delete(`/albums/${user2Albums[0].id}/assets`)
.delete(`/album/${user2Albums[0].id}/assets`)
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({ ids: [user1Asset1.id] });
@@ -563,26 +572,13 @@ describe('/albums', () => {
it('should not be able to remove assets from album as a viewer', async () => {
const { status, body } = await request(app)
.delete(`/albums/${user1Albums[3].id}/assets`)
.delete(`/album/${user1Albums[3].id}/assets`)
.set('Authorization', `Bearer ${user2.accessToken}`)
.send({ ids: [user1Asset1.id] });
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest('Not found or no album.removeAsset access'));
});
it('should remove duplicate assets only once', async () => {
const { status, body } = await request(app)
.delete(`/albums/${user1Albums[1].id}/assets`)
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({ ids: [user1Asset1.id, user1Asset1.id] });
expect(status).toBe(200);
expect(body).toEqual([
expect.objectContaining({ id: user1Asset1.id, success: true }),
expect.objectContaining({ id: user1Asset1.id, success: false, error: 'not_found' }),
]);
});
});
describe('PUT :id/users', () => {
@@ -595,7 +591,7 @@ describe('/albums', () => {
});
it('should require authentication', async () => {
const { status, body } = await request(app).put(`/albums/${user1Albums[0].id}/users`).send({ sharedUserIds: [] });
const { status, body } = await request(app).put(`/album/${user1Albums[0].id}/users`).send({ sharedUserIds: [] });
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
@@ -603,25 +599,21 @@ describe('/albums', () => {
it('should be able to add user to own album', async () => {
const { status, body } = await request(app)
.put(`/albums/${album.id}/users`)
.put(`/album/${album.id}/users`)
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({ albumUsers: [{ userId: user2.userId, role: AlbumUserRole.Editor }] });
expect(status).toBe(200);
expect(body).toEqual(
expect.objectContaining({
albumUsers: [
expect.objectContaining({
user: expect.objectContaining({ id: user2.userId }),
}),
],
sharedUsers: [expect.objectContaining({ id: user2.userId })],
}),
);
});
it('should not be able to share album with owner', async () => {
const { status, body } = await request(app)
.put(`/albums/${album.id}/users`)
.put(`/album/${album.id}/users`)
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({ albumUsers: [{ userId: user1.userId, role: AlbumUserRole.Editor }] });
@@ -631,12 +623,12 @@ describe('/albums', () => {
it('should not be able to add existing user to shared album', async () => {
await request(app)
.put(`/albums/${album.id}/users`)
.put(`/album/${album.id}/users`)
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({ albumUsers: [{ userId: user2.userId, role: AlbumUserRole.Editor }] });
const { status, body } = await request(app)
.put(`/albums/${album.id}/users`)
.put(`/album/${album.id}/users`)
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({ albumUsers: [{ userId: user2.userId, role: AlbumUserRole.Editor }] });
@@ -649,22 +641,18 @@ describe('/albums', () => {
it('should allow the album owner to change the role of a shared user', async () => {
const album = await utils.createAlbum(user1.accessToken, {
albumName: 'testAlbum',
albumUsers: [{ userId: user2.userId, role: AlbumUserRole.Viewer }],
sharedWithUserIds: [user2.userId],
});
expect(album.albumUsers[0].role).toEqual(AlbumUserRole.Viewer);
const { status } = await request(app)
.put(`/albums/${album.id}/user/${user2.userId}`)
.put(`/album/${album.id}/user/${user2.userId}`)
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({ role: AlbumUserRole.Editor });
expect(status).toBe(200);
// Get album to verify the role change
const { body } = await request(app)
.get(`/albums/${album.id}`)
.set('Authorization', `Bearer ${user1.accessToken}`);
const { body } = await request(app).get(`/album/${album.id}`).set('Authorization', `Bearer ${user1.accessToken}`);
expect(body).toEqual(
expect.objectContaining({
albumUsers: [expect.objectContaining({ role: AlbumUserRole.Editor })],
@@ -675,13 +663,11 @@ describe('/albums', () => {
it('should not allow a shared user to change the role of another shared user', async () => {
const album = await utils.createAlbum(user1.accessToken, {
albumName: 'testAlbum',
albumUsers: [{ userId: user2.userId, role: AlbumUserRole.Viewer }],
sharedWithUserIds: [user2.userId],
});
expect(album.albumUsers[0].role).toEqual(AlbumUserRole.Viewer);
const { status, body } = await request(app)
.put(`/albums/${album.id}/user/${user2.userId}`)
.put(`/album/${album.id}/user/${user2.userId}`)
.set('Authorization', `Bearer ${user2.accessToken}`)
.send({ role: AlbumUserRole.Editor });

File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More