mirror of
https://github.com/immich-app/immich.git
synced 2025-05-24 01:12:58 -04:00
Merge remote-tracking branch 'origin/main' into lighter_buckets
This commit is contained in:
commit
31bdad5d97
29
.github/workflows/build-mobile.yml
vendored
29
.github/workflows/build-mobile.yml
vendored
@ -7,6 +7,15 @@ on:
|
|||||||
ref:
|
ref:
|
||||||
required: false
|
required: false
|
||||||
type: string
|
type: string
|
||||||
|
secrets:
|
||||||
|
KEY_JKS:
|
||||||
|
required: true
|
||||||
|
ALIAS:
|
||||||
|
required: true
|
||||||
|
ANDROID_KEY_PASSWORD:
|
||||||
|
required: true
|
||||||
|
ANDROID_STORE_PASSWORD:
|
||||||
|
required: true
|
||||||
pull_request:
|
pull_request:
|
||||||
push:
|
push:
|
||||||
branches: [main]
|
branches: [main]
|
||||||
@ -15,14 +24,21 @@ concurrency:
|
|||||||
group: ${{ github.workflow }}-${{ github.ref }}
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
permissions: {}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
pre-job:
|
pre-job:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
outputs:
|
outputs:
|
||||||
should_run: ${{ steps.found_paths.outputs.mobile == 'true' || steps.should_force.outputs.should_force == 'true' }}
|
should_run: ${{ steps.found_paths.outputs.mobile == 'true' || steps.should_force.outputs.should_force == 'true' }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
- id: found_paths
|
- id: found_paths
|
||||||
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3
|
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3
|
||||||
with:
|
with:
|
||||||
@ -38,22 +54,17 @@ jobs:
|
|||||||
build-sign-android:
|
build-sign-android:
|
||||||
name: Build and sign Android
|
name: Build and sign Android
|
||||||
needs: pre-job
|
needs: pre-job
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
# Skip when PR from a fork
|
# Skip when PR from a fork
|
||||||
if: ${{ !github.event.pull_request.head.repo.fork && github.actor != 'dependabot[bot]' && needs.pre-job.outputs.should_run == 'true' }}
|
if: ${{ !github.event.pull_request.head.repo.fork && github.actor != 'dependabot[bot]' && needs.pre-job.outputs.should_run == 'true' }}
|
||||||
runs-on: macos-14
|
runs-on: macos-14
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Determine ref
|
|
||||||
id: get-ref
|
|
||||||
run: |
|
|
||||||
input_ref="${{ inputs.ref }}"
|
|
||||||
github_ref="${{ github.sha }}"
|
|
||||||
ref="${input_ref:-$github_ref}"
|
|
||||||
echo "ref=$ref" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
with:
|
with:
|
||||||
ref: ${{ steps.get-ref.outputs.ref }}
|
ref: ${{ inputs.ref || github.sha }}
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
- uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4
|
- uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4
|
||||||
with:
|
with:
|
||||||
|
17
.github/workflows/cache-cleanup.yml
vendored
17
.github/workflows/cache-cleanup.yml
vendored
@ -8,31 +8,38 @@ concurrency:
|
|||||||
group: ${{ github.workflow }}-${{ github.ref }}
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
permissions: {}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
cleanup:
|
cleanup:
|
||||||
name: Cleanup
|
name: Cleanup
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
actions: write
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code
|
- name: Check out code
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Cleanup
|
- name: Cleanup
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
REF: ${{ github.ref }}
|
||||||
run: |
|
run: |
|
||||||
gh extension install actions/gh-actions-cache
|
gh extension install actions/gh-actions-cache
|
||||||
|
|
||||||
REPO=${{ github.repository }}
|
REPO=${{ github.repository }}
|
||||||
BRANCH=${{ github.ref }}
|
|
||||||
|
|
||||||
echo "Fetching list of cache keys"
|
echo "Fetching list of cache keys"
|
||||||
cacheKeysForPR=$(gh actions-cache list -R $REPO -B $BRANCH -L 100 | cut -f 1 )
|
cacheKeysForPR=$(gh actions-cache list -R $REPO -B ${REF} -L 100 | cut -f 1 )
|
||||||
|
|
||||||
## Setting this to not fail the workflow while deleting cache keys.
|
## Setting this to not fail the workflow while deleting cache keys.
|
||||||
set +e
|
set +e
|
||||||
echo "Deleting caches..."
|
echo "Deleting caches..."
|
||||||
for cacheKey in $cacheKeysForPR
|
for cacheKey in $cacheKeysForPR
|
||||||
do
|
do
|
||||||
gh actions-cache delete $cacheKey -R $REPO -B $BRANCH --confirm
|
gh actions-cache delete $cacheKey -R "$REPO" -B "${REF}" --confirm
|
||||||
done
|
done
|
||||||
echo "Done"
|
echo "Done"
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
13
.github/workflows/cli.yml
vendored
13
.github/workflows/cli.yml
vendored
@ -16,19 +16,23 @@ concurrency:
|
|||||||
group: ${{ github.workflow }}-${{ github.ref }}
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
permissions:
|
permissions: {}
|
||||||
packages: write
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
publish:
|
publish:
|
||||||
name: CLI Publish
|
name: CLI Publish
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
working-directory: ./cli
|
working-directory: ./cli
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
# Setup .npmrc file to publish to npm
|
# Setup .npmrc file to publish to npm
|
||||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||||
with:
|
with:
|
||||||
@ -48,11 +52,16 @@ jobs:
|
|||||||
docker:
|
docker:
|
||||||
name: Docker
|
name: Docker
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
needs: publish
|
needs: publish
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0
|
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0
|
||||||
|
4
.github/workflows/codeql-analysis.yml
vendored
4
.github/workflows/codeql-analysis.yml
vendored
@ -24,6 +24,8 @@ concurrency:
|
|||||||
group: ${{ github.workflow }}-${{ github.ref }}
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
permissions: {}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
analyze:
|
analyze:
|
||||||
name: Analyze
|
name: Analyze
|
||||||
@ -43,6 +45,8 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
# Initializes the CodeQL tools for scanning.
|
# Initializes the CodeQL tools for scanning.
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
|
85
.github/workflows/docker.yml
vendored
85
.github/workflows/docker.yml
vendored
@ -12,18 +12,21 @@ concurrency:
|
|||||||
group: ${{ github.workflow }}-${{ github.ref }}
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
permissions:
|
permissions: {}
|
||||||
packages: write
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
pre-job:
|
pre-job:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
outputs:
|
outputs:
|
||||||
should_run_server: ${{ steps.found_paths.outputs.server == 'true' || steps.should_force.outputs.should_force == 'true' }}
|
should_run_server: ${{ steps.found_paths.outputs.server == 'true' || steps.should_force.outputs.should_force == 'true' }}
|
||||||
should_run_ml: ${{ steps.found_paths.outputs.machine-learning == 'true' || steps.should_force.outputs.should_force == 'true' }}
|
should_run_ml: ${{ steps.found_paths.outputs.machine-learning == 'true' || steps.should_force.outputs.should_force == 'true' }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
- id: found_paths
|
- id: found_paths
|
||||||
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3
|
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3
|
||||||
with:
|
with:
|
||||||
@ -45,6 +48,9 @@ jobs:
|
|||||||
retag_ml:
|
retag_ml:
|
||||||
name: Re-Tag ML
|
name: Re-Tag ML
|
||||||
needs: pre-job
|
needs: pre-job
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
if: ${{ needs.pre-job.outputs.should_run_ml == 'false' && !github.event.pull_request.head.repo.fork }}
|
if: ${{ needs.pre-job.outputs.should_run_ml == 'false' && !github.event.pull_request.head.repo.fork }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
@ -58,18 +64,22 @@ jobs:
|
|||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
- name: Re-tag image
|
- name: Re-tag image
|
||||||
|
env:
|
||||||
|
REGISTRY_NAME: 'ghcr.io'
|
||||||
|
REPOSITORY: ${{ github.repository_owner }}/immich-machine-learning
|
||||||
|
TAG_OLD: main${{ matrix.suffix }}
|
||||||
|
TAG_PR: ${{ github.event.number == 0 && github.ref_name || format('pr-{0}', github.event.number) }}${{ matrix.suffix }}
|
||||||
|
TAG_COMMIT: commit-${{ github.event_name != 'pull_request' && github.sha || github.event.pull_request.head.sha }}${{ matrix.suffix }}
|
||||||
run: |
|
run: |
|
||||||
REGISTRY_NAME="ghcr.io"
|
docker buildx imagetools create -t "${REGISTRY_NAME}/${REPOSITORY}:${TAG_PR}" "${REGISTRY_NAME}/${REPOSITORY}:${TAG_OLD}"
|
||||||
REPOSITORY=${{ github.repository_owner }}/immich-machine-learning
|
docker buildx imagetools create -t "${REGISTRY_NAME}/${REPOSITORY}:${TAG_COMMIT}" "${REGISTRY_NAME}/${REPOSITORY}:${TAG_OLD}"
|
||||||
TAG_OLD=main${{ matrix.suffix }}
|
|
||||||
TAG_PR=${{ github.event.number == 0 && github.ref_name || format('pr-{0}', github.event.number) }}${{ matrix.suffix }}
|
|
||||||
TAG_COMMIT=commit-${{ github.event_name != 'pull_request' && github.sha || github.event.pull_request.head.sha }}${{ matrix.suffix }}
|
|
||||||
docker buildx imagetools create -t $REGISTRY_NAME/$REPOSITORY:$TAG_PR $REGISTRY_NAME/$REPOSITORY:$TAG_OLD
|
|
||||||
docker buildx imagetools create -t $REGISTRY_NAME/$REPOSITORY:$TAG_COMMIT $REGISTRY_NAME/$REPOSITORY:$TAG_OLD
|
|
||||||
|
|
||||||
retag_server:
|
retag_server:
|
||||||
name: Re-Tag Server
|
name: Re-Tag Server
|
||||||
needs: pre-job
|
needs: pre-job
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
if: ${{ needs.pre-job.outputs.should_run_server == 'false' && !github.event.pull_request.head.repo.fork }}
|
if: ${{ needs.pre-job.outputs.should_run_server == 'false' && !github.event.pull_request.head.repo.fork }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
@ -83,18 +93,22 @@ jobs:
|
|||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
- name: Re-tag image
|
- name: Re-tag image
|
||||||
|
env:
|
||||||
|
REGISTRY_NAME: 'ghcr.io'
|
||||||
|
REPOSITORY: ${{ github.repository_owner }}/immich-server
|
||||||
|
TAG_OLD: main${{ matrix.suffix }}
|
||||||
|
TAG_PR: ${{ github.event.number == 0 && github.ref_name || format('pr-{0}', github.event.number) }}${{ matrix.suffix }}
|
||||||
|
TAG_COMMIT: commit-${{ github.event_name != 'pull_request' && github.sha || github.event.pull_request.head.sha }}${{ matrix.suffix }}
|
||||||
run: |
|
run: |
|
||||||
REGISTRY_NAME="ghcr.io"
|
docker buildx imagetools create -t "${REGISTRY_NAME}/${REPOSITORY}:${TAG_PR}" "${REGISTRY_NAME}/${REPOSITORY}:${TAG_OLD}"
|
||||||
REPOSITORY=${{ github.repository_owner }}/immich-server
|
docker buildx imagetools create -t "${REGISTRY_NAME}/${REPOSITORY}:${TAG_COMMIT}" "${REGISTRY_NAME}/${REPOSITORY}:${TAG_OLD}"
|
||||||
TAG_OLD=main${{ matrix.suffix }}
|
|
||||||
TAG_PR=${{ github.event.number == 0 && github.ref_name || format('pr-{0}', github.event.number) }}${{ matrix.suffix }}
|
|
||||||
TAG_COMMIT=commit-${{ github.event_name != 'pull_request' && github.sha || github.event.pull_request.head.sha }}${{ matrix.suffix }}
|
|
||||||
docker buildx imagetools create -t $REGISTRY_NAME/$REPOSITORY:$TAG_PR $REGISTRY_NAME/$REPOSITORY:$TAG_OLD
|
|
||||||
docker buildx imagetools create -t $REGISTRY_NAME/$REPOSITORY:$TAG_COMMIT $REGISTRY_NAME/$REPOSITORY:$TAG_OLD
|
|
||||||
|
|
||||||
build_and_push_ml:
|
build_and_push_ml:
|
||||||
name: Build and Push ML
|
name: Build and Push ML
|
||||||
needs: pre-job
|
needs: pre-job
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
if: ${{ needs.pre-job.outputs.should_run_ml == 'true' }}
|
if: ${{ needs.pre-job.outputs.should_run_ml == 'true' }}
|
||||||
runs-on: ${{ matrix.runner }}
|
runs-on: ${{ matrix.runner }}
|
||||||
env:
|
env:
|
||||||
@ -148,6 +162,8 @@ jobs:
|
|||||||
|
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0
|
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0
|
||||||
@ -161,11 +177,14 @@ jobs:
|
|||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Generate cache key suffix
|
- name: Generate cache key suffix
|
||||||
|
env:
|
||||||
|
REF: ${{ github.ref_name }}
|
||||||
run: |
|
run: |
|
||||||
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
|
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
|
||||||
echo "CACHE_KEY_SUFFIX=pr-${{ github.event.number }}" >> $GITHUB_ENV
|
echo "CACHE_KEY_SUFFIX=pr-${{ github.event.number }}" >> $GITHUB_ENV
|
||||||
else
|
else
|
||||||
echo "CACHE_KEY_SUFFIX=$(echo ${{ github.ref_name }} | sed 's/[^a-zA-Z0-9]/-/g')" >> $GITHUB_ENV
|
SUFFIX=$(echo "${REF}" | sed 's/[^a-zA-Z0-9]/-/g')
|
||||||
|
echo "CACHE_KEY_SUFFIX=${SUFFIX}" >> $GITHUB_ENV
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Generate cache target
|
- name: Generate cache target
|
||||||
@ -175,7 +194,7 @@ jobs:
|
|||||||
# Essentially just ignore the cache output (forks can't write to registry cache)
|
# Essentially just ignore the cache output (forks can't write to registry cache)
|
||||||
echo "cache-to=type=local,dest=/tmp/discard,ignore-error=true" >> $GITHUB_OUTPUT
|
echo "cache-to=type=local,dest=/tmp/discard,ignore-error=true" >> $GITHUB_OUTPUT
|
||||||
else
|
else
|
||||||
echo "cache-to=type=registry,ref=${{ env.GHCR_REPO }}-build-cache:${{ env.PLATFORM_PAIR }}-${{ matrix.device }}-${{ env.CACHE_KEY_SUFFIX }},mode=max,compression=zstd" >> $GITHUB_OUTPUT
|
echo "cache-to=type=registry,ref=${GHCR_REPO}-build-cache:${PLATFORM_PAIR}-${{ matrix.device }}-${CACHE_KEY_SUFFIX},mode=max,compression=zstd" >> $GITHUB_OUTPUT
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Generate docker image tags
|
- name: Generate docker image tags
|
||||||
@ -221,6 +240,10 @@ jobs:
|
|||||||
merge_ml:
|
merge_ml:
|
||||||
name: Merge & Push ML
|
name: Merge & Push ML
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
actions: read
|
||||||
|
packages: write
|
||||||
if: ${{ needs.pre-job.outputs.should_run_ml == 'true' && !github.event.pull_request.head.repo.fork }}
|
if: ${{ needs.pre-job.outputs.should_run_ml == 'true' && !github.event.pull_request.head.repo.fork }}
|
||||||
env:
|
env:
|
||||||
GHCR_REPO: ghcr.io/${{ github.repository_owner }}/immich-machine-learning
|
GHCR_REPO: ghcr.io/${{ github.repository_owner }}/immich-machine-learning
|
||||||
@ -308,15 +331,16 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
TAGS=$(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
TAGS=$(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
||||||
SOURCE_ARGS=$(printf '${{ env.GHCR_REPO }}@sha256:%s ' *)
|
SOURCE_ARGS=$(printf "${GHCR_REPO}@sha256:%s " *)
|
||||||
|
|
||||||
echo "docker buildx imagetools create $TAGS "${ANNOTATIONS[@]}" $SOURCE_ARGS"
|
|
||||||
|
|
||||||
docker buildx imagetools create $TAGS "${ANNOTATIONS[@]}" $SOURCE_ARGS
|
docker buildx imagetools create $TAGS "${ANNOTATIONS[@]}" $SOURCE_ARGS
|
||||||
|
|
||||||
build_and_push_server:
|
build_and_push_server:
|
||||||
name: Build and Push Server
|
name: Build and Push Server
|
||||||
runs-on: ${{ matrix.runner }}
|
runs-on: ${{ matrix.runner }}
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
needs: pre-job
|
needs: pre-job
|
||||||
if: ${{ needs.pre-job.outputs.should_run_server == 'true' }}
|
if: ${{ needs.pre-job.outputs.should_run_server == 'true' }}
|
||||||
env:
|
env:
|
||||||
@ -340,6 +364,8 @@ jobs:
|
|||||||
|
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3
|
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3
|
||||||
@ -353,11 +379,14 @@ jobs:
|
|||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Generate cache key suffix
|
- name: Generate cache key suffix
|
||||||
|
env:
|
||||||
|
REF: ${{ github.ref_name }}
|
||||||
run: |
|
run: |
|
||||||
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
|
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
|
||||||
echo "CACHE_KEY_SUFFIX=pr-${{ github.event.number }}" >> $GITHUB_ENV
|
echo "CACHE_KEY_SUFFIX=pr-${{ github.event.number }}" >> $GITHUB_ENV
|
||||||
else
|
else
|
||||||
echo "CACHE_KEY_SUFFIX=$(echo ${{ github.ref_name }} | sed 's/[^a-zA-Z0-9]/-/g')" >> $GITHUB_ENV
|
SUFFIX=$(echo "${REF}" | sed 's/[^a-zA-Z0-9]/-/g')
|
||||||
|
echo "CACHE_KEY_SUFFIX=${SUFFIX}" >> $GITHUB_ENV
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Generate cache target
|
- name: Generate cache target
|
||||||
@ -367,7 +396,7 @@ jobs:
|
|||||||
# Essentially just ignore the cache output (forks can't write to registry cache)
|
# Essentially just ignore the cache output (forks can't write to registry cache)
|
||||||
echo "cache-to=type=local,dest=/tmp/discard,ignore-error=true" >> $GITHUB_OUTPUT
|
echo "cache-to=type=local,dest=/tmp/discard,ignore-error=true" >> $GITHUB_OUTPUT
|
||||||
else
|
else
|
||||||
echo "cache-to=type=registry,ref=${{ env.GHCR_REPO }}-build-cache:${{ env.PLATFORM_PAIR }}-${{ env.CACHE_KEY_SUFFIX }},mode=max,compression=zstd" >> $GITHUB_OUTPUT
|
echo "cache-to=type=registry,ref=${GHCR_REPO}-build-cache:${PLATFORM_PAIR}-${CACHE_KEY_SUFFIX},mode=max,compression=zstd" >> $GITHUB_OUTPUT
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Generate docker image tags
|
- name: Generate docker image tags
|
||||||
@ -413,6 +442,10 @@ jobs:
|
|||||||
merge_server:
|
merge_server:
|
||||||
name: Merge & Push Server
|
name: Merge & Push Server
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
actions: read
|
||||||
|
packages: write
|
||||||
if: ${{ needs.pre-job.outputs.should_run_server == 'true' && !github.event.pull_request.head.repo.fork }}
|
if: ${{ needs.pre-job.outputs.should_run_server == 'true' && !github.event.pull_request.head.repo.fork }}
|
||||||
env:
|
env:
|
||||||
GHCR_REPO: ghcr.io/${{ github.repository_owner }}/immich-server
|
GHCR_REPO: ghcr.io/${{ github.repository_owner }}/immich-server
|
||||||
@ -486,15 +519,14 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
TAGS=$(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
TAGS=$(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
||||||
SOURCE_ARGS=$(printf '${{ env.GHCR_REPO }}@sha256:%s ' *)
|
SOURCE_ARGS=$(printf "${GHCR_REPO}@sha256:%s " *)
|
||||||
|
|
||||||
echo "docker buildx imagetools create $TAGS "${ANNOTATIONS[@]}" $SOURCE_ARGS"
|
|
||||||
|
|
||||||
docker buildx imagetools create $TAGS "${ANNOTATIONS[@]}" $SOURCE_ARGS
|
docker buildx imagetools create $TAGS "${ANNOTATIONS[@]}" $SOURCE_ARGS
|
||||||
|
|
||||||
success-check-server:
|
success-check-server:
|
||||||
name: Docker Build & Push Server Success
|
name: Docker Build & Push Server Success
|
||||||
needs: [merge_server, retag_server]
|
needs: [merge_server, retag_server]
|
||||||
|
permissions: {}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: always()
|
if: always()
|
||||||
steps:
|
steps:
|
||||||
@ -508,6 +540,7 @@ jobs:
|
|||||||
success-check-ml:
|
success-check-ml:
|
||||||
name: Docker Build & Push ML Success
|
name: Docker Build & Push ML Success
|
||||||
needs: [merge_ml, retag_ml]
|
needs: [merge_ml, retag_ml]
|
||||||
|
permissions: {}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: always()
|
if: always()
|
||||||
steps:
|
steps:
|
||||||
|
10
.github/workflows/docs-build.yml
vendored
10
.github/workflows/docs-build.yml
vendored
@ -10,14 +10,20 @@ concurrency:
|
|||||||
group: ${{ github.workflow }}-${{ github.ref }}
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
permissions: {}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
pre-job:
|
pre-job:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
outputs:
|
outputs:
|
||||||
should_run: ${{ steps.found_paths.outputs.docs == 'true' || steps.should_force.outputs.should_force == 'true' }}
|
should_run: ${{ steps.found_paths.outputs.docs == 'true' || steps.should_force.outputs.should_force == 'true' }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
- id: found_paths
|
- id: found_paths
|
||||||
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3
|
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3
|
||||||
with:
|
with:
|
||||||
@ -33,6 +39,8 @@ jobs:
|
|||||||
build:
|
build:
|
||||||
name: Docs Build
|
name: Docs Build
|
||||||
needs: pre-job
|
needs: pre-job
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
if: ${{ needs.pre-job.outputs.should_run == 'true' }}
|
if: ${{ needs.pre-job.outputs.should_run == 'true' }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
defaults:
|
defaults:
|
||||||
@ -42,6 +50,8 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||||
|
20
.github/workflows/docs-deploy.yml
vendored
20
.github/workflows/docs-deploy.yml
vendored
@ -9,6 +9,9 @@ jobs:
|
|||||||
checks:
|
checks:
|
||||||
name: Docs Deploy Checks
|
name: Docs Deploy Checks
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
actions: read
|
||||||
|
pull-requests: read
|
||||||
outputs:
|
outputs:
|
||||||
parameters: ${{ steps.parameters.outputs.result }}
|
parameters: ${{ steps.parameters.outputs.result }}
|
||||||
artifact: ${{ steps.get-artifact.outputs.result }}
|
artifact: ${{ steps.get-artifact.outputs.result }}
|
||||||
@ -36,6 +39,8 @@ jobs:
|
|||||||
- name: Determine deploy parameters
|
- name: Determine deploy parameters
|
||||||
id: parameters
|
id: parameters
|
||||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
|
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
|
||||||
|
env:
|
||||||
|
HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
const eventType = context.payload.workflow_run.event;
|
const eventType = context.payload.workflow_run.event;
|
||||||
@ -57,7 +62,8 @@ jobs:
|
|||||||
} else if (eventType == "pull_request") {
|
} else if (eventType == "pull_request") {
|
||||||
let pull_number = context.payload.workflow_run.pull_requests[0]?.number;
|
let pull_number = context.payload.workflow_run.pull_requests[0]?.number;
|
||||||
if(!pull_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 {HEAD_SHA} = process.env;
|
||||||
|
const response = await github.rest.search.issuesAndPullRequests({q: `repo:${{ github.repository }} is:pr sha:${HEAD_SHA}`,per_page: 1,})
|
||||||
const items = response.data.items
|
const items = response.data.items
|
||||||
if (items.length < 1) {
|
if (items.length < 1) {
|
||||||
throw new Error("No pull request found for the commit")
|
throw new Error("No pull request found for the commit")
|
||||||
@ -95,10 +101,16 @@ jobs:
|
|||||||
name: Docs Deploy
|
name: Docs Deploy
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: checks
|
needs: checks
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
actions: read
|
||||||
|
pull-requests: write
|
||||||
if: ${{ fromJson(needs.checks.outputs.artifact).found && fromJson(needs.checks.outputs.parameters).shouldDeploy }}
|
if: ${{ fromJson(needs.checks.outputs.artifact).found && fromJson(needs.checks.outputs.parameters).shouldDeploy }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Load parameters
|
- name: Load parameters
|
||||||
id: parameters
|
id: parameters
|
||||||
@ -162,9 +174,11 @@ jobs:
|
|||||||
|
|
||||||
- name: Output Cleaning
|
- name: Output Cleaning
|
||||||
id: clean
|
id: clean
|
||||||
|
env:
|
||||||
|
TG_OUTPUT: ${{ steps.docs-output.outputs.tg_action_output }}
|
||||||
run: |
|
run: |
|
||||||
TG_OUT=$(echo '${{ steps.docs-output.outputs.tg_action_output }}' | sed 's|%0A|\n|g ; s|%3C|<|g' | jq -c .)
|
CLEANED=$(echo "$TG_OUTPUT" | sed 's|%0A|\n|g ; s|%3C|<|g' | jq -c .)
|
||||||
echo "output=$TG_OUT" >> $GITHUB_OUTPUT
|
echo "output=$CLEANED" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Publish to Cloudflare Pages
|
- name: Publish to Cloudflare Pages
|
||||||
uses: cloudflare/pages-action@f0a1cd58cd66095dee69bfa18fa5efd1dde93bca # v1
|
uses: cloudflare/pages-action@f0a1cd58cd66095dee69bfa18fa5efd1dde93bca # v1
|
||||||
|
7
.github/workflows/docs-destroy.yml
vendored
7
.github/workflows/docs-destroy.yml
vendored
@ -3,13 +3,20 @@ on:
|
|||||||
pull_request_target:
|
pull_request_target:
|
||||||
types: [closed]
|
types: [closed]
|
||||||
|
|
||||||
|
permissions: {}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
deploy:
|
deploy:
|
||||||
name: Docs Destroy
|
name: Docs Destroy
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
pull-requests: write
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Destroy Docs Subdomain
|
- name: Destroy Docs Subdomain
|
||||||
env:
|
env:
|
||||||
|
4
.github/workflows/fix-format.yml
vendored
4
.github/workflows/fix-format.yml
vendored
@ -4,11 +4,14 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
types: [labeled]
|
types: [labeled]
|
||||||
|
|
||||||
|
permissions: {}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
fix-formatting:
|
fix-formatting:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: ${{ github.event.label.name == 'fix:formatting' }}
|
if: ${{ github.event.label.name == 'fix:formatting' }}
|
||||||
permissions:
|
permissions:
|
||||||
|
contents: write
|
||||||
pull-requests: write
|
pull-requests: write
|
||||||
steps:
|
steps:
|
||||||
- name: Generate a token
|
- name: Generate a token
|
||||||
@ -23,6 +26,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
ref: ${{ github.event.pull_request.head.ref }}
|
ref: ${{ github.event.pull_request.head.ref }}
|
||||||
token: ${{ steps.generate-token.outputs.token }}
|
token: ${{ steps.generate-token.outputs.token }}
|
||||||
|
persist-credentials: true
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||||
|
2
.github/workflows/pr-label-validation.yml
vendored
2
.github/workflows/pr-label-validation.yml
vendored
@ -4,6 +4,8 @@ on:
|
|||||||
pull_request_target:
|
pull_request_target:
|
||||||
types: [opened, labeled, unlabeled, synchronize]
|
types: [opened, labeled, unlabeled, synchronize]
|
||||||
|
|
||||||
|
permissions: {}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
validate-release-label:
|
validate-release-label:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
2
.github/workflows/pr-labeler.yml
vendored
2
.github/workflows/pr-labeler.yml
vendored
@ -2,6 +2,8 @@ name: 'Pull Request Labeler'
|
|||||||
on:
|
on:
|
||||||
- pull_request_target
|
- pull_request_target
|
||||||
|
|
||||||
|
permissions: {}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
labeler:
|
labeler:
|
||||||
permissions:
|
permissions:
|
||||||
|
@ -4,9 +4,13 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
types: [opened, synchronize, reopened, edited]
|
types: [opened, synchronize, reopened, edited]
|
||||||
|
|
||||||
|
permissions: {}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
validate-pr-title:
|
validate-pr-title:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
pull-requests: write
|
||||||
steps:
|
steps:
|
||||||
- name: PR Conventional Commit Validation
|
- name: PR Conventional Commit Validation
|
||||||
uses: ytanikin/PRConventionalCommits@b628c5a234cc32513014b7bfdd1e47b532124d98 # 1.3.0
|
uses: ytanikin/PRConventionalCommits@b628c5a234cc32513014b7bfdd1e47b532124d98 # 1.3.0
|
||||||
|
18
.github/workflows/prepare-release.yml
vendored
18
.github/workflows/prepare-release.yml
vendored
@ -21,13 +21,14 @@ concurrency:
|
|||||||
group: ${{ github.workflow }}-${{ github.ref }}-root
|
group: ${{ github.workflow }}-${{ github.ref }}-root
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
permissions: {}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
bump_version:
|
bump_version:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
outputs:
|
outputs:
|
||||||
ref: ${{ steps.push-tag.outputs.commit_long_sha }}
|
ref: ${{ steps.push-tag.outputs.commit_long_sha }}
|
||||||
|
permissions: {} # No job-level permissions are needed because it uses the app-token
|
||||||
steps:
|
steps:
|
||||||
- name: Generate a token
|
- name: Generate a token
|
||||||
id: generate-token
|
id: generate-token
|
||||||
@ -40,6 +41,7 @@ jobs:
|
|||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
with:
|
with:
|
||||||
token: ${{ steps.generate-token.outputs.token }}
|
token: ${{ steps.generate-token.outputs.token }}
|
||||||
|
persist-credentials: true
|
||||||
|
|
||||||
- name: Install uv
|
- name: Install uv
|
||||||
uses: astral-sh/setup-uv@0c5e2b8115b80b4c7c5ddf6ffdd634974642d182 # v5
|
uses: astral-sh/setup-uv@0c5e2b8115b80b4c7c5ddf6ffdd634974642d182 # v5
|
||||||
@ -59,14 +61,20 @@ jobs:
|
|||||||
build_mobile:
|
build_mobile:
|
||||||
uses: ./.github/workflows/build-mobile.yml
|
uses: ./.github/workflows/build-mobile.yml
|
||||||
needs: bump_version
|
needs: bump_version
|
||||||
secrets: inherit
|
secrets:
|
||||||
|
KEY_JKS: ${{ secrets.KEY_JKS }}
|
||||||
|
ALIAS: ${{ secrets.ALIAS }}
|
||||||
|
ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
|
||||||
|
ANDROID_STORE_PASSWORD: ${{ secrets.ANDROID_STORE_PASSWORD }}
|
||||||
with:
|
with:
|
||||||
ref: ${{ needs.bump_version.outputs.ref }}
|
ref: ${{ needs.bump_version.outputs.ref }}
|
||||||
|
|
||||||
prepare_release:
|
prepare_release:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: build_mobile
|
needs: build_mobile
|
||||||
|
permissions:
|
||||||
|
actions: read # To download the app artifact
|
||||||
|
# No content permissions are needed because it uses the app-token
|
||||||
steps:
|
steps:
|
||||||
- name: Generate a token
|
- name: Generate a token
|
||||||
id: generate-token
|
id: generate-token
|
||||||
@ -79,6 +87,7 @@ jobs:
|
|||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
with:
|
with:
|
||||||
token: ${{ steps.generate-token.outputs.token }}
|
token: ${{ steps.generate-token.outputs.token }}
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Download APK
|
- name: Download APK
|
||||||
uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4
|
uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4
|
||||||
@ -90,6 +99,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
draft: true
|
draft: true
|
||||||
tag_name: ${{ env.IMMICH_VERSION }}
|
tag_name: ${{ env.IMMICH_VERSION }}
|
||||||
|
token: ${{ steps.generate-token.outputs.token }}
|
||||||
generate_release_notes: true
|
generate_release_notes: true
|
||||||
body_path: misc/release/notes.tmpl
|
body_path: misc/release/notes.tmpl
|
||||||
files: |
|
files: |
|
||||||
|
2
.github/workflows/preview-label.yaml
vendored
2
.github/workflows/preview-label.yaml
vendored
@ -4,6 +4,8 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
types: [labeled, closed]
|
types: [labeled, closed]
|
||||||
|
|
||||||
|
permissions: {}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
comment-status:
|
comment-status:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
8
.github/workflows/sdk.yml
vendored
8
.github/workflows/sdk.yml
vendored
@ -4,18 +4,22 @@ on:
|
|||||||
release:
|
release:
|
||||||
types: [published]
|
types: [published]
|
||||||
|
|
||||||
permissions:
|
permissions: {}
|
||||||
packages: write
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
publish:
|
publish:
|
||||||
name: Publish `@immich/sdk`
|
name: Publish `@immich/sdk`
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
working-directory: ./open-api/typescript-sdk
|
working-directory: ./open-api/typescript-sdk
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
# Setup .npmrc file to publish to npm
|
# Setup .npmrc file to publish to npm
|
||||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||||
with:
|
with:
|
||||||
|
16
.github/workflows/static_analysis.yml
vendored
16
.github/workflows/static_analysis.yml
vendored
@ -9,14 +9,20 @@ concurrency:
|
|||||||
group: ${{ github.workflow }}-${{ github.ref }}
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
permissions: {}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
pre-job:
|
pre-job:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
outputs:
|
outputs:
|
||||||
should_run: ${{ steps.found_paths.outputs.mobile == 'true' || steps.should_force.outputs.should_force == 'true' }}
|
should_run: ${{ steps.found_paths.outputs.mobile == 'true' || steps.should_force.outputs.should_force == 'true' }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
- id: found_paths
|
- id: found_paths
|
||||||
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3
|
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3
|
||||||
with:
|
with:
|
||||||
@ -33,12 +39,14 @@ jobs:
|
|||||||
name: Run Dart Code Analysis
|
name: Run Dart Code Analysis
|
||||||
needs: pre-job
|
needs: pre-job
|
||||||
if: ${{ needs.pre-job.outputs.should_run == 'true' }}
|
if: ${{ needs.pre-job.outputs.should_run == 'true' }}
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Setup Flutter SDK
|
- name: Setup Flutter SDK
|
||||||
uses: subosito/flutter-action@e938fdf56512cc96ef2f93601a5a40bde3801046 # v2
|
uses: subosito/flutter-action@e938fdf56512cc96ef2f93601a5a40bde3801046 # v2
|
||||||
@ -69,9 +77,11 @@ jobs:
|
|||||||
|
|
||||||
- name: Verify files have not changed
|
- name: Verify files have not changed
|
||||||
if: steps.verify-changed-files.outputs.files_changed == 'true'
|
if: steps.verify-changed-files.outputs.files_changed == 'true'
|
||||||
|
env:
|
||||||
|
CHANGED_FILES: ${{ steps.verify-changed-files.outputs.changed_files }}
|
||||||
run: |
|
run: |
|
||||||
echo "ERROR: Generated files not up to date! Run make_build inside the mobile directory"
|
echo "ERROR: Generated files not up to date! Run make_build inside the mobile directory"
|
||||||
echo "Changed files: ${{ steps.verify-changed-files.outputs.changed_files }}"
|
echo "Changed files: ${CHANGED_FILES}"
|
||||||
exit 1
|
exit 1
|
||||||
|
|
||||||
- name: Run dart analyze
|
- name: Run dart analyze
|
||||||
|
80
.github/workflows/test.yml
vendored
80
.github/workflows/test.yml
vendored
@ -9,9 +9,13 @@ concurrency:
|
|||||||
group: ${{ github.workflow }}-${{ github.ref }}
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
permissions: {}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
pre-job:
|
pre-job:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
outputs:
|
outputs:
|
||||||
should_run_web: ${{ steps.found_paths.outputs.web == 'true' || steps.should_force.outputs.should_force == 'true' }}
|
should_run_web: ${{ steps.found_paths.outputs.web == 'true' || steps.should_force.outputs.should_force == 'true' }}
|
||||||
should_run_server: ${{ steps.found_paths.outputs.server == 'true' || steps.should_force.outputs.should_force == 'true' }}
|
should_run_server: ${{ steps.found_paths.outputs.server == 'true' || steps.should_force.outputs.should_force == 'true' }}
|
||||||
@ -25,6 +29,9 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
- id: found_paths
|
- id: found_paths
|
||||||
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3
|
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3
|
||||||
with:
|
with:
|
||||||
@ -58,6 +65,8 @@ jobs:
|
|||||||
needs: pre-job
|
needs: pre-job
|
||||||
if: ${{ needs.pre-job.outputs.should_run_server == 'true' }}
|
if: ${{ needs.pre-job.outputs.should_run_server == 'true' }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
working-directory: ./server
|
working-directory: ./server
|
||||||
@ -65,6 +74,8 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||||
@ -95,6 +106,8 @@ jobs:
|
|||||||
needs: pre-job
|
needs: pre-job
|
||||||
if: ${{ needs.pre-job.outputs.should_run_cli == 'true' }}
|
if: ${{ needs.pre-job.outputs.should_run_cli == 'true' }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
working-directory: ./cli
|
working-directory: ./cli
|
||||||
@ -102,6 +115,8 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||||
@ -136,6 +151,8 @@ jobs:
|
|||||||
needs: pre-job
|
needs: pre-job
|
||||||
if: ${{ needs.pre-job.outputs.should_run_cli == 'true' }}
|
if: ${{ needs.pre-job.outputs.should_run_cli == 'true' }}
|
||||||
runs-on: windows-latest
|
runs-on: windows-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
working-directory: ./cli
|
working-directory: ./cli
|
||||||
@ -143,6 +160,8 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||||
@ -170,6 +189,8 @@ jobs:
|
|||||||
needs: pre-job
|
needs: pre-job
|
||||||
if: ${{ needs.pre-job.outputs.should_run_web == 'true' }}
|
if: ${{ needs.pre-job.outputs.should_run_web == 'true' }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
working-directory: ./web
|
working-directory: ./web
|
||||||
@ -177,6 +198,8 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||||
@ -215,6 +238,8 @@ jobs:
|
|||||||
needs: pre-job
|
needs: pre-job
|
||||||
if: ${{ needs.pre-job.outputs.should_run_e2e == 'true' }}
|
if: ${{ needs.pre-job.outputs.should_run_e2e == 'true' }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
working-directory: ./e2e
|
working-directory: ./e2e
|
||||||
@ -222,6 +247,8 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||||
@ -254,6 +281,8 @@ jobs:
|
|||||||
needs: pre-job
|
needs: pre-job
|
||||||
if: ${{ needs.pre-job.outputs.should_run_server == 'true' }}
|
if: ${{ needs.pre-job.outputs.should_run_server == 'true' }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
working-directory: ./server
|
working-directory: ./server
|
||||||
@ -261,6 +290,8 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||||
@ -279,6 +310,8 @@ jobs:
|
|||||||
needs: pre-job
|
needs: pre-job
|
||||||
if: ${{ needs.pre-job.outputs.should_run_e2e_server_cli == 'true' }}
|
if: ${{ needs.pre-job.outputs.should_run_e2e_server_cli == 'true' }}
|
||||||
runs-on: mich
|
runs-on: mich
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
working-directory: ./e2e
|
working-directory: ./e2e
|
||||||
@ -287,6 +320,7 @@ jobs:
|
|||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
with:
|
with:
|
||||||
|
persist-credentials: false
|
||||||
submodules: 'recursive'
|
submodules: 'recursive'
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
@ -321,6 +355,8 @@ jobs:
|
|||||||
needs: pre-job
|
needs: pre-job
|
||||||
if: ${{ needs.pre-job.outputs.should_run_e2e_web == 'true' }}
|
if: ${{ needs.pre-job.outputs.should_run_e2e_web == 'true' }}
|
||||||
runs-on: mich
|
runs-on: mich
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
working-directory: ./e2e
|
working-directory: ./e2e
|
||||||
@ -329,6 +365,7 @@ jobs:
|
|||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
with:
|
with:
|
||||||
|
persist-credentials: false
|
||||||
submodules: 'recursive'
|
submodules: 'recursive'
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
@ -362,8 +399,13 @@ jobs:
|
|||||||
needs: pre-job
|
needs: pre-job
|
||||||
if: ${{ needs.pre-job.outputs.should_run_mobile == 'true' }}
|
if: ${{ needs.pre-job.outputs.should_run_mobile == 'true' }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Setup Flutter SDK
|
- name: Setup Flutter SDK
|
||||||
uses: subosito/flutter-action@e938fdf56512cc96ef2f93601a5a40bde3801046 # v2
|
uses: subosito/flutter-action@e938fdf56512cc96ef2f93601a5a40bde3801046 # v2
|
||||||
with:
|
with:
|
||||||
@ -378,11 +420,16 @@ jobs:
|
|||||||
needs: pre-job
|
needs: pre-job
|
||||||
if: ${{ needs.pre-job.outputs.should_run_ml == 'true' }}
|
if: ${{ needs.pre-job.outputs.should_run_ml == 'true' }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
working-directory: ./machine-learning
|
working-directory: ./machine-learning
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Install uv
|
- name: Install uv
|
||||||
uses: astral-sh/setup-uv@0c5e2b8115b80b4c7c5ddf6ffdd634974642d182 # v5
|
uses: astral-sh/setup-uv@0c5e2b8115b80b4c7c5ddf6ffdd634974642d182 # v5
|
||||||
- uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5
|
- uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5
|
||||||
@ -411,6 +458,8 @@ jobs:
|
|||||||
needs: pre-job
|
needs: pre-job
|
||||||
if: ${{ needs.pre-job.outputs['should_run_.github'] == 'true' }}
|
if: ${{ needs.pre-job.outputs['should_run_.github'] == 'true' }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
working-directory: ./.github
|
working-directory: ./.github
|
||||||
@ -418,6 +467,8 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||||
@ -434,22 +485,31 @@ jobs:
|
|||||||
shellcheck:
|
shellcheck:
|
||||||
name: ShellCheck
|
name: ShellCheck
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Run ShellCheck
|
- name: Run ShellCheck
|
||||||
uses: ludeeus/action-shellcheck@master
|
uses: ludeeus/action-shellcheck@master
|
||||||
with:
|
with:
|
||||||
ignore_paths: >-
|
ignore_paths: >-
|
||||||
**/open-api/**
|
**/open-api/**
|
||||||
**/openapi/**
|
**/openapi**
|
||||||
**/node_modules/**
|
**/node_modules/**
|
||||||
|
|
||||||
generated-api-up-to-date:
|
generated-api-up-to-date:
|
||||||
name: OpenAPI Clients
|
name: OpenAPI Clients
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||||
@ -476,14 +536,18 @@ jobs:
|
|||||||
|
|
||||||
- name: Verify files have not changed
|
- name: Verify files have not changed
|
||||||
if: steps.verify-changed-files.outputs.files_changed == 'true'
|
if: steps.verify-changed-files.outputs.files_changed == 'true'
|
||||||
|
env:
|
||||||
|
CHANGED_FILES: ${{ steps.verify-changed-files.outputs.changed_files }}
|
||||||
run: |
|
run: |
|
||||||
echo "ERROR: Generated files not up to date!"
|
echo "ERROR: Generated files not up to date!"
|
||||||
echo "Changed files: ${{ steps.verify-changed-files.outputs.changed_files }}"
|
echo "Changed files: ${CHANGED_FILES}"
|
||||||
exit 1
|
exit 1
|
||||||
|
|
||||||
generated-typeorm-migrations-up-to-date:
|
generated-typeorm-migrations-up-to-date:
|
||||||
name: TypeORM Checks
|
name: TypeORM Checks
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
services:
|
services:
|
||||||
postgres:
|
postgres:
|
||||||
image: tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:739cdd626151ff1f796dc95a6591b55a714f341c737e27f045019ceabf8e8c52
|
image: tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:739cdd626151ff1f796dc95a6591b55a714f341c737e27f045019ceabf8e8c52
|
||||||
@ -505,6 +569,8 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||||
@ -521,7 +587,7 @@ jobs:
|
|||||||
run: npm run migrations:run
|
run: npm run migrations:run
|
||||||
|
|
||||||
- name: Test npm run schema:reset command works
|
- name: Test npm run schema:reset command works
|
||||||
run: npm run typeorm:schema:reset
|
run: npm run schema:reset
|
||||||
|
|
||||||
- name: Generate new migrations
|
- name: Generate new migrations
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
@ -535,9 +601,11 @@ jobs:
|
|||||||
server/src
|
server/src
|
||||||
- name: Verify migration files have not changed
|
- name: Verify migration files have not changed
|
||||||
if: steps.verify-changed-files.outputs.files_changed == 'true'
|
if: steps.verify-changed-files.outputs.files_changed == 'true'
|
||||||
|
env:
|
||||||
|
CHANGED_FILES: ${{ steps.verify-changed-files.outputs.changed_files }}
|
||||||
run: |
|
run: |
|
||||||
echo "ERROR: Generated migration files not up to date!"
|
echo "ERROR: Generated migration files not up to date!"
|
||||||
echo "Changed files: ${{ steps.verify-changed-files.outputs.changed_files }}"
|
echo "Changed files: ${CHANGED_FILES}"
|
||||||
cat ./src/*-TestMigration.ts
|
cat ./src/*-TestMigration.ts
|
||||||
exit 1
|
exit 1
|
||||||
|
|
||||||
@ -555,9 +623,11 @@ jobs:
|
|||||||
|
|
||||||
- name: Verify SQL files have not changed
|
- name: Verify SQL files have not changed
|
||||||
if: steps.verify-changed-sql-files.outputs.files_changed == 'true'
|
if: steps.verify-changed-sql-files.outputs.files_changed == 'true'
|
||||||
|
env:
|
||||||
|
CHANGED_FILES: ${{ steps.verify-changed-sql-files.outputs.changed_files }}
|
||||||
run: |
|
run: |
|
||||||
echo "ERROR: Generated SQL files not up to date!"
|
echo "ERROR: Generated SQL files not up to date!"
|
||||||
echo "Changed files: ${{ steps.verify-changed-sql-files.outputs.changed_files }}"
|
echo "Changed files: ${CHANGED_FILES}"
|
||||||
exit 1
|
exit 1
|
||||||
|
|
||||||
# mobile-integration-tests:
|
# mobile-integration-tests:
|
||||||
|
13
.github/workflows/weblate-lock.yml
vendored
13
.github/workflows/weblate-lock.yml
vendored
@ -4,30 +4,32 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
branches: [main]
|
branches: [main]
|
||||||
|
|
||||||
|
permissions: {}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
pre-job:
|
pre-job:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
outputs:
|
outputs:
|
||||||
should_run: ${{ steps.found_paths.outputs.i18n == 'true' && github.head_ref != 'chore/translations'}}
|
should_run: ${{ steps.found_paths.outputs.i18n == 'true' && github.head_ref != 'chore/translations'}}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
- id: found_paths
|
- id: found_paths
|
||||||
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3
|
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3
|
||||||
with:
|
with:
|
||||||
filters: |
|
filters: |
|
||||||
i18n:
|
i18n:
|
||||||
- 'i18n/!(en)**\.json'
|
- 'i18n/!(en)**\.json'
|
||||||
- name: Debug
|
|
||||||
run: |
|
|
||||||
echo "Should run: ${{ steps.found_paths.outputs.i18n == 'true' && github.head_ref != 'chore/translations'}}"
|
|
||||||
echo "Found i18n paths: ${{ steps.found_paths.outputs.i18n }}"
|
|
||||||
echo "Head ref: ${{ github.head_ref }}"
|
|
||||||
|
|
||||||
enforce-lock:
|
enforce-lock:
|
||||||
name: Check Weblate Lock
|
name: Check Weblate Lock
|
||||||
needs: [pre-job]
|
needs: [pre-job]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
permissions: {}
|
||||||
if: ${{ needs.pre-job.outputs.should_run == 'true' }}
|
if: ${{ needs.pre-job.outputs.should_run == 'true' }}
|
||||||
steps:
|
steps:
|
||||||
- name: Check weblate lock
|
- name: Check weblate lock
|
||||||
@ -47,6 +49,7 @@ jobs:
|
|||||||
name: Weblate Lock Check Success
|
name: Weblate Lock Check Success
|
||||||
needs: [enforce-lock]
|
needs: [enforce-lock]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
permissions: {}
|
||||||
if: always()
|
if: always()
|
||||||
steps:
|
steps:
|
||||||
- name: Any jobs failed?
|
- name: Any jobs failed?
|
||||||
|
@ -1,14 +1,14 @@
|
|||||||
# Database Migrations
|
# Database Migrations
|
||||||
|
|
||||||
After making any changes in the `server/src/entities`, a database migration need to run in order to register the changes in the database. Follow the steps below to create a new migration.
|
After making any changes in the `server/src/schema`, a database migration need to run in order to register the changes in the database. Follow the steps below to create a new migration.
|
||||||
|
|
||||||
1. Run the command
|
1. Run the command
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm run typeorm:migrations:generate <migration-name>
|
npm run migrations:generate <migration-name>
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Check if the migration file makes sense.
|
2. Check if the migration file makes sense.
|
||||||
3. Move the migration file to folder `./server/src/migrations` in your code editor.
|
3. Move the migration file to folder `./server/src/schema/migrations` in your code editor.
|
||||||
|
|
||||||
The server will automatically detect `*.ts` file changes and restart. Part of the server start-up process includes running any new migrations, so it will be applied immediately.
|
The server will automatically detect `*.ts` file changes and restart. Part of the server start-up process includes running any new migrations, so it will be applied immediately.
|
||||||
|
5
docs/src/pages/errors.md
Normal file
5
docs/src/pages/errors.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
# Errors
|
||||||
|
|
||||||
|
## TypeORM Upgrade
|
||||||
|
|
||||||
|
The upgrade to Immich `v2.x.x` has a required upgrade path to `v1.132.0+`. This means it is required to start up the application at least once on version `1.132.0` (or later). Doing so will complete database schema upgrades that are required for `v2.0.0`. After Immich has successfully booted on this version, shut the system down and try the `v2.x.x` upgrade again.
|
@ -996,6 +996,7 @@
|
|||||||
"filetype": "Filetype",
|
"filetype": "Filetype",
|
||||||
"filter": "Filter",
|
"filter": "Filter",
|
||||||
"filter_people": "Filter people",
|
"filter_people": "Filter people",
|
||||||
|
"filter_places": "Filter places",
|
||||||
"find_them_fast": "Find them fast by name with search",
|
"find_them_fast": "Find them fast by name with search",
|
||||||
"fix_incorrect_match": "Fix incorrect match",
|
"fix_incorrect_match": "Fix incorrect match",
|
||||||
"folder": "Folder",
|
"folder": "Folder",
|
||||||
|
@ -35,6 +35,7 @@ linter:
|
|||||||
analyzer:
|
analyzer:
|
||||||
exclude:
|
exclude:
|
||||||
- openapi/**
|
- openapi/**
|
||||||
|
- build/**
|
||||||
- lib/generated_plugin_registrant.dart
|
- lib/generated_plugin_registrant.dart
|
||||||
- lib/**/*.g.dart
|
- lib/**/*.g.dart
|
||||||
- lib/**/*.drift.dart
|
- lib/**/*.drift.dart
|
||||||
@ -92,6 +93,9 @@ custom_lint:
|
|||||||
allowed:
|
allowed:
|
||||||
# required / wanted
|
# required / wanted
|
||||||
- lib/repositories/*_api.repository.dart
|
- lib/repositories/*_api.repository.dart
|
||||||
|
- lib/domain/models/sync_event.model.dart
|
||||||
|
- lib/{domain,infrastructure}/**/sync_stream.*
|
||||||
|
- lib/{domain,infrastructure}/**/sync_api.*
|
||||||
- lib/infrastructure/repositories/*_api.repository.dart
|
- lib/infrastructure/repositories/*_api.repository.dart
|
||||||
- lib/infrastructure/utils/*.converter.dart
|
- lib/infrastructure/utils/*.converter.dart
|
||||||
# acceptable exceptions for the time being
|
# acceptable exceptions for the time being
|
||||||
@ -144,7 +148,9 @@ dart_code_metrics:
|
|||||||
- avoid-global-state
|
- avoid-global-state
|
||||||
- avoid-inverted-boolean-checks
|
- avoid-inverted-boolean-checks
|
||||||
- avoid-late-final-reassignment
|
- avoid-late-final-reassignment
|
||||||
- avoid-local-functions
|
- avoid-local-functions:
|
||||||
|
exclude:
|
||||||
|
- test/**.dart
|
||||||
- avoid-negated-conditions
|
- avoid-negated-conditions
|
||||||
- avoid-nested-streams-and-futures
|
- avoid-nested-streams-and-futures
|
||||||
- avoid-referencing-subclasses
|
- avoid-referencing-subclasses
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
description: This file stores settings for Dart & Flutter DevTools.
|
description: This file stores settings for Dart & Flutter DevTools.
|
||||||
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
|
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
|
||||||
extensions:
|
extensions:
|
||||||
|
- drift: true
|
@ -5,5 +5,9 @@ const double downloadFailed = -2;
|
|||||||
// Number of log entries to retain on app start
|
// Number of log entries to retain on app start
|
||||||
const int kLogTruncateLimit = 250;
|
const int kLogTruncateLimit = 250;
|
||||||
|
|
||||||
|
// Sync
|
||||||
|
const int kSyncEventBatchSize = 5000;
|
||||||
|
|
||||||
|
// Hash batch limits
|
||||||
const int kBatchHashFileLimit = 128;
|
const int kBatchHashFileLimit = 128;
|
||||||
const int kBatchHashSizeLimit = 1024 * 1024 * 1024; // 1GB
|
const int kBatchHashSizeLimit = 1024 * 1024 * 1024; // 1GB
|
||||||
|
@ -1,7 +1,12 @@
|
|||||||
import 'package:immich_mobile/domain/models/sync/sync_event.model.dart';
|
import 'package:http/http.dart' as http;
|
||||||
|
import 'package:immich_mobile/domain/models/sync_event.model.dart';
|
||||||
|
|
||||||
abstract interface class ISyncApiRepository {
|
abstract interface class ISyncApiRepository {
|
||||||
Future<void> ack(String data);
|
Future<void> ack(List<String> data);
|
||||||
|
|
||||||
Stream<List<SyncEvent>> watchUserSyncEvent();
|
Future<void> streamChanges(
|
||||||
|
Function(List<SyncEvent>, Function() abort) onData, {
|
||||||
|
int batchSize,
|
||||||
|
http.Client? httpClient,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
18
mobile/lib/domain/interfaces/sync_stream.interface.dart
Normal file
18
mobile/lib/domain/interfaces/sync_stream.interface.dart
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import 'package:immich_mobile/domain/interfaces/db.interface.dart';
|
||||||
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
|
abstract interface class ISyncStreamRepository implements IDatabaseRepository {
|
||||||
|
Future<void> updateUsersV1(Iterable<SyncUserV1> data);
|
||||||
|
Future<void> deleteUsersV1(Iterable<SyncUserDeleteV1> data);
|
||||||
|
|
||||||
|
Future<void> updatePartnerV1(Iterable<SyncPartnerV1> data);
|
||||||
|
Future<void> deletePartnerV1(Iterable<SyncPartnerDeleteV1> data);
|
||||||
|
|
||||||
|
Future<void> updateAssetsV1(Iterable<SyncAssetV1> data);
|
||||||
|
Future<void> deleteAssetsV1(Iterable<SyncAssetDeleteV1> data);
|
||||||
|
Future<void> updateAssetsExifV1(Iterable<SyncAssetExifV1> data);
|
||||||
|
|
||||||
|
Future<void> updatePartnerAssetsV1(Iterable<SyncAssetV1> data);
|
||||||
|
Future<void> deletePartnerAssetsV1(Iterable<SyncAssetDeleteV1> data);
|
||||||
|
Future<void> updatePartnerAssetsExifV1(Iterable<SyncAssetExifV1> data);
|
||||||
|
}
|
@ -1,14 +0,0 @@
|
|||||||
class SyncEvent {
|
|
||||||
// dynamic
|
|
||||||
final dynamic data;
|
|
||||||
|
|
||||||
final String ack;
|
|
||||||
|
|
||||||
SyncEvent({
|
|
||||||
required this.data,
|
|
||||||
required this.ack,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
String toString() => 'SyncEvent(data: $data, ack: $ack)';
|
|
||||||
}
|
|
13
mobile/lib/domain/models/sync_event.model.dart
Normal file
13
mobile/lib/domain/models/sync_event.model.dart
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
|
class SyncEvent {
|
||||||
|
final SyncEntityType type;
|
||||||
|
// ignore: avoid-dynamic
|
||||||
|
final dynamic data;
|
||||||
|
final String ack;
|
||||||
|
|
||||||
|
const SyncEvent({required this.type, required this.data, required this.ack});
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => 'SyncEvent(type: $type, data: $data, ack: $ack)';
|
||||||
|
}
|
@ -1,49 +1,92 @@
|
|||||||
|
// ignore_for_file: avoid-passing-async-when-sync-expected
|
||||||
|
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:immich_mobile/domain/interfaces/sync_api.interface.dart';
|
import 'package:immich_mobile/domain/interfaces/sync_api.interface.dart';
|
||||||
|
import 'package:immich_mobile/domain/interfaces/sync_stream.interface.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/sync_event.model.dart';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
import 'package:openapi/api.dart';
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
class SyncStreamService {
|
class SyncStreamService {
|
||||||
|
final Logger _logger = Logger('SyncStreamService');
|
||||||
|
|
||||||
final ISyncApiRepository _syncApiRepository;
|
final ISyncApiRepository _syncApiRepository;
|
||||||
|
final ISyncStreamRepository _syncStreamRepository;
|
||||||
|
final bool Function()? _cancelChecker;
|
||||||
|
|
||||||
SyncStreamService(this._syncApiRepository);
|
SyncStreamService({
|
||||||
|
required ISyncApiRepository syncApiRepository,
|
||||||
|
required ISyncStreamRepository syncStreamRepository,
|
||||||
|
bool Function()? cancelChecker,
|
||||||
|
}) : _syncApiRepository = syncApiRepository,
|
||||||
|
_syncStreamRepository = syncStreamRepository,
|
||||||
|
_cancelChecker = cancelChecker;
|
||||||
|
|
||||||
StreamSubscription? _userSyncSubscription;
|
bool get isCancelled => _cancelChecker?.call() ?? false;
|
||||||
|
|
||||||
void syncUsers() {
|
Future<void> sync() => _syncApiRepository.streamChanges(_handleEvents);
|
||||||
_userSyncSubscription =
|
|
||||||
_syncApiRepository.watchUserSyncEvent().listen((events) async {
|
|
||||||
for (final event in events) {
|
|
||||||
if (event.data is SyncUserV1) {
|
|
||||||
final data = event.data as SyncUserV1;
|
|
||||||
debugPrint("User Update: $data");
|
|
||||||
|
|
||||||
// final user = await _userRepository.get(data.id);
|
Future<void> _handleEvents(List<SyncEvent> events, Function() abort) async {
|
||||||
|
List<SyncEvent> items = [];
|
||||||
// if (user == null) {
|
for (final event in events) {
|
||||||
// continue;
|
if (isCancelled) {
|
||||||
// }
|
_logger.warning("Sync stream cancelled");
|
||||||
|
abort();
|
||||||
// user.name = data.name;
|
return;
|
||||||
// user.email = data.email;
|
|
||||||
// user.updatedAt = DateTime.now();
|
|
||||||
|
|
||||||
// await _userRepository.update(user);
|
|
||||||
// await _syncApiRepository.ack(event.ack);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.data is SyncUserDeleteV1) {
|
|
||||||
final data = event.data as SyncUserDeleteV1;
|
|
||||||
|
|
||||||
debugPrint("User delete: $data");
|
|
||||||
// await _syncApiRepository.ack(event.ack);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
if (event.type != items.firstOrNull?.type) {
|
||||||
|
await _processBatch(items);
|
||||||
|
}
|
||||||
|
|
||||||
|
items.add(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
await _processBatch(items);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> dispose() async {
|
Future<void> _processBatch(List<SyncEvent> batch) async {
|
||||||
await _userSyncSubscription?.cancel();
|
if (batch.isEmpty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final type = batch.first.type;
|
||||||
|
await _handleSyncData(type, batch.map((e) => e.data));
|
||||||
|
await _syncApiRepository.ack([batch.last.ack]);
|
||||||
|
batch.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _handleSyncData(
|
||||||
|
SyncEntityType type,
|
||||||
|
// ignore: avoid-dynamic
|
||||||
|
Iterable<dynamic> data,
|
||||||
|
) async {
|
||||||
|
_logger.fine("Processing sync data for $type of length ${data.length}");
|
||||||
|
// ignore: prefer-switch-expression
|
||||||
|
switch (type) {
|
||||||
|
case SyncEntityType.userV1:
|
||||||
|
return _syncStreamRepository.updateUsersV1(data.cast());
|
||||||
|
case SyncEntityType.userDeleteV1:
|
||||||
|
return _syncStreamRepository.deleteUsersV1(data.cast());
|
||||||
|
case SyncEntityType.partnerV1:
|
||||||
|
return _syncStreamRepository.updatePartnerV1(data.cast());
|
||||||
|
case SyncEntityType.partnerDeleteV1:
|
||||||
|
return _syncStreamRepository.deletePartnerV1(data.cast());
|
||||||
|
case SyncEntityType.assetV1:
|
||||||
|
return _syncStreamRepository.updateAssetsV1(data.cast());
|
||||||
|
case SyncEntityType.assetDeleteV1:
|
||||||
|
return _syncStreamRepository.deleteAssetsV1(data.cast());
|
||||||
|
case SyncEntityType.assetExifV1:
|
||||||
|
return _syncStreamRepository.updateAssetsExifV1(data.cast());
|
||||||
|
case SyncEntityType.partnerAssetV1:
|
||||||
|
return _syncStreamRepository.updatePartnerAssetsV1(data.cast());
|
||||||
|
case SyncEntityType.partnerAssetDeleteV1:
|
||||||
|
return _syncStreamRepository.deletePartnerAssetsV1(data.cast());
|
||||||
|
case SyncEntityType.partnerAssetExifV1:
|
||||||
|
return _syncStreamRepository.updatePartnerAssetsExifV1(data.cast());
|
||||||
|
default:
|
||||||
|
_logger.warning("Unknown sync data type: $type");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
39
mobile/lib/domain/utils/background_sync.dart
Normal file
39
mobile/lib/domain/utils/background_sync.dart
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
// ignore_for_file: avoid-passing-async-when-sync-expected
|
||||||
|
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/sync_stream.provider.dart';
|
||||||
|
import 'package:immich_mobile/utils/isolate.dart';
|
||||||
|
import 'package:worker_manager/worker_manager.dart';
|
||||||
|
|
||||||
|
class BackgroundSyncManager {
|
||||||
|
Cancelable<void>? _syncTask;
|
||||||
|
|
||||||
|
BackgroundSyncManager();
|
||||||
|
|
||||||
|
Future<void> cancel() {
|
||||||
|
final futures = <Future>[];
|
||||||
|
|
||||||
|
if (_syncTask != null) {
|
||||||
|
futures.add(_syncTask!.future);
|
||||||
|
}
|
||||||
|
_syncTask?.cancel();
|
||||||
|
_syncTask = null;
|
||||||
|
|
||||||
|
return Future.wait(futures);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> sync() {
|
||||||
|
if (_syncTask != null) {
|
||||||
|
return _syncTask!.future;
|
||||||
|
}
|
||||||
|
|
||||||
|
_syncTask = runInIsolateGentle(
|
||||||
|
computation: (ref) => ref.read(syncStreamServiceProvider).sync(),
|
||||||
|
);
|
||||||
|
_syncTask!.whenComplete(() {
|
||||||
|
_syncTask = null;
|
||||||
|
});
|
||||||
|
return _syncTask!.future;
|
||||||
|
}
|
||||||
|
}
|
@ -1,3 +1,7 @@
|
|||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
import 'package:uuid/parsing.dart';
|
||||||
|
|
||||||
extension StringExtension on String {
|
extension StringExtension on String {
|
||||||
String capitalize() {
|
String capitalize() {
|
||||||
return split(" ")
|
return split(" ")
|
||||||
@ -29,3 +33,8 @@ extension DurationExtension on String {
|
|||||||
return int.parse(this);
|
return int.parse(this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension UUIDExtension on String {
|
||||||
|
Uint8List toUuidByte({bool shouldValidate = false}) =>
|
||||||
|
UuidParsing.parseAsByteList(this, validate: shouldValidate);
|
||||||
|
}
|
||||||
|
@ -1,55 +1,72 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:immich_mobile/domain/interfaces/sync_api.interface.dart';
|
|
||||||
import 'package:immich_mobile/domain/models/sync/sync_event.model.dart';
|
|
||||||
import 'package:immich_mobile/services/api.service.dart';
|
|
||||||
import 'package:openapi/api.dart';
|
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
|
import 'package:immich_mobile/constants/constants.dart';
|
||||||
|
import 'package:immich_mobile/domain/interfaces/sync_api.interface.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/sync_event.model.dart';
|
||||||
|
import 'package:immich_mobile/services/api.service.dart';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
class SyncApiRepository implements ISyncApiRepository {
|
class SyncApiRepository implements ISyncApiRepository {
|
||||||
|
final Logger _logger = Logger('SyncApiRepository');
|
||||||
final ApiService _api;
|
final ApiService _api;
|
||||||
const SyncApiRepository(this._api);
|
SyncApiRepository(this._api);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Stream<List<SyncEvent>> watchUserSyncEvent() {
|
Future<void> ack(List<String> data) {
|
||||||
return _getSyncStream(
|
return _api.syncApi.sendSyncAck(SyncAckSetDto(acks: data));
|
||||||
SyncStreamDto(types: [SyncRequestType.usersV1]),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> ack(String data) {
|
Future<void> streamChanges(
|
||||||
return _api.syncApi.sendSyncAck(SyncAckSetDto(acks: [data]));
|
Function(List<SyncEvent>, Function() abort) onData, {
|
||||||
}
|
int batchSize = kSyncEventBatchSize,
|
||||||
|
http.Client? httpClient,
|
||||||
Stream<List<SyncEvent>> _getSyncStream(
|
}) async {
|
||||||
SyncStreamDto dto, {
|
// ignore: avoid-unused-assignment
|
||||||
int batchSize = 5000,
|
final stopwatch = Stopwatch()..start();
|
||||||
}) async* {
|
final client = httpClient ?? http.Client();
|
||||||
final client = http.Client();
|
|
||||||
final endpoint = "${_api.apiClient.basePath}/sync/stream";
|
final endpoint = "${_api.apiClient.basePath}/sync/stream";
|
||||||
|
|
||||||
final headers = <String, String>{
|
final headers = {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'Accept': 'application/jsonlines+json',
|
'Accept': 'application/jsonlines+json',
|
||||||
};
|
};
|
||||||
|
|
||||||
final queryParams = <QueryParam>[];
|
|
||||||
final headerParams = <String, String>{};
|
final headerParams = <String, String>{};
|
||||||
await _api.applyToParams(queryParams, headerParams);
|
await _api.applyToParams([], headerParams);
|
||||||
headers.addAll(headerParams);
|
headers.addAll(headerParams);
|
||||||
|
|
||||||
final request = http.Request('POST', Uri.parse(endpoint));
|
final request = http.Request('POST', Uri.parse(endpoint));
|
||||||
request.headers.addAll(headers);
|
request.headers.addAll(headers);
|
||||||
request.body = jsonEncode(dto.toJson());
|
request.body = jsonEncode(
|
||||||
|
SyncStreamDto(
|
||||||
|
types: [
|
||||||
|
SyncRequestType.usersV1,
|
||||||
|
SyncRequestType.partnersV1,
|
||||||
|
SyncRequestType.assetsV1,
|
||||||
|
SyncRequestType.partnerAssetsV1,
|
||||||
|
SyncRequestType.assetExifsV1,
|
||||||
|
SyncRequestType.partnerAssetExifsV1,
|
||||||
|
],
|
||||||
|
).toJson(),
|
||||||
|
);
|
||||||
|
|
||||||
String previousChunk = '';
|
String previousChunk = '';
|
||||||
List<String> lines = [];
|
List<String> lines = [];
|
||||||
|
|
||||||
|
bool shouldAbort = false;
|
||||||
|
|
||||||
|
void abort() {
|
||||||
|
_logger.warning("Abort requested, stopping sync stream");
|
||||||
|
shouldAbort = true;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final response = await client.send(request);
|
final response =
|
||||||
|
await client.send(request).timeout(const Duration(seconds: 20));
|
||||||
|
|
||||||
if (response.statusCode != 200) {
|
if (response.statusCode != 200) {
|
||||||
final errorBody = await response.stream.bytesToString();
|
final errorBody = await response.stream.bytesToString();
|
||||||
@ -60,8 +77,12 @@ class SyncApiRepository implements ISyncApiRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await for (final chunk in response.stream.transform(utf8.decoder)) {
|
await for (final chunk in response.stream.transform(utf8.decoder)) {
|
||||||
|
if (shouldAbort) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
previousChunk += chunk;
|
previousChunk += chunk;
|
||||||
final parts = previousChunk.split('\n');
|
final parts = previousChunk.toString().split('\n');
|
||||||
previousChunk = parts.removeLast();
|
previousChunk = parts.removeLast();
|
||||||
lines.addAll(parts);
|
lines.addAll(parts);
|
||||||
|
|
||||||
@ -69,44 +90,59 @@ class SyncApiRepository implements ISyncApiRepository {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
yield await compute(_parseSyncResponse, lines);
|
await onData(_parseLines(lines), abort);
|
||||||
lines.clear();
|
lines.clear();
|
||||||
}
|
}
|
||||||
} finally {
|
|
||||||
if (lines.isNotEmpty) {
|
if (lines.isNotEmpty && !shouldAbort) {
|
||||||
yield await compute(_parseSyncResponse, lines);
|
await onData(_parseLines(lines), abort);
|
||||||
}
|
}
|
||||||
|
} catch (error, stack) {
|
||||||
|
_logger.severe("error processing stream", error, stack);
|
||||||
|
return Future.error(error, stack);
|
||||||
|
} finally {
|
||||||
client.close();
|
client.close();
|
||||||
}
|
}
|
||||||
|
stopwatch.stop();
|
||||||
|
_logger
|
||||||
|
.info("Remote Sync completed in ${stopwatch.elapsed.inMilliseconds}ms");
|
||||||
|
}
|
||||||
|
|
||||||
|
List<SyncEvent> _parseLines(List<String> lines) {
|
||||||
|
final List<SyncEvent> data = [];
|
||||||
|
|
||||||
|
for (final line in lines) {
|
||||||
|
try {
|
||||||
|
final jsonData = jsonDecode(line);
|
||||||
|
final type = SyncEntityType.fromJson(jsonData['type'])!;
|
||||||
|
final dataJson = jsonData['data'];
|
||||||
|
final ack = jsonData['ack'];
|
||||||
|
final converter = _kResponseMap[type];
|
||||||
|
if (converter == null) {
|
||||||
|
_logger.warning("[_parseSyncResponse] Unknown type $type");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
data.add(SyncEvent(type: type, data: converter(dataJson), ack: ack));
|
||||||
|
} catch (error, stack) {
|
||||||
|
_logger.severe("[_parseSyncResponse] Error parsing json", error, stack);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ignore: avoid-dynamic
|
||||||
const _kResponseMap = <SyncEntityType, Function(dynamic)>{
|
const _kResponseMap = <SyncEntityType, Function(dynamic)>{
|
||||||
SyncEntityType.userV1: SyncUserV1.fromJson,
|
SyncEntityType.userV1: SyncUserV1.fromJson,
|
||||||
SyncEntityType.userDeleteV1: SyncUserDeleteV1.fromJson,
|
SyncEntityType.userDeleteV1: SyncUserDeleteV1.fromJson,
|
||||||
|
SyncEntityType.partnerV1: SyncPartnerV1.fromJson,
|
||||||
|
SyncEntityType.partnerDeleteV1: SyncPartnerDeleteV1.fromJson,
|
||||||
|
SyncEntityType.assetV1: SyncAssetV1.fromJson,
|
||||||
|
SyncEntityType.assetDeleteV1: SyncAssetDeleteV1.fromJson,
|
||||||
|
SyncEntityType.assetExifV1: SyncAssetExifV1.fromJson,
|
||||||
|
SyncEntityType.partnerAssetV1: SyncAssetV1.fromJson,
|
||||||
|
SyncEntityType.partnerAssetDeleteV1: SyncAssetDeleteV1.fromJson,
|
||||||
|
SyncEntityType.partnerAssetExifV1: SyncAssetExifV1.fromJson,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Need to be outside of the class to be able to use compute
|
|
||||||
List<SyncEvent> _parseSyncResponse(List<String> lines) {
|
|
||||||
final List<SyncEvent> data = [];
|
|
||||||
|
|
||||||
for (var line in lines) {
|
|
||||||
try {
|
|
||||||
final jsonData = jsonDecode(line);
|
|
||||||
final type = SyncEntityType.fromJson(jsonData['type'])!;
|
|
||||||
final dataJson = jsonData['data'];
|
|
||||||
final ack = jsonData['ack'];
|
|
||||||
final converter = _kResponseMap[type];
|
|
||||||
if (converter == null) {
|
|
||||||
debugPrint("[_parseSyncReponse] Unknown type $type");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
data.add(SyncEvent(data: converter(dataJson), ack: ack));
|
|
||||||
} catch (error, stack) {
|
|
||||||
debugPrint("[_parseSyncReponse] Error parsing json $error $stack");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
@ -0,0 +1,134 @@
|
|||||||
|
import 'package:drift/drift.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:immich_mobile/domain/interfaces/sync_stream.interface.dart';
|
||||||
|
import 'package:immich_mobile/extensions/string_extensions.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/entities/partner.entity.drift.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
|
class DriftSyncStreamRepository extends DriftDatabaseRepository
|
||||||
|
implements ISyncStreamRepository {
|
||||||
|
final Logger _logger = Logger('DriftSyncStreamRepository');
|
||||||
|
final Drift _db;
|
||||||
|
|
||||||
|
DriftSyncStreamRepository(super.db) : _db = db;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> deleteUsersV1(Iterable<SyncUserDeleteV1> data) async {
|
||||||
|
try {
|
||||||
|
await _db.batch((batch) {
|
||||||
|
for (final user in data) {
|
||||||
|
batch.delete(
|
||||||
|
_db.userEntity,
|
||||||
|
UserEntityCompanion(id: Value(user.userId.toUuidByte())),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error, stack) {
|
||||||
|
_logger.severe('Error while processing SyncUserDeleteV1', error, stack);
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> updateUsersV1(Iterable<SyncUserV1> data) async {
|
||||||
|
try {
|
||||||
|
await _db.batch((batch) {
|
||||||
|
for (final user in data) {
|
||||||
|
final companion = UserEntityCompanion(
|
||||||
|
name: Value(user.name),
|
||||||
|
email: Value(user.email),
|
||||||
|
);
|
||||||
|
|
||||||
|
batch.insert(
|
||||||
|
_db.userEntity,
|
||||||
|
companion.copyWith(id: Value(user.id.toUuidByte())),
|
||||||
|
onConflict: DoUpdate((_) => companion),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error, stack) {
|
||||||
|
_logger.severe('Error while processing SyncUserV1', error, stack);
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> deletePartnerV1(Iterable<SyncPartnerDeleteV1> data) async {
|
||||||
|
try {
|
||||||
|
await _db.batch((batch) {
|
||||||
|
for (final partner in data) {
|
||||||
|
batch.delete(
|
||||||
|
_db.partnerEntity,
|
||||||
|
PartnerEntityCompanion(
|
||||||
|
sharedById: Value(partner.sharedById.toUuidByte()),
|
||||||
|
sharedWithId: Value(partner.sharedWithId.toUuidByte()),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (e, s) {
|
||||||
|
_logger.severe('Error while processing SyncPartnerDeleteV1', e, s);
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> updatePartnerV1(Iterable<SyncPartnerV1> data) async {
|
||||||
|
try {
|
||||||
|
await _db.batch((batch) {
|
||||||
|
for (final partner in data) {
|
||||||
|
final companion =
|
||||||
|
PartnerEntityCompanion(inTimeline: Value(partner.inTimeline));
|
||||||
|
|
||||||
|
batch.insert(
|
||||||
|
_db.partnerEntity,
|
||||||
|
companion.copyWith(
|
||||||
|
sharedById: Value(partner.sharedById.toUuidByte()),
|
||||||
|
sharedWithId: Value(partner.sharedWithId.toUuidByte()),
|
||||||
|
),
|
||||||
|
onConflict: DoUpdate((_) => companion),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (e, s) {
|
||||||
|
_logger.severe('Error while processing SyncPartnerV1', e, s);
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assets
|
||||||
|
@override
|
||||||
|
Future<void> updateAssetsV1(Iterable<SyncAssetV1> data) async {
|
||||||
|
debugPrint("updateAssetsV1 - ${data.length}");
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> deleteAssetsV1(Iterable<SyncAssetDeleteV1> data) async {
|
||||||
|
debugPrint("deleteAssetsV1 - ${data.length}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Partner Assets
|
||||||
|
@override
|
||||||
|
Future<void> updatePartnerAssetsV1(Iterable<SyncAssetV1> data) async {
|
||||||
|
debugPrint("updatePartnerAssetsV1 - ${data.length}");
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> deletePartnerAssetsV1(Iterable<SyncAssetDeleteV1> data) async {
|
||||||
|
debugPrint("deletePartnerAssetsV1 - ${data.length}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// EXIF
|
||||||
|
@override
|
||||||
|
Future<void> updateAssetsExifV1(Iterable<SyncAssetExifV1> data) async {
|
||||||
|
debugPrint("updateAssetsExifV1 - ${data.length}");
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> updatePartnerAssetsExifV1(Iterable<SyncAssetExifV1> data) async {
|
||||||
|
debugPrint("updatePartnerAssetsExifV1 - ${data.length}");
|
||||||
|
}
|
||||||
|
}
|
@ -11,6 +11,7 @@ import 'package:flutter_displaymode/flutter_displaymode.dart';
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/constants/locales.dart';
|
import 'package:immich_mobile/constants/locales.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
|
import 'package:immich_mobile/generated/codegen_loader.g.dart';
|
||||||
import 'package:immich_mobile/providers/app_life_cycle.provider.dart';
|
import 'package:immich_mobile/providers/app_life_cycle.provider.dart';
|
||||||
import 'package:immich_mobile/providers/asset_viewer/share_intent_upload.provider.dart';
|
import 'package:immich_mobile/providers/asset_viewer/share_intent_upload.provider.dart';
|
||||||
import 'package:immich_mobile/providers/db.provider.dart';
|
import 'package:immich_mobile/providers/db.provider.dart';
|
||||||
@ -31,13 +32,15 @@ import 'package:immich_mobile/utils/migration.dart';
|
|||||||
import 'package:intl/date_symbol_data_local.dart';
|
import 'package:intl/date_symbol_data_local.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:timezone/data/latest.dart';
|
import 'package:timezone/data/latest.dart';
|
||||||
import 'package:immich_mobile/generated/codegen_loader.g.dart';
|
import 'package:worker_manager/worker_manager.dart';
|
||||||
|
|
||||||
void main() async {
|
void main() async {
|
||||||
ImmichWidgetsBinding();
|
ImmichWidgetsBinding();
|
||||||
final db = await Bootstrap.initIsar();
|
final db = await Bootstrap.initIsar();
|
||||||
await Bootstrap.initDomain(db);
|
await Bootstrap.initDomain(db);
|
||||||
await initApp();
|
await initApp();
|
||||||
|
// Warm-up isolate pool for worker manager
|
||||||
|
await workerManager.init(dynamicSpawning: true);
|
||||||
await migrateDatabaseIfNeeded(db);
|
await migrateDatabaseIfNeeded(db);
|
||||||
HttpOverrides.global = HttpSSLCertOverride();
|
HttpOverrides.global = HttpSSLCertOverride();
|
||||||
|
|
||||||
|
@ -4,11 +4,11 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/extensions/theme_extensions.dart';
|
|
||||||
import 'package:immich_mobile/providers/search/people.provider.dart';
|
import 'package:immich_mobile/providers/search/people.provider.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
import 'package:immich_mobile/services/api.service.dart';
|
import 'package:immich_mobile/services/api.service.dart';
|
||||||
import 'package:immich_mobile/utils/image_url_builder.dart';
|
import 'package:immich_mobile/utils/image_url_builder.dart';
|
||||||
|
import 'package:immich_mobile/widgets/common/search_field.dart';
|
||||||
import 'package:immich_mobile/widgets/search/person_name_edit_form.dart';
|
import 'package:immich_mobile/widgets/search/person_name_edit_form.dart';
|
||||||
|
|
||||||
@RoutePage()
|
@RoutePage()
|
||||||
@ -42,47 +42,12 @@ class PeopleCollectionPage extends HookConsumerWidget {
|
|||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
automaticallyImplyLeading: search.value == null,
|
automaticallyImplyLeading: search.value == null,
|
||||||
title: search.value != null
|
title: search.value != null
|
||||||
? TextField(
|
? SearchField(
|
||||||
focusNode: formFocus,
|
focusNode: formFocus,
|
||||||
onTapOutside: (_) => formFocus.unfocus(),
|
onTapOutside: (_) => formFocus.unfocus(),
|
||||||
onChanged: (value) => search.value = value,
|
onChanged: (value) => search.value = value,
|
||||||
decoration: InputDecoration(
|
filled: true,
|
||||||
contentPadding: const EdgeInsets.only(left: 24),
|
hintText: 'filter_people'.tr(),
|
||||||
filled: true,
|
|
||||||
fillColor: context.primaryColor.withValues(alpha: 0.1),
|
|
||||||
hintStyle: context.textTheme.bodyLarge?.copyWith(
|
|
||||||
color: context.themeData.colorScheme.onSurfaceSecondary,
|
|
||||||
),
|
|
||||||
border: OutlineInputBorder(
|
|
||||||
borderRadius: BorderRadius.circular(25),
|
|
||||||
borderSide: BorderSide(
|
|
||||||
color: context.colorScheme.surfaceContainerHighest,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
enabledBorder: OutlineInputBorder(
|
|
||||||
borderRadius: BorderRadius.circular(25),
|
|
||||||
borderSide: BorderSide(
|
|
||||||
color: context.colorScheme.surfaceContainerHighest,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
disabledBorder: OutlineInputBorder(
|
|
||||||
borderRadius: BorderRadius.circular(25),
|
|
||||||
borderSide: BorderSide(
|
|
||||||
color: context.colorScheme.surfaceContainerHighest,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
focusedBorder: OutlineInputBorder(
|
|
||||||
borderRadius: BorderRadius.circular(25),
|
|
||||||
borderSide: BorderSide(
|
|
||||||
color: context.colorScheme.primary.withAlpha(150),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
prefixIcon: Icon(
|
|
||||||
Icons.search_rounded,
|
|
||||||
color: context.colorScheme.primary,
|
|
||||||
),
|
|
||||||
hintText: 'filter_people'.tr(),
|
|
||||||
),
|
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
)
|
)
|
||||||
: Text('people'.tr()),
|
: Text('people'.tr()),
|
||||||
|
@ -2,6 +2,7 @@ import 'package:auto_route/auto_route.dart';
|
|||||||
import 'package:cached_network_image/cached_network_image.dart';
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||||
@ -12,6 +13,7 @@ import 'package:immich_mobile/pages/common/large_leading_tile.dart';
|
|||||||
import 'package:immich_mobile/providers/search/search_page_state.provider.dart';
|
import 'package:immich_mobile/providers/search/search_page_state.provider.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
import 'package:immich_mobile/services/api.service.dart';
|
import 'package:immich_mobile/services/api.service.dart';
|
||||||
|
import 'package:immich_mobile/widgets/common/search_field.dart';
|
||||||
import 'package:immich_mobile/widgets/map/map_thumbnail.dart';
|
import 'package:immich_mobile/widgets/map/map_thumbnail.dart';
|
||||||
import 'package:maplibre_gl/maplibre_gl.dart';
|
import 'package:maplibre_gl/maplibre_gl.dart';
|
||||||
|
|
||||||
@ -21,34 +23,62 @@ class PlacesCollectionPage extends HookConsumerWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final places = ref.watch(getAllPlacesProvider);
|
final places = ref.watch(getAllPlacesProvider);
|
||||||
|
final formFocus = useFocusNode();
|
||||||
|
final ValueNotifier<String?> search = useState(null);
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text('places'.tr()),
|
automaticallyImplyLeading: search.value == null,
|
||||||
|
title: search.value != null
|
||||||
|
? SearchField(
|
||||||
|
autofocus: true,
|
||||||
|
filled: true,
|
||||||
|
focusNode: formFocus,
|
||||||
|
onChanged: (value) => search.value = value,
|
||||||
|
onTapOutside: (_) => formFocus.unfocus(),
|
||||||
|
hintText: 'filter_places'.tr(),
|
||||||
|
)
|
||||||
|
: Text('places'.tr()),
|
||||||
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(search.value != null ? Icons.close : Icons.search),
|
||||||
|
onPressed: () {
|
||||||
|
search.value = search.value == null ? '' : null;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
body: ListView(
|
body: ListView(
|
||||||
shrinkWrap: true,
|
shrinkWrap: true,
|
||||||
children: [
|
children: [
|
||||||
Padding(
|
if (search.value == null)
|
||||||
padding: const EdgeInsets.all(16.0),
|
Padding(
|
||||||
child: SizedBox(
|
padding: const EdgeInsets.all(16.0),
|
||||||
height: 200,
|
child: SizedBox(
|
||||||
width: context.width,
|
height: 200,
|
||||||
child: MapThumbnail(
|
width: context.width,
|
||||||
onTap: (_, __) => context.pushRoute(const MapRoute()),
|
child: MapThumbnail(
|
||||||
zoom: 8,
|
onTap: (_, __) => context.pushRoute(const MapRoute()),
|
||||||
centre: const LatLng(
|
zoom: 8,
|
||||||
21.44950,
|
centre: const LatLng(
|
||||||
-157.91959,
|
21.44950,
|
||||||
|
-157.91959,
|
||||||
|
),
|
||||||
|
showAttribution: false,
|
||||||
|
themeMode:
|
||||||
|
context.isDarkTheme ? ThemeMode.dark : ThemeMode.light,
|
||||||
),
|
),
|
||||||
showAttribution: false,
|
|
||||||
themeMode:
|
|
||||||
context.isDarkTheme ? ThemeMode.dark : ThemeMode.light,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
places.when(
|
places.when(
|
||||||
data: (places) {
|
data: (places) {
|
||||||
|
if (search.value != null) {
|
||||||
|
places = places.where((place) {
|
||||||
|
return place.label
|
||||||
|
.toLowerCase()
|
||||||
|
.contains(search.value!.toLowerCase());
|
||||||
|
}).toList();
|
||||||
|
}
|
||||||
return ListView.builder(
|
return ListView.builder(
|
||||||
shrinkWrap: true,
|
shrinkWrap: true,
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
|
8
mobile/lib/providers/background_sync.provider.dart
Normal file
8
mobile/lib/providers/background_sync.provider.dart
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/domain/utils/background_sync.dart';
|
||||||
|
|
||||||
|
final backgroundSyncProvider = Provider<BackgroundSyncManager>((ref) {
|
||||||
|
final manager = BackgroundSyncManager();
|
||||||
|
ref.onDispose(manager.cancel);
|
||||||
|
return manager;
|
||||||
|
});
|
12
mobile/lib/providers/infrastructure/cancel.provider.dart
Normal file
12
mobile/lib/providers/infrastructure/cancel.provider.dart
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
|
||||||
|
/// Provider holding a boolean function that returns true when cancellation is requested.
|
||||||
|
/// A computation running in the isolate uses the function to implement cooperative cancellation.
|
||||||
|
final cancellationProvider = Provider<bool Function()>(
|
||||||
|
// This will be overridden in the isolate's container.
|
||||||
|
// Throwing ensures it's not used without an override.
|
||||||
|
(ref) => throw UnimplementedError(
|
||||||
|
"cancellationProvider must be overridden in the isolate's ProviderContainer and not to be used in the root isolate",
|
||||||
|
),
|
||||||
|
name: 'cancellationProvider',
|
||||||
|
);
|
@ -1,4 +1,7 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||||
import 'package:isar/isar.dart';
|
import 'package:isar/isar.dart';
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
|
||||||
@ -6,3 +9,9 @@ part 'db.provider.g.dart';
|
|||||||
|
|
||||||
@Riverpod(keepAlive: true)
|
@Riverpod(keepAlive: true)
|
||||||
Isar isar(Ref ref) => throw UnimplementedError('isar');
|
Isar isar(Ref ref) => throw UnimplementedError('isar');
|
||||||
|
|
||||||
|
final driftProvider = Provider<Drift>((ref) {
|
||||||
|
final drift = Drift();
|
||||||
|
ref.onDispose(() => unawaited(drift.close()));
|
||||||
|
return drift;
|
||||||
|
});
|
||||||
|
@ -1,24 +1,23 @@
|
|||||||
import 'dart:async';
|
|
||||||
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/domain/services/sync_stream.service.dart';
|
import 'package:immich_mobile/domain/services/sync_stream.service.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/sync_api.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/sync_api.repository.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/repositories/sync_stream.repository.dart';
|
||||||
import 'package:immich_mobile/providers/api.provider.dart';
|
import 'package:immich_mobile/providers/api.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/cancel.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
|
||||||
|
|
||||||
final syncStreamServiceProvider = Provider(
|
final syncStreamServiceProvider = Provider(
|
||||||
(ref) {
|
(ref) => SyncStreamService(
|
||||||
final instance = SyncStreamService(
|
syncApiRepository: ref.watch(syncApiRepositoryProvider),
|
||||||
ref.watch(syncApiRepositoryProvider),
|
syncStreamRepository: ref.watch(syncStreamRepositoryProvider),
|
||||||
);
|
cancelChecker: ref.watch(cancellationProvider),
|
||||||
|
),
|
||||||
ref.onDispose(() => unawaited(instance.dispose()));
|
|
||||||
|
|
||||||
return instance;
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
|
||||||
final syncApiRepositoryProvider = Provider(
|
final syncApiRepositoryProvider = Provider(
|
||||||
(ref) => SyncApiRepository(
|
(ref) => SyncApiRepository(ref.watch(apiServiceProvider)),
|
||||||
ref.watch(apiServiceProvider),
|
);
|
||||||
),
|
|
||||||
|
final syncStreamRepositoryProvider = Provider(
|
||||||
|
(ref) => DriftSyncStreamRepository(ref.watch(driftProvider)),
|
||||||
);
|
);
|
||||||
|
@ -3,12 +3,14 @@ import 'dart:io';
|
|||||||
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||||
|
import 'package:immich_mobile/domain/utils/background_sync.dart';
|
||||||
import 'package:immich_mobile/entities/store.entity.dart';
|
import 'package:immich_mobile/entities/store.entity.dart';
|
||||||
import 'package:immich_mobile/interfaces/auth.interface.dart';
|
import 'package:immich_mobile/interfaces/auth.interface.dart';
|
||||||
import 'package:immich_mobile/interfaces/auth_api.interface.dart';
|
import 'package:immich_mobile/interfaces/auth_api.interface.dart';
|
||||||
import 'package:immich_mobile/models/auth/auxilary_endpoint.model.dart';
|
import 'package:immich_mobile/models/auth/auxilary_endpoint.model.dart';
|
||||||
import 'package:immich_mobile/models/auth/login_response.model.dart';
|
import 'package:immich_mobile/models/auth/login_response.model.dart';
|
||||||
import 'package:immich_mobile/providers/api.provider.dart';
|
import 'package:immich_mobile/providers/api.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/background_sync.provider.dart';
|
||||||
import 'package:immich_mobile/repositories/auth.repository.dart';
|
import 'package:immich_mobile/repositories/auth.repository.dart';
|
||||||
import 'package:immich_mobile/repositories/auth_api.repository.dart';
|
import 'package:immich_mobile/repositories/auth_api.repository.dart';
|
||||||
import 'package:immich_mobile/services/api.service.dart';
|
import 'package:immich_mobile/services/api.service.dart';
|
||||||
@ -22,6 +24,7 @@ final authServiceProvider = Provider(
|
|||||||
ref.watch(authRepositoryProvider),
|
ref.watch(authRepositoryProvider),
|
||||||
ref.watch(apiServiceProvider),
|
ref.watch(apiServiceProvider),
|
||||||
ref.watch(networkServiceProvider),
|
ref.watch(networkServiceProvider),
|
||||||
|
ref.watch(backgroundSyncProvider),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -30,6 +33,7 @@ class AuthService {
|
|||||||
final IAuthRepository _authRepository;
|
final IAuthRepository _authRepository;
|
||||||
final ApiService _apiService;
|
final ApiService _apiService;
|
||||||
final NetworkService _networkService;
|
final NetworkService _networkService;
|
||||||
|
final BackgroundSyncManager _backgroundSyncManager;
|
||||||
|
|
||||||
final _log = Logger("AuthService");
|
final _log = Logger("AuthService");
|
||||||
|
|
||||||
@ -38,6 +42,7 @@ class AuthService {
|
|||||||
this._authRepository,
|
this._authRepository,
|
||||||
this._apiService,
|
this._apiService,
|
||||||
this._networkService,
|
this._networkService,
|
||||||
|
this._backgroundSyncManager,
|
||||||
);
|
);
|
||||||
|
|
||||||
/// Validates the provided server URL by resolving and setting the endpoint.
|
/// Validates the provided server URL by resolving and setting the endpoint.
|
||||||
@ -115,8 +120,10 @@ class AuthService {
|
|||||||
/// - Asset ETag
|
/// - Asset ETag
|
||||||
///
|
///
|
||||||
/// All deletions are executed in parallel using [Future.wait].
|
/// All deletions are executed in parallel using [Future.wait].
|
||||||
Future<void> clearLocalData() {
|
Future<void> clearLocalData() async {
|
||||||
return Future.wait([
|
// Cancel any ongoing background sync operations before clearing data
|
||||||
|
await _backgroundSyncManager.cancel();
|
||||||
|
await Future.wait([
|
||||||
_authRepository.clearLocalData(),
|
_authRepository.clearLocalData(),
|
||||||
Store.delete(StoreKey.currentUser),
|
Store.delete(StoreKey.currentUser),
|
||||||
Store.delete(StoreKey.accessToken),
|
Store.delete(StoreKey.accessToken),
|
||||||
|
@ -351,7 +351,6 @@ class BackupService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
baseRequest.headers.addAll(ApiService.getRequestHeaders());
|
baseRequest.headers.addAll(ApiService.getRequestHeaders());
|
||||||
baseRequest.headers["Transfer-Encoding"] = "chunked";
|
|
||||||
baseRequest.fields['deviceAssetId'] = asset.localId!;
|
baseRequest.fields['deviceAssetId'] = asset.localId!;
|
||||||
baseRequest.fields['deviceId'] = deviceId;
|
baseRequest.fields['deviceId'] = deviceId;
|
||||||
baseRequest.fields['fileCreatedAt'] =
|
baseRequest.fields['fileCreatedAt'] =
|
||||||
|
@ -48,11 +48,15 @@ abstract final class Bootstrap {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<void> initDomain(Isar db) async {
|
static Future<void> initDomain(
|
||||||
|
Isar db, {
|
||||||
|
bool shouldBufferLogs = true,
|
||||||
|
}) async {
|
||||||
await StoreService.init(storeRepository: IsarStoreRepository(db));
|
await StoreService.init(storeRepository: IsarStoreRepository(db));
|
||||||
await LogService.init(
|
await LogService.init(
|
||||||
logRepository: IsarLogRepository(db),
|
logRepository: IsarLogRepository(db),
|
||||||
storeRepository: IsarStoreRepository(db),
|
storeRepository: IsarStoreRepository(db),
|
||||||
|
shouldBuffer: shouldBufferLogs,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
69
mobile/lib/utils/isolate.dart
Normal file
69
mobile/lib/utils/isolate.dart
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:ui';
|
||||||
|
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/providers/db.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/cancel.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
|
||||||
|
import 'package:immich_mobile/utils/bootstrap.dart';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
|
import 'package:worker_manager/worker_manager.dart';
|
||||||
|
|
||||||
|
class InvalidIsolateUsageException implements Exception {
|
||||||
|
const InvalidIsolateUsageException();
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() =>
|
||||||
|
"IsolateHelper should only be used from the root isolate";
|
||||||
|
}
|
||||||
|
|
||||||
|
// !! Should be used only from the root isolate
|
||||||
|
Cancelable<T?> runInIsolateGentle<T>({
|
||||||
|
required Future<T> Function(ProviderContainer ref) computation,
|
||||||
|
String? debugLabel,
|
||||||
|
}) {
|
||||||
|
final token = RootIsolateToken.instance;
|
||||||
|
if (token == null) {
|
||||||
|
throw const InvalidIsolateUsageException();
|
||||||
|
}
|
||||||
|
|
||||||
|
return workerManager.executeGentle((cancelledChecker) async {
|
||||||
|
BackgroundIsolateBinaryMessenger.ensureInitialized(token);
|
||||||
|
DartPluginRegistrant.ensureInitialized();
|
||||||
|
|
||||||
|
final db = await Bootstrap.initIsar();
|
||||||
|
await Bootstrap.initDomain(db, shouldBufferLogs: false);
|
||||||
|
final ref = ProviderContainer(
|
||||||
|
overrides: [
|
||||||
|
// TODO: Remove once isar is removed
|
||||||
|
dbProvider.overrideWithValue(db),
|
||||||
|
isarProvider.overrideWithValue(db),
|
||||||
|
cancellationProvider.overrideWithValue(cancelledChecker),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
Logger log = Logger("IsolateLogger");
|
||||||
|
|
||||||
|
try {
|
||||||
|
return await computation(ref);
|
||||||
|
} on CanceledError {
|
||||||
|
log.warning(
|
||||||
|
"Computation cancelled ${debugLabel == null ? '' : ' for $debugLabel'}",
|
||||||
|
);
|
||||||
|
} catch (error, stack) {
|
||||||
|
log.severe(
|
||||||
|
"Error in runInIsolateGentle ${debugLabel == null ? '' : ' for $debugLabel'}",
|
||||||
|
error,
|
||||||
|
stack,
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
// Wait for the logs to flush
|
||||||
|
await Future.delayed(const Duration(seconds: 2));
|
||||||
|
// Always close the new db connection on Isolate end
|
||||||
|
ref.read(driftProvider).close();
|
||||||
|
ref.read(isarProvider).close();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
@ -1,11 +1,13 @@
|
|||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_svg/svg.dart';
|
import 'package:flutter_svg/svg.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/models/backup/backup_state.model.dart';
|
import 'package:immich_mobile/models/backup/backup_state.model.dart';
|
||||||
import 'package:immich_mobile/models/server_info/server_info.model.dart';
|
import 'package:immich_mobile/models/server_info/server_info.model.dart';
|
||||||
|
import 'package:immich_mobile/providers/background_sync.provider.dart';
|
||||||
import 'package:immich_mobile/providers/backup/backup.provider.dart';
|
import 'package:immich_mobile/providers/backup/backup.provider.dart';
|
||||||
import 'package:immich_mobile/providers/server_info.provider.dart';
|
import 'package:immich_mobile/providers/server_info.provider.dart';
|
||||||
import 'package:immich_mobile/providers/user.provider.dart';
|
import 'package:immich_mobile/providers/user.provider.dart';
|
||||||
@ -178,6 +180,11 @@ class ImmichAppBar extends ConsumerWidget implements PreferredSizeWidget {
|
|||||||
child: action,
|
child: action,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
if (kDebugMode)
|
||||||
|
IconButton(
|
||||||
|
onPressed: () => ref.read(backgroundSyncProvider).sync(),
|
||||||
|
icon: const Icon(Icons.sync),
|
||||||
|
),
|
||||||
if (showUploadButton)
|
if (showUploadButton)
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(right: 20),
|
padding: const EdgeInsets.only(right: 20),
|
||||||
|
@ -1806,7 +1806,7 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.4"
|
version: "3.1.4"
|
||||||
uuid:
|
uuid:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: uuid
|
name: uuid
|
||||||
sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff
|
sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff
|
||||||
@ -1933,6 +1933,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.0.3"
|
version: "0.0.3"
|
||||||
|
worker_manager:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: worker_manager
|
||||||
|
sha256: "086ed63e9b36266e851404ca90fd44e37c0f4c9bbf819e5f8d7c87f9741c0591"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "7.2.3"
|
||||||
xdg_directories:
|
xdg_directories:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -60,7 +60,9 @@ dependencies:
|
|||||||
thumbhash: 0.1.0+1
|
thumbhash: 0.1.0+1
|
||||||
timezone: ^0.9.4
|
timezone: ^0.9.4
|
||||||
url_launcher: ^6.3.1
|
url_launcher: ^6.3.1
|
||||||
|
uuid: ^4.5.1
|
||||||
wakelock_plus: ^1.2.10
|
wakelock_plus: ^1.2.10
|
||||||
|
worker_manager: ^7.2.3
|
||||||
|
|
||||||
native_video_player:
|
native_video_player:
|
||||||
git:
|
git:
|
||||||
|
@ -2,3 +2,5 @@ import 'package:mocktail/mocktail.dart';
|
|||||||
import 'package:openapi/api.dart';
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
class MockAssetsApi extends Mock implements AssetsApi {}
|
class MockAssetsApi extends Mock implements AssetsApi {}
|
||||||
|
|
||||||
|
class MockSyncApi extends Mock implements SyncApi {}
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
import 'package:immich_mobile/domain/services/store.service.dart';
|
import 'package:immich_mobile/domain/services/store.service.dart';
|
||||||
import 'package:immich_mobile/domain/services/user.service.dart';
|
import 'package:immich_mobile/domain/services/user.service.dart';
|
||||||
|
import 'package:immich_mobile/domain/utils/background_sync.dart';
|
||||||
import 'package:mocktail/mocktail.dart';
|
import 'package:mocktail/mocktail.dart';
|
||||||
|
|
||||||
class MockStoreService extends Mock implements StoreService {}
|
class MockStoreService extends Mock implements StoreService {}
|
||||||
|
|
||||||
class MockUserService extends Mock implements UserService {}
|
class MockUserService extends Mock implements UserService {}
|
||||||
|
|
||||||
|
class MockBackgroundSyncManager extends Mock implements BackgroundSyncManager {}
|
||||||
|
222
mobile/test/domain/services/sync_stream_service_test.dart
Normal file
222
mobile/test/domain/services/sync_stream_service_test.dart
Normal file
@ -0,0 +1,222 @@
|
|||||||
|
// ignore_for_file: avoid-declaring-call-method, avoid-unnecessary-futures
|
||||||
|
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:immich_mobile/domain/interfaces/sync_api.interface.dart';
|
||||||
|
import 'package:immich_mobile/domain/interfaces/sync_stream.interface.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/sync_event.model.dart';
|
||||||
|
import 'package:immich_mobile/domain/services/sync_stream.service.dart';
|
||||||
|
import 'package:mocktail/mocktail.dart';
|
||||||
|
|
||||||
|
import '../../fixtures/sync_stream.stub.dart';
|
||||||
|
import '../../infrastructure/repository.mock.dart';
|
||||||
|
|
||||||
|
class _AbortCallbackWrapper {
|
||||||
|
const _AbortCallbackWrapper();
|
||||||
|
|
||||||
|
bool call() => false;
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MockAbortCallbackWrapper extends Mock implements _AbortCallbackWrapper {}
|
||||||
|
|
||||||
|
class _CancellationWrapper {
|
||||||
|
const _CancellationWrapper();
|
||||||
|
|
||||||
|
bool call() => false;
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MockCancellationWrapper extends Mock implements _CancellationWrapper {}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
late SyncStreamService sut;
|
||||||
|
late ISyncStreamRepository mockSyncStreamRepo;
|
||||||
|
late ISyncApiRepository mockSyncApiRepo;
|
||||||
|
late Function(List<SyncEvent>, Function()) handleEventsCallback;
|
||||||
|
late _MockAbortCallbackWrapper mockAbortCallbackWrapper;
|
||||||
|
|
||||||
|
successHandler(Invocation _) async => true;
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
mockSyncStreamRepo = MockSyncStreamRepository();
|
||||||
|
mockSyncApiRepo = MockSyncApiRepository();
|
||||||
|
mockAbortCallbackWrapper = _MockAbortCallbackWrapper();
|
||||||
|
|
||||||
|
when(() => mockAbortCallbackWrapper()).thenReturn(false);
|
||||||
|
|
||||||
|
when(() => mockSyncApiRepo.streamChanges(any()))
|
||||||
|
.thenAnswer((invocation) async {
|
||||||
|
// ignore: avoid-unsafe-collection-methods
|
||||||
|
handleEventsCallback = invocation.positionalArguments.first;
|
||||||
|
});
|
||||||
|
|
||||||
|
when(() => mockSyncApiRepo.ack(any())).thenAnswer((_) async => {});
|
||||||
|
|
||||||
|
when(() => mockSyncStreamRepo.updateUsersV1(any()))
|
||||||
|
.thenAnswer(successHandler);
|
||||||
|
when(() => mockSyncStreamRepo.deleteUsersV1(any()))
|
||||||
|
.thenAnswer(successHandler);
|
||||||
|
when(() => mockSyncStreamRepo.updatePartnerV1(any()))
|
||||||
|
.thenAnswer(successHandler);
|
||||||
|
when(() => mockSyncStreamRepo.deletePartnerV1(any()))
|
||||||
|
.thenAnswer(successHandler);
|
||||||
|
when(() => mockSyncStreamRepo.updateAssetsV1(any()))
|
||||||
|
.thenAnswer(successHandler);
|
||||||
|
when(() => mockSyncStreamRepo.deleteAssetsV1(any()))
|
||||||
|
.thenAnswer(successHandler);
|
||||||
|
when(() => mockSyncStreamRepo.updateAssetsExifV1(any()))
|
||||||
|
.thenAnswer(successHandler);
|
||||||
|
when(() => mockSyncStreamRepo.updatePartnerAssetsV1(any()))
|
||||||
|
.thenAnswer(successHandler);
|
||||||
|
when(() => mockSyncStreamRepo.deletePartnerAssetsV1(any()))
|
||||||
|
.thenAnswer(successHandler);
|
||||||
|
when(() => mockSyncStreamRepo.updatePartnerAssetsExifV1(any()))
|
||||||
|
.thenAnswer(successHandler);
|
||||||
|
|
||||||
|
sut = SyncStreamService(
|
||||||
|
syncApiRepository: mockSyncApiRepo,
|
||||||
|
syncStreamRepository: mockSyncStreamRepo,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
Future<void> simulateEvents(List<SyncEvent> events) async {
|
||||||
|
await sut.sync();
|
||||||
|
await handleEventsCallback(events, mockAbortCallbackWrapper.call);
|
||||||
|
}
|
||||||
|
|
||||||
|
group("SyncStreamService - _handleEvents", () {
|
||||||
|
test(
|
||||||
|
"processes events and acks successfully when handlers succeed",
|
||||||
|
() async {
|
||||||
|
final events = [
|
||||||
|
SyncStreamStub.userDeleteV1,
|
||||||
|
SyncStreamStub.userV1Admin,
|
||||||
|
SyncStreamStub.userV1User,
|
||||||
|
SyncStreamStub.partnerDeleteV1,
|
||||||
|
SyncStreamStub.partnerV1,
|
||||||
|
];
|
||||||
|
|
||||||
|
await simulateEvents(events);
|
||||||
|
|
||||||
|
verifyInOrder([
|
||||||
|
() => mockSyncStreamRepo.deleteUsersV1(any()),
|
||||||
|
() => mockSyncApiRepo.ack(["2"]),
|
||||||
|
() => mockSyncStreamRepo.updateUsersV1(any()),
|
||||||
|
() => mockSyncApiRepo.ack(["5"]),
|
||||||
|
() => mockSyncStreamRepo.deletePartnerV1(any()),
|
||||||
|
() => mockSyncApiRepo.ack(["4"]),
|
||||||
|
() => mockSyncStreamRepo.updatePartnerV1(any()),
|
||||||
|
() => mockSyncApiRepo.ack(["3"]),
|
||||||
|
]);
|
||||||
|
verifyNever(() => mockAbortCallbackWrapper());
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test("processes final batch correctly", () async {
|
||||||
|
final events = [
|
||||||
|
SyncStreamStub.userDeleteV1,
|
||||||
|
SyncStreamStub.userV1Admin,
|
||||||
|
];
|
||||||
|
|
||||||
|
await simulateEvents(events);
|
||||||
|
|
||||||
|
verifyInOrder([
|
||||||
|
() => mockSyncStreamRepo.deleteUsersV1(any()),
|
||||||
|
() => mockSyncApiRepo.ack(["2"]),
|
||||||
|
() => mockSyncStreamRepo.updateUsersV1(any()),
|
||||||
|
() => mockSyncApiRepo.ack(["1"]),
|
||||||
|
]);
|
||||||
|
verifyNever(() => mockAbortCallbackWrapper());
|
||||||
|
});
|
||||||
|
|
||||||
|
test("does not process or ack when event list is empty", () async {
|
||||||
|
await simulateEvents([]);
|
||||||
|
|
||||||
|
verifyNever(() => mockSyncStreamRepo.updateUsersV1(any()));
|
||||||
|
verifyNever(() => mockSyncStreamRepo.deleteUsersV1(any()));
|
||||||
|
verifyNever(() => mockSyncStreamRepo.updatePartnerV1(any()));
|
||||||
|
verifyNever(() => mockSyncStreamRepo.deletePartnerV1(any()));
|
||||||
|
verifyNever(() => mockAbortCallbackWrapper());
|
||||||
|
verifyNever(() => mockSyncApiRepo.ack(any()));
|
||||||
|
});
|
||||||
|
|
||||||
|
test("aborts and stops processing if cancelled during iteration", () async {
|
||||||
|
final cancellationChecker = _MockCancellationWrapper();
|
||||||
|
when(() => cancellationChecker()).thenReturn(false);
|
||||||
|
|
||||||
|
sut = SyncStreamService(
|
||||||
|
syncApiRepository: mockSyncApiRepo,
|
||||||
|
syncStreamRepository: mockSyncStreamRepo,
|
||||||
|
cancelChecker: cancellationChecker.call,
|
||||||
|
);
|
||||||
|
await sut.sync();
|
||||||
|
|
||||||
|
final events = [
|
||||||
|
SyncStreamStub.userDeleteV1,
|
||||||
|
SyncStreamStub.userV1Admin,
|
||||||
|
SyncStreamStub.partnerDeleteV1,
|
||||||
|
];
|
||||||
|
|
||||||
|
when(() => mockSyncStreamRepo.deleteUsersV1(any())).thenAnswer((_) async {
|
||||||
|
when(() => cancellationChecker()).thenReturn(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
await handleEventsCallback(events, mockAbortCallbackWrapper.call);
|
||||||
|
|
||||||
|
verify(() => mockSyncStreamRepo.deleteUsersV1(any())).called(1);
|
||||||
|
verifyNever(() => mockSyncStreamRepo.updateUsersV1(any()));
|
||||||
|
verifyNever(() => mockSyncStreamRepo.deletePartnerV1(any()));
|
||||||
|
|
||||||
|
verify(() => mockAbortCallbackWrapper()).called(1);
|
||||||
|
|
||||||
|
verify(() => mockSyncApiRepo.ack(["2"])).called(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test(
|
||||||
|
"aborts and stops processing if cancelled before processing batch",
|
||||||
|
() async {
|
||||||
|
final cancellationChecker = _MockCancellationWrapper();
|
||||||
|
when(() => cancellationChecker()).thenReturn(false);
|
||||||
|
|
||||||
|
final processingCompleter = Completer<void>();
|
||||||
|
bool handler1Started = false;
|
||||||
|
when(() => mockSyncStreamRepo.deleteUsersV1(any()))
|
||||||
|
.thenAnswer((_) async {
|
||||||
|
handler1Started = true;
|
||||||
|
return processingCompleter.future;
|
||||||
|
});
|
||||||
|
|
||||||
|
sut = SyncStreamService(
|
||||||
|
syncApiRepository: mockSyncApiRepo,
|
||||||
|
syncStreamRepository: mockSyncStreamRepo,
|
||||||
|
cancelChecker: cancellationChecker.call,
|
||||||
|
);
|
||||||
|
|
||||||
|
await sut.sync();
|
||||||
|
|
||||||
|
final events = [
|
||||||
|
SyncStreamStub.userDeleteV1,
|
||||||
|
SyncStreamStub.userV1Admin,
|
||||||
|
SyncStreamStub.partnerDeleteV1,
|
||||||
|
];
|
||||||
|
|
||||||
|
final processingFuture =
|
||||||
|
handleEventsCallback(events, mockAbortCallbackWrapper.call);
|
||||||
|
await pumpEventQueue();
|
||||||
|
|
||||||
|
expect(handler1Started, isTrue);
|
||||||
|
|
||||||
|
// Signal cancellation while handler 1 is waiting
|
||||||
|
when(() => cancellationChecker()).thenReturn(true);
|
||||||
|
await pumpEventQueue();
|
||||||
|
|
||||||
|
processingCompleter.complete();
|
||||||
|
await processingFuture;
|
||||||
|
|
||||||
|
verifyNever(() => mockSyncStreamRepo.updateUsersV1(any()));
|
||||||
|
|
||||||
|
verify(() => mockSyncApiRepo.ack(["2"])).called(1);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
45
mobile/test/fixtures/sync_stream.stub.dart
vendored
Normal file
45
mobile/test/fixtures/sync_stream.stub.dart
vendored
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import 'package:immich_mobile/domain/models/sync_event.model.dart';
|
||||||
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
|
abstract final class SyncStreamStub {
|
||||||
|
static final userV1Admin = SyncEvent(
|
||||||
|
type: SyncEntityType.userV1,
|
||||||
|
data: SyncUserV1(
|
||||||
|
deletedAt: DateTime(2020),
|
||||||
|
email: "admin@admin",
|
||||||
|
id: "1",
|
||||||
|
name: "Admin",
|
||||||
|
),
|
||||||
|
ack: "1",
|
||||||
|
);
|
||||||
|
static final userV1User = SyncEvent(
|
||||||
|
type: SyncEntityType.userV1,
|
||||||
|
data: SyncUserV1(
|
||||||
|
deletedAt: DateTime(2021),
|
||||||
|
email: "user@user",
|
||||||
|
id: "5",
|
||||||
|
name: "User",
|
||||||
|
),
|
||||||
|
ack: "5",
|
||||||
|
);
|
||||||
|
static final userDeleteV1 = SyncEvent(
|
||||||
|
type: SyncEntityType.userDeleteV1,
|
||||||
|
data: SyncUserDeleteV1(userId: "2"),
|
||||||
|
ack: "2",
|
||||||
|
);
|
||||||
|
|
||||||
|
static final partnerV1 = SyncEvent(
|
||||||
|
type: SyncEntityType.partnerV1,
|
||||||
|
data: SyncPartnerV1(
|
||||||
|
inTimeline: true,
|
||||||
|
sharedById: "1",
|
||||||
|
sharedWithId: "2",
|
||||||
|
),
|
||||||
|
ack: "3",
|
||||||
|
);
|
||||||
|
static final partnerDeleteV1 = SyncEvent(
|
||||||
|
type: SyncEntityType.partnerDeleteV1,
|
||||||
|
data: SyncPartnerDeleteV1(sharedById: "3", sharedWithId: "4"),
|
||||||
|
ack: "4",
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,299 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:http/http.dart' as http;
|
||||||
|
import 'package:immich_mobile/domain/models/sync_event.model.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/repositories/sync_api.repository.dart';
|
||||||
|
import 'package:mocktail/mocktail.dart';
|
||||||
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
|
import '../../api.mocks.dart';
|
||||||
|
import '../../service.mocks.dart';
|
||||||
|
|
||||||
|
class MockHttpClient extends Mock implements http.Client {}
|
||||||
|
|
||||||
|
class MockApiClient extends Mock implements ApiClient {}
|
||||||
|
|
||||||
|
class MockStreamedResponse extends Mock implements http.StreamedResponse {}
|
||||||
|
|
||||||
|
class FakeBaseRequest extends Fake implements http.BaseRequest {}
|
||||||
|
|
||||||
|
String _createJsonLine(String type, Map<String, dynamic> data, String ack) {
|
||||||
|
return '${jsonEncode({'type': type, 'data': data, 'ack': ack})}\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
late SyncApiRepository sut;
|
||||||
|
late MockApiService mockApiService;
|
||||||
|
late MockApiClient mockApiClient;
|
||||||
|
late MockSyncApi mockSyncApi;
|
||||||
|
late MockHttpClient mockHttpClient;
|
||||||
|
late MockStreamedResponse mockStreamedResponse;
|
||||||
|
late StreamController<List<int>> responseStreamController;
|
||||||
|
late int testBatchSize = 3;
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
mockApiService = MockApiService();
|
||||||
|
mockApiClient = MockApiClient();
|
||||||
|
mockSyncApi = MockSyncApi();
|
||||||
|
mockHttpClient = MockHttpClient();
|
||||||
|
mockStreamedResponse = MockStreamedResponse();
|
||||||
|
responseStreamController =
|
||||||
|
StreamController<List<int>>.broadcast(sync: true);
|
||||||
|
|
||||||
|
registerFallbackValue(FakeBaseRequest());
|
||||||
|
|
||||||
|
when(() => mockApiService.apiClient).thenReturn(mockApiClient);
|
||||||
|
when(() => mockApiService.syncApi).thenReturn(mockSyncApi);
|
||||||
|
when(() => mockApiClient.basePath).thenReturn('http://demo.immich.app/api');
|
||||||
|
when(() => mockApiService.applyToParams(any(), any()))
|
||||||
|
.thenAnswer((_) async => {});
|
||||||
|
|
||||||
|
// Mock HTTP client behavior
|
||||||
|
when(() => mockHttpClient.send(any()))
|
||||||
|
.thenAnswer((_) async => mockStreamedResponse);
|
||||||
|
when(() => mockStreamedResponse.statusCode).thenReturn(200);
|
||||||
|
when(() => mockStreamedResponse.stream)
|
||||||
|
.thenAnswer((_) => http.ByteStream(responseStreamController.stream));
|
||||||
|
when(() => mockHttpClient.close()).thenAnswer((_) => {});
|
||||||
|
|
||||||
|
sut = SyncApiRepository(mockApiService);
|
||||||
|
});
|
||||||
|
|
||||||
|
tearDown(() async {
|
||||||
|
if (!responseStreamController.isClosed) {
|
||||||
|
await responseStreamController.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Future<void> streamChanges(
|
||||||
|
Function(List<SyncEvent>, Function() abort) onDataCallback,
|
||||||
|
) {
|
||||||
|
return sut.streamChanges(
|
||||||
|
onDataCallback,
|
||||||
|
batchSize: testBatchSize,
|
||||||
|
httpClient: mockHttpClient,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
test('streamChanges stops processing stream when abort is called', () async {
|
||||||
|
int onDataCallCount = 0;
|
||||||
|
bool abortWasCalledInCallback = false;
|
||||||
|
List<SyncEvent> receivedEventsBatch1 = [];
|
||||||
|
|
||||||
|
onDataCallback(List<SyncEvent> events, Function() abort) {
|
||||||
|
onDataCallCount++;
|
||||||
|
if (onDataCallCount == 1) {
|
||||||
|
receivedEventsBatch1 = events;
|
||||||
|
abort();
|
||||||
|
abortWasCalledInCallback = true;
|
||||||
|
} else {
|
||||||
|
fail("onData called more than once after abort was invoked");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final streamChangesFuture = streamChanges(onDataCallback);
|
||||||
|
|
||||||
|
await pumpEventQueue();
|
||||||
|
|
||||||
|
for (int i = 0; i < testBatchSize; i++) {
|
||||||
|
responseStreamController.add(
|
||||||
|
utf8.encode(
|
||||||
|
_createJsonLine(
|
||||||
|
SyncEntityType.userDeleteV1.toString(),
|
||||||
|
SyncUserDeleteV1(userId: "user$i").toJson(),
|
||||||
|
'ack$i',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = testBatchSize; i < testBatchSize * 2; i++) {
|
||||||
|
responseStreamController.add(
|
||||||
|
utf8.encode(
|
||||||
|
_createJsonLine(
|
||||||
|
SyncEntityType.userDeleteV1.toString(),
|
||||||
|
SyncUserDeleteV1(userId: "user$i").toJson(),
|
||||||
|
'ack$i',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await responseStreamController.close();
|
||||||
|
await expectLater(streamChangesFuture, completes);
|
||||||
|
|
||||||
|
expect(onDataCallCount, 1);
|
||||||
|
expect(abortWasCalledInCallback, isTrue);
|
||||||
|
expect(receivedEventsBatch1.length, testBatchSize);
|
||||||
|
verify(() => mockHttpClient.close()).called(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test(
|
||||||
|
'streamChanges does not process remaining lines in finally block if aborted',
|
||||||
|
() async {
|
||||||
|
int onDataCallCount = 0;
|
||||||
|
bool abortWasCalledInCallback = false;
|
||||||
|
|
||||||
|
onDataCallback(List<SyncEvent> events, Function() abort) {
|
||||||
|
onDataCallCount++;
|
||||||
|
if (onDataCallCount == 1) {
|
||||||
|
abort();
|
||||||
|
abortWasCalledInCallback = true;
|
||||||
|
} else {
|
||||||
|
fail("onData called more than once after abort was invoked");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final streamChangesFuture = streamChanges(onDataCallback);
|
||||||
|
|
||||||
|
await pumpEventQueue();
|
||||||
|
|
||||||
|
for (int i = 0; i < testBatchSize; i++) {
|
||||||
|
responseStreamController.add(
|
||||||
|
utf8.encode(
|
||||||
|
_createJsonLine(
|
||||||
|
SyncEntityType.userDeleteV1.toString(),
|
||||||
|
SyncUserDeleteV1(userId: "user$i").toJson(),
|
||||||
|
'ack$i',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// emit a single event to skip batching and trigger finally
|
||||||
|
responseStreamController.add(
|
||||||
|
utf8.encode(
|
||||||
|
_createJsonLine(
|
||||||
|
SyncEntityType.userDeleteV1.toString(),
|
||||||
|
SyncUserDeleteV1(userId: "user100").toJson(),
|
||||||
|
'ack100',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await responseStreamController.close();
|
||||||
|
await expectLater(streamChangesFuture, completes);
|
||||||
|
|
||||||
|
expect(onDataCallCount, 1);
|
||||||
|
expect(abortWasCalledInCallback, isTrue);
|
||||||
|
verify(() => mockHttpClient.close()).called(1);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
'streamChanges processes remaining lines in finally block if not aborted',
|
||||||
|
() async {
|
||||||
|
int onDataCallCount = 0;
|
||||||
|
List<SyncEvent> receivedEventsBatch1 = [];
|
||||||
|
List<SyncEvent> receivedEventsBatch2 = [];
|
||||||
|
|
||||||
|
onDataCallback(List<SyncEvent> events, Function() _) {
|
||||||
|
onDataCallCount++;
|
||||||
|
if (onDataCallCount == 1) {
|
||||||
|
receivedEventsBatch1 = events;
|
||||||
|
} else if (onDataCallCount == 2) {
|
||||||
|
receivedEventsBatch2 = events;
|
||||||
|
} else {
|
||||||
|
fail("onData called more than expected");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final streamChangesFuture = streamChanges(onDataCallback);
|
||||||
|
|
||||||
|
await pumpEventQueue();
|
||||||
|
|
||||||
|
// Batch 1
|
||||||
|
for (int i = 0; i < testBatchSize; i++) {
|
||||||
|
responseStreamController.add(
|
||||||
|
utf8.encode(
|
||||||
|
_createJsonLine(
|
||||||
|
SyncEntityType.userDeleteV1.toString(),
|
||||||
|
SyncUserDeleteV1(userId: "user$i").toJson(),
|
||||||
|
'ack$i',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Partial Batch 2
|
||||||
|
responseStreamController.add(
|
||||||
|
utf8.encode(
|
||||||
|
_createJsonLine(
|
||||||
|
SyncEntityType.userDeleteV1.toString(),
|
||||||
|
SyncUserDeleteV1(userId: "user100").toJson(),
|
||||||
|
'ack100',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await responseStreamController.close();
|
||||||
|
await expectLater(streamChangesFuture, completes);
|
||||||
|
|
||||||
|
expect(onDataCallCount, 2);
|
||||||
|
expect(receivedEventsBatch1.length, testBatchSize);
|
||||||
|
expect(receivedEventsBatch2.length, 1);
|
||||||
|
verify(() => mockHttpClient.close()).called(1);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test('streamChanges handles stream error gracefully', () async {
|
||||||
|
final streamError = Exception("Network Error");
|
||||||
|
int onDataCallCount = 0;
|
||||||
|
|
||||||
|
onDataCallback(List<SyncEvent> events, Function() _) {
|
||||||
|
onDataCallCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
final streamChangesFuture = streamChanges(onDataCallback);
|
||||||
|
|
||||||
|
await pumpEventQueue();
|
||||||
|
|
||||||
|
responseStreamController.add(
|
||||||
|
utf8.encode(
|
||||||
|
_createJsonLine(
|
||||||
|
SyncEntityType.userDeleteV1.toString(),
|
||||||
|
SyncUserDeleteV1(userId: "user1").toJson(),
|
||||||
|
'ack1',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
responseStreamController.addError(streamError);
|
||||||
|
await expectLater(streamChangesFuture, throwsA(streamError));
|
||||||
|
|
||||||
|
expect(onDataCallCount, 0);
|
||||||
|
verify(() => mockHttpClient.close()).called(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('streamChanges throws ApiException on non-200 status code', () async {
|
||||||
|
when(() => mockStreamedResponse.statusCode).thenReturn(401);
|
||||||
|
final errorBodyController = StreamController<List<int>>(sync: true);
|
||||||
|
when(() => mockStreamedResponse.stream)
|
||||||
|
.thenAnswer((_) => http.ByteStream(errorBodyController.stream));
|
||||||
|
|
||||||
|
int onDataCallCount = 0;
|
||||||
|
|
||||||
|
onDataCallback(List<SyncEvent> events, Function() _) {
|
||||||
|
onDataCallCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
final future = streamChanges(onDataCallback);
|
||||||
|
|
||||||
|
errorBodyController.add(utf8.encode('{"error":"Unauthorized"}'));
|
||||||
|
await errorBodyController.close();
|
||||||
|
|
||||||
|
await expectLater(
|
||||||
|
future,
|
||||||
|
throwsA(
|
||||||
|
isA<ApiException>()
|
||||||
|
.having((e) => e.code, 'code', 401)
|
||||||
|
.having((e) => e.message, 'message', contains('Unauthorized')),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(onDataCallCount, 0);
|
||||||
|
verify(() => mockHttpClient.close()).called(1);
|
||||||
|
});
|
||||||
|
}
|
@ -1,6 +1,8 @@
|
|||||||
import 'package:immich_mobile/domain/interfaces/device_asset.interface.dart';
|
import 'package:immich_mobile/domain/interfaces/device_asset.interface.dart';
|
||||||
import 'package:immich_mobile/domain/interfaces/log.interface.dart';
|
import 'package:immich_mobile/domain/interfaces/log.interface.dart';
|
||||||
import 'package:immich_mobile/domain/interfaces/store.interface.dart';
|
import 'package:immich_mobile/domain/interfaces/store.interface.dart';
|
||||||
|
import 'package:immich_mobile/domain/interfaces/sync_api.interface.dart';
|
||||||
|
import 'package:immich_mobile/domain/interfaces/sync_stream.interface.dart';
|
||||||
import 'package:immich_mobile/domain/interfaces/user.interface.dart';
|
import 'package:immich_mobile/domain/interfaces/user.interface.dart';
|
||||||
import 'package:immich_mobile/domain/interfaces/user_api.interface.dart';
|
import 'package:immich_mobile/domain/interfaces/user_api.interface.dart';
|
||||||
import 'package:mocktail/mocktail.dart';
|
import 'package:mocktail/mocktail.dart';
|
||||||
@ -14,5 +16,9 @@ class MockUserRepository extends Mock implements IUserRepository {}
|
|||||||
class MockDeviceAssetRepository extends Mock
|
class MockDeviceAssetRepository extends Mock
|
||||||
implements IDeviceAssetRepository {}
|
implements IDeviceAssetRepository {}
|
||||||
|
|
||||||
|
class MockSyncStreamRepository extends Mock implements ISyncStreamRepository {}
|
||||||
|
|
||||||
// API Repos
|
// API Repos
|
||||||
class MockUserApiRepository extends Mock implements IUserApiRepository {}
|
class MockUserApiRepository extends Mock implements IUserApiRepository {}
|
||||||
|
|
||||||
|
class MockSyncApiRepository extends Mock implements ISyncApiRepository {}
|
||||||
|
@ -29,4 +29,3 @@ class MockSearchApi extends Mock implements SearchApi {}
|
|||||||
class MockAppSettingService extends Mock implements AppSettingsService {}
|
class MockAppSettingService extends Mock implements AppSettingsService {}
|
||||||
|
|
||||||
class MockBackgroundService extends Mock implements BackgroundService {}
|
class MockBackgroundService extends Mock implements BackgroundService {}
|
||||||
|
|
||||||
|
@ -8,6 +8,7 @@ import 'package:isar/isar.dart';
|
|||||||
import 'package:mocktail/mocktail.dart';
|
import 'package:mocktail/mocktail.dart';
|
||||||
import 'package:openapi/api.dart';
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
|
import '../domain/service.mock.dart';
|
||||||
import '../repository.mocks.dart';
|
import '../repository.mocks.dart';
|
||||||
import '../service.mocks.dart';
|
import '../service.mocks.dart';
|
||||||
import '../test_utils.dart';
|
import '../test_utils.dart';
|
||||||
@ -18,6 +19,7 @@ void main() {
|
|||||||
late MockAuthRepository authRepository;
|
late MockAuthRepository authRepository;
|
||||||
late MockApiService apiService;
|
late MockApiService apiService;
|
||||||
late MockNetworkService networkService;
|
late MockNetworkService networkService;
|
||||||
|
late MockBackgroundSyncManager backgroundSyncManager;
|
||||||
late Isar db;
|
late Isar db;
|
||||||
|
|
||||||
setUp(() async {
|
setUp(() async {
|
||||||
@ -25,12 +27,14 @@ void main() {
|
|||||||
authRepository = MockAuthRepository();
|
authRepository = MockAuthRepository();
|
||||||
apiService = MockApiService();
|
apiService = MockApiService();
|
||||||
networkService = MockNetworkService();
|
networkService = MockNetworkService();
|
||||||
|
backgroundSyncManager = MockBackgroundSyncManager();
|
||||||
|
|
||||||
sut = AuthService(
|
sut = AuthService(
|
||||||
authApiRepository,
|
authApiRepository,
|
||||||
authRepository,
|
authRepository,
|
||||||
apiService,
|
apiService,
|
||||||
networkService,
|
networkService,
|
||||||
|
backgroundSyncManager,
|
||||||
);
|
);
|
||||||
|
|
||||||
registerFallbackValue(Uri());
|
registerFallbackValue(Uri());
|
||||||
@ -116,24 +120,28 @@ void main() {
|
|||||||
group('logout', () {
|
group('logout', () {
|
||||||
test('Should logout user', () async {
|
test('Should logout user', () async {
|
||||||
when(() => authApiRepository.logout()).thenAnswer((_) async => {});
|
when(() => authApiRepository.logout()).thenAnswer((_) async => {});
|
||||||
|
when(() => backgroundSyncManager.cancel()).thenAnswer((_) async => {});
|
||||||
when(() => authRepository.clearLocalData())
|
when(() => authRepository.clearLocalData())
|
||||||
.thenAnswer((_) => Future.value(null));
|
.thenAnswer((_) => Future.value(null));
|
||||||
|
|
||||||
await sut.logout();
|
await sut.logout();
|
||||||
|
|
||||||
verify(() => authApiRepository.logout()).called(1);
|
verify(() => authApiRepository.logout()).called(1);
|
||||||
|
verify(() => backgroundSyncManager.cancel()).called(1);
|
||||||
verify(() => authRepository.clearLocalData()).called(1);
|
verify(() => authRepository.clearLocalData()).called(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Should clear local data even on server error', () async {
|
test('Should clear local data even on server error', () async {
|
||||||
when(() => authApiRepository.logout())
|
when(() => authApiRepository.logout())
|
||||||
.thenThrow(Exception('Server error'));
|
.thenThrow(Exception('Server error'));
|
||||||
|
when(() => backgroundSyncManager.cancel()).thenAnswer((_) async => {});
|
||||||
when(() => authRepository.clearLocalData())
|
when(() => authRepository.clearLocalData())
|
||||||
.thenAnswer((_) => Future.value(null));
|
.thenAnswer((_) => Future.value(null));
|
||||||
|
|
||||||
await sut.logout();
|
await sut.logout();
|
||||||
|
|
||||||
verify(() => authApiRepository.logout()).called(1);
|
verify(() => authApiRepository.logout()).called(1);
|
||||||
|
verify(() => backgroundSyncManager.cancel()).called(1);
|
||||||
verify(() => authRepository.clearLocalData()).called(1);
|
verify(() => authRepository.clearLocalData()).called(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -26,9 +26,8 @@
|
|||||||
"migrations:generate": "node ./dist/bin/migrations.js generate",
|
"migrations:generate": "node ./dist/bin/migrations.js generate",
|
||||||
"migrations:create": "node ./dist/bin/migrations.js create",
|
"migrations:create": "node ./dist/bin/migrations.js create",
|
||||||
"migrations:run": "node ./dist/bin/migrations.js run",
|
"migrations:run": "node ./dist/bin/migrations.js run",
|
||||||
"typeorm:migrations:revert": "typeorm migration:revert -d ./dist/bin/database.js",
|
"schema:drop": "node ./dist/bin/migrations.js query 'DROP schema public cascade; CREATE schema public;'",
|
||||||
"typeorm:schema:drop": "typeorm query -d ./dist/bin/database.js 'DROP schema public cascade; CREATE schema public;'",
|
"schema:reset": "npm run schema:drop && npm run migrations:run",
|
||||||
"typeorm:schema:reset": "npm run typeorm:schema:drop && npm run migrations:run",
|
|
||||||
"kysely:codegen": "npx kysely-codegen --include-pattern=\"(public|vectors).*\" --dialect postgres --url postgres://postgres:postgres@localhost/immich --log-level debug --out-file=./src/db.d.ts",
|
"kysely:codegen": "npx kysely-codegen --include-pattern=\"(public|vectors).*\" --dialect postgres --url postgres://postgres:postgres@localhost/immich --log-level debug --out-file=./src/db.d.ts",
|
||||||
"sync:open-api": "node ./dist/bin/sync-open-api.js",
|
"sync:open-api": "node ./dist/bin/sync-open-api.js",
|
||||||
"sync:sql": "node ./dist/bin/sync-sql.js",
|
"sync:sql": "node ./dist/bin/sync-sql.js",
|
||||||
|
@ -1,11 +0,0 @@
|
|||||||
import { ConfigRepository } from 'src/repositories/config.repository';
|
|
||||||
import { DataSource } from 'typeorm';
|
|
||||||
|
|
||||||
const { database } = new ConfigRepository().getEnv();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated - DO NOT USE THIS
|
|
||||||
*
|
|
||||||
* this export is ONLY to be used for TypeORM commands in package.json#scripts
|
|
||||||
*/
|
|
||||||
export const dataSource = new DataSource({ ...database.config.typeorm, host: 'localhost' });
|
|
@ -1,8 +1,8 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
process.env.DB_URL = process.env.DB_URL || 'postgres://postgres:postgres@localhost:5432/immich';
|
process.env.DB_URL = process.env.DB_URL || 'postgres://postgres:postgres@localhost:5432/immich';
|
||||||
|
|
||||||
import { Kysely } from 'kysely';
|
import { Kysely, sql } from 'kysely';
|
||||||
import { writeFileSync } from 'node:fs';
|
import { mkdirSync, writeFileSync } from 'node:fs';
|
||||||
import { basename, dirname, extname, join } from 'node:path';
|
import { basename, dirname, extname, join } from 'node:path';
|
||||||
import postgres from 'postgres';
|
import postgres from 'postgres';
|
||||||
import { ConfigRepository } from 'src/repositories/config.repository';
|
import { ConfigRepository } from 'src/repositories/config.repository';
|
||||||
@ -23,8 +23,13 @@ const main = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
case 'run': {
|
case 'run': {
|
||||||
const only = process.argv[3] as 'kysely' | 'typeorm' | undefined;
|
await runMigrations();
|
||||||
await run(only);
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'query': {
|
||||||
|
const query = process.argv[3];
|
||||||
|
await runQuery(query);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -48,14 +53,25 @@ const main = async () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const run = async (only?: 'kysely' | 'typeorm') => {
|
const getDatabaseClient = () => {
|
||||||
const configRepository = new ConfigRepository();
|
const configRepository = new ConfigRepository();
|
||||||
const { database } = configRepository.getEnv();
|
const { database } = configRepository.getEnv();
|
||||||
const logger = new LoggingRepository(undefined, configRepository);
|
return new Kysely<any>(getKyselyConfig(database.config.kysely));
|
||||||
const db = new Kysely<any>(getKyselyConfig(database.config.kysely));
|
};
|
||||||
const databaseRepository = new DatabaseRepository(db, logger, configRepository);
|
|
||||||
|
|
||||||
await databaseRepository.runMigrations({ only });
|
const runQuery = async (query: string) => {
|
||||||
|
const db = getDatabaseClient();
|
||||||
|
await sql.raw(query).execute(db);
|
||||||
|
await db.destroy();
|
||||||
|
};
|
||||||
|
|
||||||
|
const runMigrations = async () => {
|
||||||
|
const configRepository = new ConfigRepository();
|
||||||
|
const logger = new LoggingRepository(undefined, configRepository);
|
||||||
|
const db = getDatabaseClient();
|
||||||
|
const databaseRepository = new DatabaseRepository(db, logger, configRepository);
|
||||||
|
await databaseRepository.runMigrations();
|
||||||
|
await db.destroy();
|
||||||
};
|
};
|
||||||
|
|
||||||
const debug = async () => {
|
const debug = async () => {
|
||||||
@ -81,7 +97,8 @@ const create = (path: string, up: string[], down: string[]) => {
|
|||||||
const filename = `${timestamp}-${name}.ts`;
|
const filename = `${timestamp}-${name}.ts`;
|
||||||
const folder = dirname(path);
|
const folder = dirname(path);
|
||||||
const fullPath = join(folder, filename);
|
const fullPath = join(folder, filename);
|
||||||
writeFileSync(fullPath, asMigration('typeorm', { name, timestamp, up, down }));
|
mkdirSync(folder, { recursive: true });
|
||||||
|
writeFileSync(fullPath, asMigration('kysely', { name, timestamp, up, down }));
|
||||||
console.log(`Wrote ${fullPath}`);
|
console.log(`Wrote ${fullPath}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
import { Selectable } from 'kysely';
|
import { Selectable } from 'kysely';
|
||||||
import { AssetJobStatus as DatabaseAssetJobStatus, Exif as DatabaseExif } from 'src/db';
|
import { Albums, Exif as DatabaseExif } from 'src/db';
|
||||||
import { AssetEntity } from 'src/entities/asset.entity';
|
import { MapAsset } from 'src/dtos/asset-response.dto';
|
||||||
import {
|
import {
|
||||||
AlbumUserRole,
|
AlbumUserRole,
|
||||||
AssetFileType,
|
AssetFileType,
|
||||||
AssetStatus,
|
|
||||||
AssetType,
|
AssetType,
|
||||||
MemoryType,
|
MemoryType,
|
||||||
Permission,
|
Permission,
|
||||||
|
SharedLinkType,
|
||||||
SourceType,
|
SourceType,
|
||||||
UserStatus,
|
UserStatus,
|
||||||
} from 'src/enum';
|
} from 'src/enum';
|
||||||
@ -44,7 +44,7 @@ export type Library = {
|
|||||||
exclusionPatterns: string[];
|
exclusionPatterns: string[];
|
||||||
deletedAt: Date | null;
|
deletedAt: Date | null;
|
||||||
refreshedAt: Date | null;
|
refreshedAt: Date | null;
|
||||||
assets?: Asset[];
|
assets?: MapAsset[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type AuthApiKey = {
|
export type AuthApiKey = {
|
||||||
@ -96,7 +96,26 @@ export type Memory = {
|
|||||||
data: OnThisDayData;
|
data: OnThisDayData;
|
||||||
ownerId: string;
|
ownerId: string;
|
||||||
isSaved: boolean;
|
isSaved: boolean;
|
||||||
assets: Asset[];
|
assets: MapAsset[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Asset = {
|
||||||
|
id: string;
|
||||||
|
checksum: Buffer<ArrayBufferLike>;
|
||||||
|
deviceAssetId: string;
|
||||||
|
deviceId: string;
|
||||||
|
fileCreatedAt: Date;
|
||||||
|
fileModifiedAt: Date;
|
||||||
|
isExternal: boolean;
|
||||||
|
isVisible: boolean;
|
||||||
|
libraryId: string | null;
|
||||||
|
livePhotoVideoId: string | null;
|
||||||
|
localDateTime: Date;
|
||||||
|
originalFileName: string;
|
||||||
|
originalPath: string;
|
||||||
|
ownerId: string;
|
||||||
|
sidecarPath: string | null;
|
||||||
|
type: AssetType;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type User = {
|
export type User = {
|
||||||
@ -128,39 +147,6 @@ export type StorageAsset = {
|
|||||||
encodedVideoPath: string | null;
|
encodedVideoPath: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Asset = {
|
|
||||||
createdAt: Date;
|
|
||||||
updatedAt: Date;
|
|
||||||
deletedAt: Date | null;
|
|
||||||
id: string;
|
|
||||||
updateId: string;
|
|
||||||
status: AssetStatus;
|
|
||||||
checksum: Buffer<ArrayBufferLike>;
|
|
||||||
deviceAssetId: string;
|
|
||||||
deviceId: string;
|
|
||||||
duplicateId: string | null;
|
|
||||||
duration: string | null;
|
|
||||||
encodedVideoPath: string | null;
|
|
||||||
fileCreatedAt: Date | null;
|
|
||||||
fileModifiedAt: Date | null;
|
|
||||||
isArchived: boolean;
|
|
||||||
isExternal: boolean;
|
|
||||||
isFavorite: boolean;
|
|
||||||
isOffline: boolean;
|
|
||||||
isVisible: boolean;
|
|
||||||
libraryId: string | null;
|
|
||||||
livePhotoVideoId: string | null;
|
|
||||||
localDateTime: Date | null;
|
|
||||||
originalFileName: string;
|
|
||||||
originalPath: string;
|
|
||||||
ownerId: string;
|
|
||||||
sidecarPath: string | null;
|
|
||||||
stack?: Stack | null;
|
|
||||||
stackId: string | null;
|
|
||||||
thumbhash: Buffer<ArrayBufferLike> | null;
|
|
||||||
type: AssetType;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type SidecarWriteAsset = {
|
export type SidecarWriteAsset = {
|
||||||
id: string;
|
id: string;
|
||||||
sidecarPath: string | null;
|
sidecarPath: string | null;
|
||||||
@ -173,7 +159,7 @@ export type Stack = {
|
|||||||
primaryAssetId: string;
|
primaryAssetId: string;
|
||||||
owner?: User;
|
owner?: User;
|
||||||
ownerId: string;
|
ownerId: string;
|
||||||
assets: AssetEntity[];
|
assets: MapAsset[];
|
||||||
assetCount?: number;
|
assetCount?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -187,6 +173,28 @@ export type AuthSharedLink = {
|
|||||||
password: string | null;
|
password: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type SharedLink = {
|
||||||
|
id: string;
|
||||||
|
album?: Album | null;
|
||||||
|
albumId: string | null;
|
||||||
|
allowDownload: boolean;
|
||||||
|
allowUpload: boolean;
|
||||||
|
assets: MapAsset[];
|
||||||
|
createdAt: Date;
|
||||||
|
description: string | null;
|
||||||
|
expiresAt: Date | null;
|
||||||
|
key: Buffer;
|
||||||
|
password: string | null;
|
||||||
|
showExif: boolean;
|
||||||
|
type: SharedLinkType;
|
||||||
|
userId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Album = Selectable<Albums> & {
|
||||||
|
owner: User;
|
||||||
|
assets: MapAsset[];
|
||||||
|
};
|
||||||
|
|
||||||
export type AuthSession = {
|
export type AuthSession = {
|
||||||
id: string;
|
id: string;
|
||||||
};
|
};
|
||||||
@ -256,10 +264,6 @@ export type AssetFace = {
|
|||||||
person?: Person | null;
|
person?: Person | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type AssetJobStatus = Selectable<DatabaseAssetJobStatus> & {
|
|
||||||
asset: AssetEntity;
|
|
||||||
};
|
|
||||||
|
|
||||||
const userColumns = ['id', 'name', 'email', 'profileImagePath', 'profileChangedAt'] as const;
|
const userColumns = ['id', 'name', 'email', 'profileImagePath', 'profileChangedAt'] as const;
|
||||||
|
|
||||||
export const columns = {
|
export const columns = {
|
||||||
|
4
server/src/db.d.ts
vendored
4
server/src/db.d.ts
vendored
@ -143,8 +143,8 @@ export interface Assets {
|
|||||||
duplicateId: string | null;
|
duplicateId: string | null;
|
||||||
duration: string | null;
|
duration: string | null;
|
||||||
encodedVideoPath: Generated<string | null>;
|
encodedVideoPath: Generated<string | null>;
|
||||||
fileCreatedAt: Timestamp | null;
|
fileCreatedAt: Timestamp;
|
||||||
fileModifiedAt: Timestamp | null;
|
fileModifiedAt: Timestamp;
|
||||||
id: Generated<string>;
|
id: Generated<string>;
|
||||||
isArchived: Generated<boolean>;
|
isArchived: Generated<boolean>;
|
||||||
isExternal: Generated<boolean>;
|
isExternal: Generated<boolean>;
|
||||||
|
@ -11,7 +11,8 @@ import { setUnion } from 'src/utils/set';
|
|||||||
const GeneratedUuidV7Column = (options: Omit<ColumnOptions, 'type' | 'default' | 'nullable'> = {}) =>
|
const GeneratedUuidV7Column = (options: Omit<ColumnOptions, 'type' | 'default' | 'nullable'> = {}) =>
|
||||||
Column({ ...options, type: 'uuid', nullable: false, default: () => `${immich_uuid_v7.name}()` });
|
Column({ ...options, type: 'uuid', nullable: false, default: () => `${immich_uuid_v7.name}()` });
|
||||||
|
|
||||||
export const UpdateIdColumn = () => GeneratedUuidV7Column();
|
export const UpdateIdColumn = (options: Omit<ColumnOptions, 'type' | 'default' | 'nullable'> = {}) =>
|
||||||
|
GeneratedUuidV7Column(options);
|
||||||
|
|
||||||
export const PrimaryGeneratedUuidV7Column = () => GeneratedUuidV7Column({ primary: true });
|
export const PrimaryGeneratedUuidV7Column = () => GeneratedUuidV7Column({ primary: true });
|
||||||
|
|
||||||
|
@ -2,10 +2,10 @@ import { ApiProperty } from '@nestjs/swagger';
|
|||||||
import { Type } from 'class-transformer';
|
import { Type } from 'class-transformer';
|
||||||
import { ArrayNotEmpty, IsArray, IsEnum, IsString, ValidateNested } from 'class-validator';
|
import { ArrayNotEmpty, IsArray, IsEnum, IsString, ValidateNested } from 'class-validator';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto';
|
import { AlbumUser, AuthSharedLink, User } from 'src/database';
|
||||||
|
import { AssetResponseDto, MapAsset, mapAsset } from 'src/dtos/asset-response.dto';
|
||||||
import { AuthDto } from 'src/dtos/auth.dto';
|
import { AuthDto } from 'src/dtos/auth.dto';
|
||||||
import { UserResponseDto, mapUser } from 'src/dtos/user.dto';
|
import { UserResponseDto, mapUser } from 'src/dtos/user.dto';
|
||||||
import { AlbumEntity } from 'src/entities/album.entity';
|
|
||||||
import { AlbumUserRole, AssetOrder } from 'src/enum';
|
import { AlbumUserRole, AssetOrder } from 'src/enum';
|
||||||
import { Optional, ValidateBoolean, ValidateUUID } from 'src/validation';
|
import { Optional, ValidateBoolean, ValidateUUID } from 'src/validation';
|
||||||
|
|
||||||
@ -142,7 +142,23 @@ export class AlbumResponseDto {
|
|||||||
order?: AssetOrder;
|
order?: AssetOrder;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const mapAlbum = (entity: AlbumEntity, withAssets: boolean, auth?: AuthDto): AlbumResponseDto => {
|
export type MapAlbumDto = {
|
||||||
|
albumUsers?: AlbumUser[];
|
||||||
|
assets?: MapAsset[];
|
||||||
|
sharedLinks?: AuthSharedLink[];
|
||||||
|
albumName: string;
|
||||||
|
description: string;
|
||||||
|
albumThumbnailAssetId: string | null;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
id: string;
|
||||||
|
ownerId: string;
|
||||||
|
owner: User;
|
||||||
|
isActivityEnabled: boolean;
|
||||||
|
order: AssetOrder;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const mapAlbum = (entity: MapAlbumDto, withAssets: boolean, auth?: AuthDto): AlbumResponseDto => {
|
||||||
const albumUsers: AlbumUserResponseDto[] = [];
|
const albumUsers: AlbumUserResponseDto[] = [];
|
||||||
|
|
||||||
if (entity.albumUsers) {
|
if (entity.albumUsers) {
|
||||||
@ -159,7 +175,7 @@ export const mapAlbum = (entity: AlbumEntity, withAssets: boolean, auth?: AuthDt
|
|||||||
|
|
||||||
const assets = entity.assets || [];
|
const assets = entity.assets || [];
|
||||||
|
|
||||||
const hasSharedLink = entity.sharedLinks?.length > 0;
|
const hasSharedLink = !!entity.sharedLinks && entity.sharedLinks.length > 0;
|
||||||
const hasSharedUser = albumUsers.length > 0;
|
const hasSharedUser = albumUsers.length > 0;
|
||||||
|
|
||||||
let startDate = assets.at(0)?.localDateTime;
|
let startDate = assets.at(0)?.localDateTime;
|
||||||
@ -190,5 +206,5 @@ export const mapAlbum = (entity: AlbumEntity, withAssets: boolean, auth?: AuthDt
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const mapAlbumWithAssets = (entity: AlbumEntity) => mapAlbum(entity, true);
|
export const mapAlbumWithAssets = (entity: MapAlbumDto) => mapAlbum(entity, true);
|
||||||
export const mapAlbumWithoutAssets = (entity: AlbumEntity) => mapAlbum(entity, false);
|
export const mapAlbumWithoutAssets = (entity: MapAlbumDto) => mapAlbum(entity, false);
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { ApiProperty } from '@nestjs/swagger';
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
import { AssetFace } from 'src/database';
|
import { Selectable } from 'kysely';
|
||||||
|
import { AssetFace, AssetFile, Exif, Stack, Tag, User } from 'src/database';
|
||||||
import { PropertyLifecycle } from 'src/decorators';
|
import { PropertyLifecycle } from 'src/decorators';
|
||||||
import { AuthDto } from 'src/dtos/auth.dto';
|
import { AuthDto } from 'src/dtos/auth.dto';
|
||||||
import { ExifResponseDto, mapExif } from 'src/dtos/exif.dto';
|
import { ExifResponseDto, mapExif } from 'src/dtos/exif.dto';
|
||||||
@ -11,8 +12,7 @@ import {
|
|||||||
} from 'src/dtos/person.dto';
|
} from 'src/dtos/person.dto';
|
||||||
import { TagResponseDto, mapTag } from 'src/dtos/tag.dto';
|
import { TagResponseDto, mapTag } from 'src/dtos/tag.dto';
|
||||||
import { UserResponseDto, mapUser } from 'src/dtos/user.dto';
|
import { UserResponseDto, mapUser } from 'src/dtos/user.dto';
|
||||||
import { AssetEntity } from 'src/entities/asset.entity';
|
import { AssetStatus, AssetType } from 'src/enum';
|
||||||
import { AssetType } from 'src/enum';
|
|
||||||
import { hexOrBufferToBase64 } from 'src/utils/bytes';
|
import { hexOrBufferToBase64 } from 'src/utils/bytes';
|
||||||
import { mimeTypes } from 'src/utils/mime-types';
|
import { mimeTypes } from 'src/utils/mime-types';
|
||||||
|
|
||||||
@ -57,6 +57,44 @@ export class AssetResponseDto extends SanitizedAssetResponseDto {
|
|||||||
resized?: boolean;
|
resized?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type MapAsset = {
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
deletedAt: Date | null;
|
||||||
|
id: string;
|
||||||
|
updateId: string;
|
||||||
|
status: AssetStatus;
|
||||||
|
checksum: Buffer<ArrayBufferLike>;
|
||||||
|
deviceAssetId: string;
|
||||||
|
deviceId: string;
|
||||||
|
duplicateId: string | null;
|
||||||
|
duration: string | null;
|
||||||
|
encodedVideoPath: string | null;
|
||||||
|
exifInfo?: Selectable<Exif> | null;
|
||||||
|
faces?: AssetFace[];
|
||||||
|
fileCreatedAt: Date;
|
||||||
|
fileModifiedAt: Date;
|
||||||
|
files?: AssetFile[];
|
||||||
|
isArchived: boolean;
|
||||||
|
isExternal: boolean;
|
||||||
|
isFavorite: boolean;
|
||||||
|
isOffline: boolean;
|
||||||
|
isVisible: boolean;
|
||||||
|
libraryId: string | null;
|
||||||
|
livePhotoVideoId: string | null;
|
||||||
|
localDateTime: Date;
|
||||||
|
originalFileName: string;
|
||||||
|
originalPath: string;
|
||||||
|
owner?: User | null;
|
||||||
|
ownerId: string;
|
||||||
|
sidecarPath: string | null;
|
||||||
|
stack?: Stack | null;
|
||||||
|
stackId: string | null;
|
||||||
|
tags?: Tag[];
|
||||||
|
thumbhash: Buffer<ArrayBufferLike> | null;
|
||||||
|
type: AssetType;
|
||||||
|
};
|
||||||
|
|
||||||
export class AssetStackResponseDto {
|
export class AssetStackResponseDto {
|
||||||
id!: string;
|
id!: string;
|
||||||
|
|
||||||
@ -73,7 +111,7 @@ export type AssetMapOptions = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// TODO: this is inefficient
|
// TODO: this is inefficient
|
||||||
const peopleWithFaces = (faces: AssetFace[]): PersonWithFacesResponseDto[] => {
|
const peopleWithFaces = (faces?: AssetFace[]): PersonWithFacesResponseDto[] => {
|
||||||
const result: PersonWithFacesResponseDto[] = [];
|
const result: PersonWithFacesResponseDto[] = [];
|
||||||
if (faces) {
|
if (faces) {
|
||||||
for (const face of faces) {
|
for (const face of faces) {
|
||||||
@ -91,7 +129,7 @@ const peopleWithFaces = (faces: AssetFace[]): PersonWithFacesResponseDto[] => {
|
|||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapStack = (entity: AssetEntity) => {
|
const mapStack = (entity: { stack?: Stack | null }) => {
|
||||||
if (!entity.stack) {
|
if (!entity.stack) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -103,7 +141,7 @@ const mapStack = (entity: AssetEntity) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export function mapAsset(entity: AssetEntity, options: AssetMapOptions = {}): AssetResponseDto {
|
export function mapAsset(entity: MapAsset, options: AssetMapOptions = {}): AssetResponseDto {
|
||||||
const { stripMetadata = false, withStack = false } = options;
|
const { stripMetadata = false, withStack = false } = options;
|
||||||
|
|
||||||
if (stripMetadata) {
|
if (stripMetadata) {
|
||||||
|
@ -4,7 +4,6 @@ import { IsEnum, IsInt, IsObject, IsPositive, ValidateNested } from 'class-valid
|
|||||||
import { Memory } from 'src/database';
|
import { Memory } from 'src/database';
|
||||||
import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto';
|
import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto';
|
||||||
import { AuthDto } from 'src/dtos/auth.dto';
|
import { AuthDto } from 'src/dtos/auth.dto';
|
||||||
import { AssetEntity } from 'src/entities/asset.entity';
|
|
||||||
import { MemoryType } from 'src/enum';
|
import { MemoryType } from 'src/enum';
|
||||||
import { Optional, ValidateBoolean, ValidateDate, ValidateUUID } from 'src/validation';
|
import { Optional, ValidateBoolean, ValidateDate, ValidateUUID } from 'src/validation';
|
||||||
|
|
||||||
@ -103,6 +102,6 @@ export const mapMemory = (entity: Memory, auth: AuthDto): MemoryResponseDto => {
|
|||||||
type: entity.type as MemoryType,
|
type: entity.type as MemoryType,
|
||||||
data: entity.data as unknown as MemoryData,
|
data: entity.data as unknown as MemoryData,
|
||||||
isSaved: entity.isSaved,
|
isSaved: entity.isSaved,
|
||||||
assets: ('assets' in entity ? entity.assets : []).map((asset) => mapAsset(asset as AssetEntity, { auth })),
|
assets: ('assets' in entity ? entity.assets : []).map((asset) => mapAsset(asset, { auth })),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import { ApiProperty } from '@nestjs/swagger';
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
import { IsEnum, IsString } from 'class-validator';
|
import { IsEnum, IsString } from 'class-validator';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
|
import { SharedLink } from 'src/database';
|
||||||
import { AlbumResponseDto, mapAlbumWithoutAssets } from 'src/dtos/album.dto';
|
import { AlbumResponseDto, mapAlbumWithoutAssets } from 'src/dtos/album.dto';
|
||||||
import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto';
|
import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto';
|
||||||
import { SharedLinkEntity } from 'src/entities/shared-link.entity';
|
|
||||||
import { SharedLinkType } from 'src/enum';
|
import { SharedLinkType } from 'src/enum';
|
||||||
import { Optional, ValidateBoolean, ValidateDate, ValidateUUID } from 'src/validation';
|
import { Optional, ValidateBoolean, ValidateDate, ValidateUUID } from 'src/validation';
|
||||||
|
|
||||||
@ -102,7 +102,7 @@ export class SharedLinkResponseDto {
|
|||||||
showMetadata!: boolean;
|
showMetadata!: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function mapSharedLink(sharedLink: SharedLinkEntity): SharedLinkResponseDto {
|
export function mapSharedLink(sharedLink: SharedLink): SharedLinkResponseDto {
|
||||||
const linkAssets = sharedLink.assets || [];
|
const linkAssets = sharedLink.assets || [];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -122,7 +122,7 @@ export function mapSharedLink(sharedLink: SharedLinkEntity): SharedLinkResponseD
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function mapSharedLinkWithoutMetadata(sharedLink: SharedLinkEntity): SharedLinkResponseDto {
|
export function mapSharedLinkWithoutMetadata(sharedLink: SharedLink): SharedLinkResponseDto {
|
||||||
const linkAssets = sharedLink.assets || [];
|
const linkAssets = sharedLink.assets || [];
|
||||||
const albumAssets = (sharedLink?.album?.assets || []).map((asset) => asset);
|
const albumAssets = (sharedLink?.album?.assets || []).map((asset) => asset);
|
||||||
|
|
||||||
@ -137,7 +137,7 @@ export function mapSharedLinkWithoutMetadata(sharedLink: SharedLinkEntity): Shar
|
|||||||
type: sharedLink.type,
|
type: sharedLink.type,
|
||||||
createdAt: sharedLink.createdAt,
|
createdAt: sharedLink.createdAt,
|
||||||
expiresAt: sharedLink.expiresAt,
|
expiresAt: sharedLink.expiresAt,
|
||||||
assets: assets.map((asset) => mapAsset(asset, { stripMetadata: true })) as AssetResponseDto[],
|
assets: assets.map((asset) => mapAsset(asset, { stripMetadata: true })),
|
||||||
album: sharedLink.album ? mapAlbumWithoutAssets(sharedLink.album) : undefined,
|
album: sharedLink.album ? mapAlbumWithoutAssets(sharedLink.album) : undefined,
|
||||||
allowUpload: sharedLink.allowUpload,
|
allowUpload: sharedLink.allowUpload,
|
||||||
allowDownload: sharedLink.allowDownload,
|
allowDownload: sharedLink.allowDownload,
|
||||||
|
@ -1,23 +0,0 @@
|
|||||||
import { AlbumUser, User } from 'src/database';
|
|
||||||
import { AssetEntity } from 'src/entities/asset.entity';
|
|
||||||
import { SharedLinkEntity } from 'src/entities/shared-link.entity';
|
|
||||||
import { AssetOrder } from 'src/enum';
|
|
||||||
|
|
||||||
export class AlbumEntity {
|
|
||||||
id!: string;
|
|
||||||
owner!: User;
|
|
||||||
ownerId!: string;
|
|
||||||
albumName!: string;
|
|
||||||
description!: string;
|
|
||||||
createdAt!: Date;
|
|
||||||
updatedAt!: Date;
|
|
||||||
updateId?: string;
|
|
||||||
deletedAt!: Date | null;
|
|
||||||
albumThumbnailAsset!: AssetEntity | null;
|
|
||||||
albumThumbnailAssetId!: string | null;
|
|
||||||
albumUsers!: AlbumUser[];
|
|
||||||
assets!: AssetEntity[];
|
|
||||||
sharedLinks!: SharedLinkEntity[];
|
|
||||||
isActivityEnabled!: boolean;
|
|
||||||
order!: AssetOrder;
|
|
||||||
}
|
|
@ -1,311 +0,0 @@
|
|||||||
import {
|
|
||||||
DeduplicateJoinsPlugin,
|
|
||||||
Expression,
|
|
||||||
expressionBuilder,
|
|
||||||
ExpressionBuilder,
|
|
||||||
Kysely,
|
|
||||||
SelectQueryBuilder,
|
|
||||||
sql,
|
|
||||||
} from 'kysely';
|
|
||||||
import { jsonArrayFrom, jsonObjectFrom } from 'kysely/helpers/postgres';
|
|
||||||
import { AssetFace, AssetFile, AssetJobStatus, columns, Exif, Stack, Tag, User } from 'src/database';
|
|
||||||
import { DB } from 'src/db';
|
|
||||||
import { SharedLinkEntity } from 'src/entities/shared-link.entity';
|
|
||||||
import { AssetFileType, AssetStatus, AssetType } from 'src/enum';
|
|
||||||
import { TimeBucketSize } from 'src/repositories/asset.repository';
|
|
||||||
import { AssetSearchBuilderOptions } from 'src/repositories/search.repository';
|
|
||||||
import { anyUuid, asUuid } from 'src/utils/database';
|
|
||||||
|
|
||||||
export const ASSET_CHECKSUM_CONSTRAINT = 'UQ_assets_owner_checksum';
|
|
||||||
|
|
||||||
export class AssetEntity {
|
|
||||||
id!: string;
|
|
||||||
deviceAssetId!: string;
|
|
||||||
owner!: User;
|
|
||||||
ownerId!: string;
|
|
||||||
libraryId?: string | null;
|
|
||||||
deviceId!: string;
|
|
||||||
type!: AssetType;
|
|
||||||
status!: AssetStatus;
|
|
||||||
originalPath!: string;
|
|
||||||
files!: AssetFile[];
|
|
||||||
thumbhash!: Buffer | null;
|
|
||||||
encodedVideoPath!: string | null;
|
|
||||||
createdAt!: Date;
|
|
||||||
updatedAt!: Date;
|
|
||||||
updateId?: string;
|
|
||||||
deletedAt!: Date | null;
|
|
||||||
fileCreatedAt!: Date;
|
|
||||||
localDateTime!: Date;
|
|
||||||
fileModifiedAt!: Date;
|
|
||||||
isFavorite!: boolean;
|
|
||||||
isArchived!: boolean;
|
|
||||||
isExternal!: boolean;
|
|
||||||
isOffline!: boolean;
|
|
||||||
checksum!: Buffer; // sha1 checksum
|
|
||||||
duration!: string | null;
|
|
||||||
isVisible!: boolean;
|
|
||||||
livePhotoVideo!: AssetEntity | null;
|
|
||||||
livePhotoVideoId!: string | null;
|
|
||||||
originalFileName!: string;
|
|
||||||
sidecarPath!: string | null;
|
|
||||||
exifInfo?: Exif;
|
|
||||||
tags?: Tag[];
|
|
||||||
sharedLinks!: SharedLinkEntity[];
|
|
||||||
faces!: AssetFace[];
|
|
||||||
stackId?: string | null;
|
|
||||||
stack?: Stack | null;
|
|
||||||
jobStatus?: AssetJobStatus;
|
|
||||||
duplicateId!: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function withExif<O>(qb: SelectQueryBuilder<DB, 'assets', O>) {
|
|
||||||
return qb
|
|
||||||
.leftJoin('exif', 'assets.id', 'exif.assetId')
|
|
||||||
.select((eb) => eb.fn.toJson(eb.table('exif')).$castTo<Exif | null>().as('exifInfo'));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function withExifInner<O>(qb: SelectQueryBuilder<DB, 'assets', O>) {
|
|
||||||
return qb
|
|
||||||
.innerJoin('exif', 'assets.id', 'exif.assetId')
|
|
||||||
.select((eb) => eb.fn.toJson(eb.table('exif')).$castTo<Exif>().as('exifInfo'));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function withSmartSearch<O>(qb: SelectQueryBuilder<DB, 'assets', O>) {
|
|
||||||
return qb
|
|
||||||
.leftJoin('smart_search', 'assets.id', 'smart_search.assetId')
|
|
||||||
.select((eb) => eb.fn.toJson(eb.table('smart_search')).as('smartSearch'));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function withFaces(eb: ExpressionBuilder<DB, 'assets'>, withDeletedFace?: boolean) {
|
|
||||||
return jsonArrayFrom(
|
|
||||||
eb
|
|
||||||
.selectFrom('asset_faces')
|
|
||||||
.selectAll('asset_faces')
|
|
||||||
.whereRef('asset_faces.assetId', '=', 'assets.id')
|
|
||||||
.$if(!withDeletedFace, (qb) => qb.where('asset_faces.deletedAt', 'is', null)),
|
|
||||||
).as('faces');
|
|
||||||
}
|
|
||||||
|
|
||||||
export function withFiles(eb: ExpressionBuilder<DB, 'assets'>, type?: AssetFileType) {
|
|
||||||
return jsonArrayFrom(
|
|
||||||
eb
|
|
||||||
.selectFrom('asset_files')
|
|
||||||
.select(columns.assetFiles)
|
|
||||||
.whereRef('asset_files.assetId', '=', 'assets.id')
|
|
||||||
.$if(!!type, (qb) => qb.where('asset_files.type', '=', type!)),
|
|
||||||
).as('files');
|
|
||||||
}
|
|
||||||
|
|
||||||
export function withFacesAndPeople(eb: ExpressionBuilder<DB, 'assets'>, withDeletedFace?: boolean) {
|
|
||||||
return jsonArrayFrom(
|
|
||||||
eb
|
|
||||||
.selectFrom('asset_faces')
|
|
||||||
.leftJoinLateral(
|
|
||||||
(eb) =>
|
|
||||||
eb.selectFrom('person').selectAll('person').whereRef('asset_faces.personId', '=', 'person.id').as('person'),
|
|
||||||
(join) => join.onTrue(),
|
|
||||||
)
|
|
||||||
.selectAll('asset_faces')
|
|
||||||
.select((eb) => eb.table('person').as('person'))
|
|
||||||
.whereRef('asset_faces.assetId', '=', 'assets.id')
|
|
||||||
.$if(!withDeletedFace, (qb) => qb.where('asset_faces.deletedAt', 'is', null)),
|
|
||||||
).as('faces');
|
|
||||||
}
|
|
||||||
|
|
||||||
// export function hasPeople<O>(qb: SelectQueryBuilder<DB, 'assets', O>, personIds: string[]) {
|
|
||||||
// return qb.innerJoin(
|
|
||||||
// (eb) =>
|
|
||||||
// eb
|
|
||||||
// .selectFrom('asset_faces')
|
|
||||||
// .select('assetId')
|
|
||||||
// .where('personId', '=', anyUuid(personIds!))
|
|
||||||
// .where('deletedAt', 'is', null)
|
|
||||||
// .groupBy('assetId')
|
|
||||||
// .having((eb) => eb.fn.count('personId').distinct(), '=', personIds.length)
|
|
||||||
// .as('has_people'),
|
|
||||||
// (join) => join.onRef('has_people.assetId', '=', 'assets.id'),
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
export function hasPeople(personIds: string[]) {
|
|
||||||
const eb = expressionBuilder<DB, never>();
|
|
||||||
return eb
|
|
||||||
.selectFrom('asset_faces')
|
|
||||||
.select('assetId')
|
|
||||||
.where('personId', '=', anyUuid(personIds!))
|
|
||||||
.where('deletedAt', 'is', null)
|
|
||||||
.groupBy('assetId')
|
|
||||||
.having((eb) => eb.fn.count('personId').distinct(), '=', personIds.length)
|
|
||||||
.as('has_people');
|
|
||||||
}
|
|
||||||
|
|
||||||
export function hasTags<O>(qb: SelectQueryBuilder<DB, 'assets', O>, tagIds: string[]) {
|
|
||||||
return qb.innerJoin(
|
|
||||||
(eb) =>
|
|
||||||
eb
|
|
||||||
.selectFrom('tag_asset')
|
|
||||||
.select('assetsId')
|
|
||||||
.innerJoin('tags_closure', 'tag_asset.tagsId', 'tags_closure.id_descendant')
|
|
||||||
.where('tags_closure.id_ancestor', '=', anyUuid(tagIds))
|
|
||||||
.groupBy('assetsId')
|
|
||||||
.having((eb) => eb.fn.count('tags_closure.id_ancestor').distinct(), '>=', tagIds.length)
|
|
||||||
.as('has_tags'),
|
|
||||||
(join) => join.onRef('has_tags.assetsId', '=', 'assets.id'),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function withOwner(eb: ExpressionBuilder<DB, 'assets'>) {
|
|
||||||
return jsonObjectFrom(eb.selectFrom('users').selectAll().whereRef('users.id', '=', 'assets.ownerId')).as('owner');
|
|
||||||
}
|
|
||||||
|
|
||||||
export function withLibrary(eb: ExpressionBuilder<DB, 'assets'>) {
|
|
||||||
return jsonObjectFrom(eb.selectFrom('libraries').selectAll().whereRef('libraries.id', '=', 'assets.libraryId')).as(
|
|
||||||
'library',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function withTags(eb: ExpressionBuilder<DB, 'assets'>) {
|
|
||||||
return jsonArrayFrom(
|
|
||||||
eb
|
|
||||||
.selectFrom('tags')
|
|
||||||
.select(columns.tag)
|
|
||||||
.innerJoin('tag_asset', 'tags.id', 'tag_asset.tagsId')
|
|
||||||
.whereRef('assets.id', '=', 'tag_asset.assetsId'),
|
|
||||||
).as('tags');
|
|
||||||
}
|
|
||||||
|
|
||||||
export function truncatedDate<O>(size: TimeBucketSize) {
|
|
||||||
return sql<O>`date_trunc(${size}, "localDateTime" at time zone 'UTC') at time zone 'UTC'`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// export function withTagId<O>(qb: SelectQueryBuilder<DB, 'assets', O>, tagId: string) {
|
|
||||||
// return qb.where((eb) =>
|
|
||||||
// eb.exists(
|
|
||||||
// eb
|
|
||||||
// .selectFrom('tags_closure')
|
|
||||||
// .innerJoin('tag_asset', 'tag_asset.tagsId', 'tags_closure.id_descendant')
|
|
||||||
// .whereRef('tag_asset.assetsId', '=', 'assets.id')
|
|
||||||
// .where('tags_closure.id_ancestor', '=', tagId),
|
|
||||||
// ),
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
export function withTagId(tagId: string, assetId: Expression<string>) {
|
|
||||||
const eb = expressionBuilder<DB, never>();
|
|
||||||
return eb.exists(
|
|
||||||
eb
|
|
||||||
.selectFrom('tags_closure')
|
|
||||||
.innerJoin('tag_asset', 'tag_asset.tagsId', 'tags_closure.id_descendant')
|
|
||||||
.whereRef('tag_asset.assetsId', '=', assetId)
|
|
||||||
.where('tags_closure.id_ancestor', '=', tagId),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const joinDeduplicationPlugin = new DeduplicateJoinsPlugin();
|
|
||||||
|
|
||||||
/** TODO: This should only be used for search-related queries, not as a general purpose query builder */
|
|
||||||
export function searchAssetBuilder(kysely: Kysely<DB>, options: AssetSearchBuilderOptions) {
|
|
||||||
options.isArchived ??= options.withArchived ? undefined : false;
|
|
||||||
options.withDeleted ||= !!(options.trashedAfter || options.trashedBefore || options.isOffline);
|
|
||||||
return (
|
|
||||||
kysely
|
|
||||||
.withPlugin(joinDeduplicationPlugin)
|
|
||||||
.selectFrom('assets')
|
|
||||||
.selectAll('assets')
|
|
||||||
.$if(!!options.tagIds && options.tagIds.length > 0, (qb) => hasTags(qb, options.tagIds!))
|
|
||||||
// .$if(!!options.personIds && options.personIds.length > 0, (qb) => hasPeople(qb, options.personIds!))
|
|
||||||
.$if(!!options.personIds && options.personIds.length > 0, (qb) =>
|
|
||||||
qb.innerJoin(
|
|
||||||
() => hasPeople(options.personIds!),
|
|
||||||
(join) => join.onRef('has_people.assetId', '=', 'assets.id'),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.$if(!!options.createdBefore, (qb) => qb.where('assets.createdAt', '<=', options.createdBefore!))
|
|
||||||
.$if(!!options.createdAfter, (qb) => qb.where('assets.createdAt', '>=', options.createdAfter!))
|
|
||||||
.$if(!!options.updatedBefore, (qb) => qb.where('assets.updatedAt', '<=', options.updatedBefore!))
|
|
||||||
.$if(!!options.updatedAfter, (qb) => qb.where('assets.updatedAt', '>=', options.updatedAfter!))
|
|
||||||
.$if(!!options.trashedBefore, (qb) => qb.where('assets.deletedAt', '<=', options.trashedBefore!))
|
|
||||||
.$if(!!options.trashedAfter, (qb) => qb.where('assets.deletedAt', '>=', options.trashedAfter!))
|
|
||||||
.$if(!!options.takenBefore, (qb) => qb.where('assets.fileCreatedAt', '<=', options.takenBefore!))
|
|
||||||
.$if(!!options.takenAfter, (qb) => qb.where('assets.fileCreatedAt', '>=', options.takenAfter!))
|
|
||||||
.$if(options.city !== undefined, (qb) =>
|
|
||||||
qb
|
|
||||||
.innerJoin('exif', 'assets.id', 'exif.assetId')
|
|
||||||
.where('exif.city', options.city === null ? 'is' : '=', options.city!),
|
|
||||||
)
|
|
||||||
.$if(options.state !== undefined, (qb) =>
|
|
||||||
qb
|
|
||||||
.innerJoin('exif', 'assets.id', 'exif.assetId')
|
|
||||||
.where('exif.state', options.state === null ? 'is' : '=', options.state!),
|
|
||||||
)
|
|
||||||
.$if(options.country !== undefined, (qb) =>
|
|
||||||
qb
|
|
||||||
.innerJoin('exif', 'assets.id', 'exif.assetId')
|
|
||||||
.where('exif.country', options.country === null ? 'is' : '=', options.country!),
|
|
||||||
)
|
|
||||||
.$if(options.make !== undefined, (qb) =>
|
|
||||||
qb
|
|
||||||
.innerJoin('exif', 'assets.id', 'exif.assetId')
|
|
||||||
.where('exif.make', options.make === null ? 'is' : '=', options.make!),
|
|
||||||
)
|
|
||||||
.$if(options.model !== undefined, (qb) =>
|
|
||||||
qb
|
|
||||||
.innerJoin('exif', 'assets.id', 'exif.assetId')
|
|
||||||
.where('exif.model', options.model === null ? 'is' : '=', options.model!),
|
|
||||||
)
|
|
||||||
.$if(options.lensModel !== undefined, (qb) =>
|
|
||||||
qb
|
|
||||||
.innerJoin('exif', 'assets.id', 'exif.assetId')
|
|
||||||
.where('exif.lensModel', options.lensModel === null ? 'is' : '=', options.lensModel!),
|
|
||||||
)
|
|
||||||
.$if(options.rating !== undefined, (qb) =>
|
|
||||||
qb
|
|
||||||
.innerJoin('exif', 'assets.id', 'exif.assetId')
|
|
||||||
.where('exif.rating', options.rating === null ? 'is' : '=', options.rating!),
|
|
||||||
)
|
|
||||||
.$if(!!options.checksum, (qb) => qb.where('assets.checksum', '=', options.checksum!))
|
|
||||||
.$if(!!options.deviceAssetId, (qb) => qb.where('assets.deviceAssetId', '=', options.deviceAssetId!))
|
|
||||||
.$if(!!options.deviceId, (qb) => qb.where('assets.deviceId', '=', options.deviceId!))
|
|
||||||
.$if(!!options.id, (qb) => qb.where('assets.id', '=', asUuid(options.id!)))
|
|
||||||
.$if(!!options.libraryId, (qb) => qb.where('assets.libraryId', '=', asUuid(options.libraryId!)))
|
|
||||||
.$if(!!options.userIds, (qb) => qb.where('assets.ownerId', '=', anyUuid(options.userIds!)))
|
|
||||||
.$if(!!options.encodedVideoPath, (qb) => qb.where('assets.encodedVideoPath', '=', options.encodedVideoPath!))
|
|
||||||
.$if(!!options.originalPath, (qb) =>
|
|
||||||
qb.where(
|
|
||||||
sql`f_unaccent(assets."originalPath")`,
|
|
||||||
'ilike',
|
|
||||||
sql`'%' || f_unaccent(${options.originalPath}) || '%'`,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.$if(!!options.originalFileName, (qb) =>
|
|
||||||
qb.where(
|
|
||||||
sql`f_unaccent(assets."originalFileName")`,
|
|
||||||
'ilike',
|
|
||||||
sql`'%' || f_unaccent(${options.originalFileName}) || '%'`,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.$if(!!options.description, (qb) =>
|
|
||||||
qb
|
|
||||||
.innerJoin('exif', 'assets.id', 'exif.assetId')
|
|
||||||
.where(sql`f_unaccent(exif.description)`, 'ilike', sql`'%' || f_unaccent(${options.description}) || '%'`),
|
|
||||||
)
|
|
||||||
.$if(!!options.type, (qb) => qb.where('assets.type', '=', options.type!))
|
|
||||||
.$if(options.isFavorite !== undefined, (qb) => qb.where('assets.isFavorite', '=', options.isFavorite!))
|
|
||||||
.$if(options.isOffline !== undefined, (qb) => qb.where('assets.isOffline', '=', options.isOffline!))
|
|
||||||
.$if(options.isVisible !== undefined, (qb) => qb.where('assets.isVisible', '=', options.isVisible!))
|
|
||||||
.$if(options.isArchived !== undefined, (qb) => qb.where('assets.isArchived', '=', options.isArchived!))
|
|
||||||
.$if(options.isEncoded !== undefined, (qb) =>
|
|
||||||
qb.where('assets.encodedVideoPath', options.isEncoded ? 'is not' : 'is', null),
|
|
||||||
)
|
|
||||||
.$if(options.isMotion !== undefined, (qb) =>
|
|
||||||
qb.where('assets.livePhotoVideoId', options.isMotion ? 'is not' : 'is', null),
|
|
||||||
)
|
|
||||||
.$if(!!options.isNotInAlbum, (qb) =>
|
|
||||||
qb.where((eb) =>
|
|
||||||
eb.not(eb.exists((eb) => eb.selectFrom('albums_assets_assets').whereRef('assetsId', '=', 'assets.id'))),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.$if(!!options.withExif, withExifInner)
|
|
||||||
.$if(!!(options.withFaces || options.withPeople || options.personIds), (qb) => qb.select(withFacesAndPeople))
|
|
||||||
.$if(!options.withDeleted, (qb) => qb.where('assets.deletedAt', 'is', null))
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,20 +0,0 @@
|
|||||||
import { AlbumEntity } from 'src/entities/album.entity';
|
|
||||||
import { AssetEntity } from 'src/entities/asset.entity';
|
|
||||||
import { SharedLinkType } from 'src/enum';
|
|
||||||
|
|
||||||
export class SharedLinkEntity {
|
|
||||||
id!: string;
|
|
||||||
description!: string | null;
|
|
||||||
password!: string | null;
|
|
||||||
userId!: string;
|
|
||||||
key!: Buffer; // use to access the inidividual asset
|
|
||||||
type!: SharedLinkType;
|
|
||||||
createdAt!: Date;
|
|
||||||
expiresAt!: Date | null;
|
|
||||||
allowUpload!: boolean;
|
|
||||||
allowDownload!: boolean;
|
|
||||||
showExif!: boolean;
|
|
||||||
assets!: AssetEntity[];
|
|
||||||
album?: AlbumEntity;
|
|
||||||
albumId!: string | null;
|
|
||||||
}
|
|
51
server/src/migrations/1744900200559-AddForeignKeyIndexes.ts
Normal file
51
server/src/migrations/1744900200559-AddForeignKeyIndexes.ts
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||||
|
|
||||||
|
export class AddForeignKeyIndexes1744900200559 implements MigrationInterface {
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`CREATE INDEX "IDX_0f6fc2fb195f24d19b0fb0d57c" ON "libraries" ("ownerId")`);
|
||||||
|
await queryRunner.query(`CREATE INDEX "IDX_91704e101438fd0653f582426d" ON "asset_stack" ("primaryAssetId")`);
|
||||||
|
await queryRunner.query(`CREATE INDEX "IDX_c05079e542fd74de3b5ecb5c1c" ON "asset_stack" ("ownerId")`);
|
||||||
|
await queryRunner.query(`CREATE INDEX "IDX_2c5ac0d6fb58b238fd2068de67" ON "assets" ("ownerId")`);
|
||||||
|
await queryRunner.query(`CREATE INDEX "IDX_16294b83fa8c0149719a1f631e" ON "assets" ("livePhotoVideoId")`);
|
||||||
|
await queryRunner.query(`CREATE INDEX "IDX_9977c3c1de01c3d848039a6b90" ON "assets" ("libraryId")`);
|
||||||
|
await queryRunner.query(`CREATE INDEX "IDX_f15d48fa3ea5e4bda05ca8ab20" ON "assets" ("stackId")`);
|
||||||
|
await queryRunner.query(`CREATE INDEX "IDX_b22c53f35ef20c28c21637c85f" ON "albums" ("ownerId")`);
|
||||||
|
await queryRunner.query(`CREATE INDEX "IDX_05895aa505a670300d4816debc" ON "albums" ("albumThumbnailAssetId")`);
|
||||||
|
await queryRunner.query(`CREATE INDEX "IDX_1af8519996fbfb3684b58df280" ON "activity" ("albumId")`);
|
||||||
|
await queryRunner.query(`CREATE INDEX "IDX_3571467bcbe021f66e2bdce96e" ON "activity" ("userId")`);
|
||||||
|
await queryRunner.query(`CREATE INDEX "IDX_8091ea76b12338cb4428d33d78" ON "activity" ("assetId")`);
|
||||||
|
await queryRunner.query(`CREATE INDEX "IDX_6c2e267ae764a9413b863a2934" ON "api_keys" ("userId")`);
|
||||||
|
await queryRunner.query(`CREATE INDEX "IDX_5527cc99f530a547093f9e577b" ON "person" ("ownerId")`);
|
||||||
|
await queryRunner.query(`CREATE INDEX "IDX_2bbabe31656b6778c6b87b6102" ON "person" ("faceAssetId")`);
|
||||||
|
await queryRunner.query(`CREATE INDEX "IDX_575842846f0c28fa5da46c99b1" ON "memories" ("ownerId")`);
|
||||||
|
await queryRunner.query(`CREATE INDEX "IDX_d7e875c6c60e661723dbf372fd" ON "partners" ("sharedWithId")`);
|
||||||
|
await queryRunner.query(`CREATE INDEX "IDX_57de40bc620f456c7311aa3a1e" ON "sessions" ("userId")`);
|
||||||
|
await queryRunner.query(`CREATE INDEX "IDX_66fe3837414c5a9f1c33ca4934" ON "shared_links" ("userId")`);
|
||||||
|
await queryRunner.query(`CREATE INDEX "IDX_d8ddd9d687816cc490432b3d4b" ON "session_sync_checkpoints" ("sessionId")`);
|
||||||
|
await queryRunner.query(`CREATE INDEX "IDX_9f9590cc11561f1f48ff034ef9" ON "tags" ("parentId")`);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`DROP INDEX "IDX_66fe3837414c5a9f1c33ca4934";`);
|
||||||
|
await queryRunner.query(`DROP INDEX "IDX_91704e101438fd0653f582426d";`);
|
||||||
|
await queryRunner.query(`DROP INDEX "IDX_c05079e542fd74de3b5ecb5c1c";`);
|
||||||
|
await queryRunner.query(`DROP INDEX "IDX_5527cc99f530a547093f9e577b";`);
|
||||||
|
await queryRunner.query(`DROP INDEX "IDX_2bbabe31656b6778c6b87b6102";`);
|
||||||
|
await queryRunner.query(`DROP INDEX "IDX_0f6fc2fb195f24d19b0fb0d57c";`);
|
||||||
|
await queryRunner.query(`DROP INDEX "IDX_9f9590cc11561f1f48ff034ef9";`);
|
||||||
|
await queryRunner.query(`DROP INDEX "IDX_2c5ac0d6fb58b238fd2068de67";`);
|
||||||
|
await queryRunner.query(`DROP INDEX "IDX_16294b83fa8c0149719a1f631e";`);
|
||||||
|
await queryRunner.query(`DROP INDEX "IDX_9977c3c1de01c3d848039a6b90";`);
|
||||||
|
await queryRunner.query(`DROP INDEX "IDX_f15d48fa3ea5e4bda05ca8ab20";`);
|
||||||
|
await queryRunner.query(`DROP INDEX "IDX_b22c53f35ef20c28c21637c85f";`);
|
||||||
|
await queryRunner.query(`DROP INDEX "IDX_05895aa505a670300d4816debc";`);
|
||||||
|
await queryRunner.query(`DROP INDEX "IDX_57de40bc620f456c7311aa3a1e";`);
|
||||||
|
await queryRunner.query(`DROP INDEX "IDX_d8ddd9d687816cc490432b3d4b";`);
|
||||||
|
await queryRunner.query(`DROP INDEX "IDX_d7e875c6c60e661723dbf372fd";`);
|
||||||
|
await queryRunner.query(`DROP INDEX "IDX_575842846f0c28fa5da46c99b1";`);
|
||||||
|
await queryRunner.query(`DROP INDEX "IDX_6c2e267ae764a9413b863a2934";`);
|
||||||
|
await queryRunner.query(`DROP INDEX "IDX_1af8519996fbfb3684b58df280";`);
|
||||||
|
await queryRunner.query(`DROP INDEX "IDX_3571467bcbe021f66e2bdce96e";`);
|
||||||
|
await queryRunner.query(`DROP INDEX "IDX_8091ea76b12338cb4428d33d78";`);
|
||||||
|
}
|
||||||
|
}
|
13
server/src/migrations/1744910873956-AddMissingIndex.ts
Normal file
13
server/src/migrations/1744910873956-AddMissingIndex.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||||
|
|
||||||
|
export class AddMissingIndex1744910873956 implements MigrationInterface {
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(
|
||||||
|
`CREATE INDEX IF NOT EXISTS "IDX_geodata_gist_earthcoord" ON "geodata_places" (ll_to_earth_public(latitude, longitude))`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`DROP INDEX "IDX_geodata_gist_earthcoord";`);
|
||||||
|
}
|
||||||
|
}
|
@ -82,7 +82,7 @@ from
|
|||||||
where
|
where
|
||||||
"assets"."id" = any ($1::uuid[])
|
"assets"."id" = any ($1::uuid[])
|
||||||
|
|
||||||
-- AssetRepository.getByIdsWithAllRelations
|
-- AssetRepository.getByIdsWithAllRelationsButStacks
|
||||||
select
|
select
|
||||||
"assets".*,
|
"assets".*,
|
||||||
(
|
(
|
||||||
@ -127,28 +127,13 @@ select
|
|||||||
"assets"."id" = "tag_asset"."assetsId"
|
"assets"."id" = "tag_asset"."assetsId"
|
||||||
) as agg
|
) as agg
|
||||||
) as "tags",
|
) as "tags",
|
||||||
to_json("exif") as "exifInfo",
|
to_json("exif") as "exifInfo"
|
||||||
to_json("stacked_assets") as "stack"
|
|
||||||
from
|
from
|
||||||
"assets"
|
"assets"
|
||||||
left join "exif" on "assets"."id" = "exif"."assetId"
|
left join "exif" on "assets"."id" = "exif"."assetId"
|
||||||
left join "asset_stack" on "asset_stack"."id" = "assets"."stackId"
|
left join "asset_stack" on "asset_stack"."id" = "assets"."stackId"
|
||||||
left join lateral (
|
|
||||||
select
|
|
||||||
"asset_stack".*,
|
|
||||||
array_agg("stacked") as "assets"
|
|
||||||
from
|
|
||||||
"assets" as "stacked"
|
|
||||||
where
|
|
||||||
"stacked"."stackId" = "asset_stack"."id"
|
|
||||||
and "stacked"."id" != "asset_stack"."primaryAssetId"
|
|
||||||
and "stacked"."deletedAt" is null
|
|
||||||
and "stacked"."isArchived" = $1
|
|
||||||
group by
|
|
||||||
"asset_stack"."id"
|
|
||||||
) as "stacked_assets" on "asset_stack"."id" is not null
|
|
||||||
where
|
where
|
||||||
"assets"."id" = any ($2::uuid[])
|
"assets"."id" = any ($1::uuid[])
|
||||||
|
|
||||||
-- AssetRepository.deleteAll
|
-- AssetRepository.deleteAll
|
||||||
delete from "assets"
|
delete from "assets"
|
||||||
|
@ -1,12 +1,11 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { ExpressionBuilder, Insertable, Kysely, sql, Updateable } from 'kysely';
|
import { ExpressionBuilder, Insertable, Kysely, NotNull, sql, Updateable } from 'kysely';
|
||||||
import { jsonArrayFrom, jsonObjectFrom } from 'kysely/helpers/postgres';
|
import { jsonArrayFrom, jsonObjectFrom } from 'kysely/helpers/postgres';
|
||||||
import { InjectKysely } from 'nestjs-kysely';
|
import { InjectKysely } from 'nestjs-kysely';
|
||||||
import { columns } from 'src/database';
|
import { columns, Exif } from 'src/database';
|
||||||
import { Albums, DB } from 'src/db';
|
import { Albums, DB } from 'src/db';
|
||||||
import { Chunked, ChunkedArray, ChunkedSet, DummyValue, GenerateSql } from 'src/decorators';
|
import { Chunked, ChunkedArray, ChunkedSet, DummyValue, GenerateSql } from 'src/decorators';
|
||||||
import { AlbumUserCreateDto } from 'src/dtos/album.dto';
|
import { AlbumUserCreateDto } from 'src/dtos/album.dto';
|
||||||
import { AlbumEntity } from 'src/entities/album.entity';
|
|
||||||
|
|
||||||
export interface AlbumAssetCount {
|
export interface AlbumAssetCount {
|
||||||
albumId: string;
|
albumId: string;
|
||||||
@ -21,9 +20,9 @@ export interface AlbumInfoOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const withOwner = (eb: ExpressionBuilder<DB, 'albums'>) => {
|
const withOwner = (eb: ExpressionBuilder<DB, 'albums'>) => {
|
||||||
return jsonObjectFrom(eb.selectFrom('users').select(columns.user).whereRef('users.id', '=', 'albums.ownerId')).as(
|
return jsonObjectFrom(eb.selectFrom('users').select(columns.user).whereRef('users.id', '=', 'albums.ownerId'))
|
||||||
'owner',
|
.$notNull()
|
||||||
);
|
.as('owner');
|
||||||
};
|
};
|
||||||
|
|
||||||
const withAlbumUsers = (eb: ExpressionBuilder<DB, 'albums'>) => {
|
const withAlbumUsers = (eb: ExpressionBuilder<DB, 'albums'>) => {
|
||||||
@ -32,12 +31,14 @@ const withAlbumUsers = (eb: ExpressionBuilder<DB, 'albums'>) => {
|
|||||||
.selectFrom('albums_shared_users_users as album_users')
|
.selectFrom('albums_shared_users_users as album_users')
|
||||||
.select('album_users.role')
|
.select('album_users.role')
|
||||||
.select((eb) =>
|
.select((eb) =>
|
||||||
jsonObjectFrom(eb.selectFrom('users').select(columns.user).whereRef('users.id', '=', 'album_users.usersId')).as(
|
jsonObjectFrom(eb.selectFrom('users').select(columns.user).whereRef('users.id', '=', 'album_users.usersId'))
|
||||||
'user',
|
.$notNull()
|
||||||
),
|
.as('user'),
|
||||||
)
|
)
|
||||||
.whereRef('album_users.albumsId', '=', 'albums.id'),
|
.whereRef('album_users.albumsId', '=', 'albums.id'),
|
||||||
).as('albumUsers');
|
)
|
||||||
|
.$notNull()
|
||||||
|
.as('albumUsers');
|
||||||
};
|
};
|
||||||
|
|
||||||
const withSharedLink = (eb: ExpressionBuilder<DB, 'albums'>) => {
|
const withSharedLink = (eb: ExpressionBuilder<DB, 'albums'>) => {
|
||||||
@ -53,7 +54,7 @@ const withAssets = (eb: ExpressionBuilder<DB, 'albums'>) => {
|
|||||||
.selectFrom('assets')
|
.selectFrom('assets')
|
||||||
.selectAll('assets')
|
.selectAll('assets')
|
||||||
.leftJoin('exif', 'assets.id', 'exif.assetId')
|
.leftJoin('exif', 'assets.id', 'exif.assetId')
|
||||||
.select((eb) => eb.table('exif').as('exifInfo'))
|
.select((eb) => eb.table('exif').$castTo<Exif>().as('exifInfo'))
|
||||||
.innerJoin('albums_assets_assets', 'albums_assets_assets.assetsId', 'assets.id')
|
.innerJoin('albums_assets_assets', 'albums_assets_assets.assetsId', 'assets.id')
|
||||||
.whereRef('albums_assets_assets.albumsId', '=', 'albums.id')
|
.whereRef('albums_assets_assets.albumsId', '=', 'albums.id')
|
||||||
.where('assets.deletedAt', 'is', null)
|
.where('assets.deletedAt', 'is', null)
|
||||||
@ -69,7 +70,7 @@ export class AlbumRepository {
|
|||||||
constructor(@InjectKysely() private db: Kysely<DB>) {}
|
constructor(@InjectKysely() private db: Kysely<DB>) {}
|
||||||
|
|
||||||
@GenerateSql({ params: [DummyValue.UUID, { withAssets: true }] })
|
@GenerateSql({ params: [DummyValue.UUID, { withAssets: true }] })
|
||||||
async getById(id: string, options: AlbumInfoOptions): Promise<AlbumEntity | undefined> {
|
async getById(id: string, options: AlbumInfoOptions) {
|
||||||
return this.db
|
return this.db
|
||||||
.selectFrom('albums')
|
.selectFrom('albums')
|
||||||
.selectAll('albums')
|
.selectAll('albums')
|
||||||
@ -79,11 +80,12 @@ export class AlbumRepository {
|
|||||||
.select(withAlbumUsers)
|
.select(withAlbumUsers)
|
||||||
.select(withSharedLink)
|
.select(withSharedLink)
|
||||||
.$if(options.withAssets, (eb) => eb.select(withAssets))
|
.$if(options.withAssets, (eb) => eb.select(withAssets))
|
||||||
.executeTakeFirst() as Promise<AlbumEntity | undefined>;
|
.$narrowType<{ assets: NotNull }>()
|
||||||
|
.executeTakeFirst();
|
||||||
}
|
}
|
||||||
|
|
||||||
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID] })
|
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID] })
|
||||||
async getByAssetId(ownerId: string, assetId: string): Promise<AlbumEntity[]> {
|
async getByAssetId(ownerId: string, assetId: string) {
|
||||||
return this.db
|
return this.db
|
||||||
.selectFrom('albums')
|
.selectFrom('albums')
|
||||||
.selectAll('albums')
|
.selectAll('albums')
|
||||||
@ -105,7 +107,7 @@ export class AlbumRepository {
|
|||||||
.select(withOwner)
|
.select(withOwner)
|
||||||
.select(withAlbumUsers)
|
.select(withAlbumUsers)
|
||||||
.orderBy('albums.createdAt', 'desc')
|
.orderBy('albums.createdAt', 'desc')
|
||||||
.execute() as unknown as Promise<AlbumEntity[]>;
|
.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
@GenerateSql({ params: [[DummyValue.UUID]] })
|
@GenerateSql({ params: [[DummyValue.UUID]] })
|
||||||
@ -134,7 +136,7 @@ export class AlbumRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@GenerateSql({ params: [DummyValue.UUID] })
|
@GenerateSql({ params: [DummyValue.UUID] })
|
||||||
async getOwned(ownerId: string): Promise<AlbumEntity[]> {
|
async getOwned(ownerId: string) {
|
||||||
return this.db
|
return this.db
|
||||||
.selectFrom('albums')
|
.selectFrom('albums')
|
||||||
.selectAll('albums')
|
.selectAll('albums')
|
||||||
@ -144,14 +146,14 @@ export class AlbumRepository {
|
|||||||
.where('albums.ownerId', '=', ownerId)
|
.where('albums.ownerId', '=', ownerId)
|
||||||
.where('albums.deletedAt', 'is', null)
|
.where('albums.deletedAt', 'is', null)
|
||||||
.orderBy('albums.createdAt', 'desc')
|
.orderBy('albums.createdAt', 'desc')
|
||||||
.execute() as unknown as Promise<AlbumEntity[]>;
|
.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get albums shared with and shared by owner.
|
* Get albums shared with and shared by owner.
|
||||||
*/
|
*/
|
||||||
@GenerateSql({ params: [DummyValue.UUID] })
|
@GenerateSql({ params: [DummyValue.UUID] })
|
||||||
async getShared(ownerId: string): Promise<AlbumEntity[]> {
|
async getShared(ownerId: string) {
|
||||||
return this.db
|
return this.db
|
||||||
.selectFrom('albums')
|
.selectFrom('albums')
|
||||||
.selectAll('albums')
|
.selectAll('albums')
|
||||||
@ -176,14 +178,14 @@ export class AlbumRepository {
|
|||||||
.select(withOwner)
|
.select(withOwner)
|
||||||
.select(withSharedLink)
|
.select(withSharedLink)
|
||||||
.orderBy('albums.createdAt', 'desc')
|
.orderBy('albums.createdAt', 'desc')
|
||||||
.execute() as unknown as Promise<AlbumEntity[]>;
|
.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get albums of owner that are _not_ shared
|
* Get albums of owner that are _not_ shared
|
||||||
*/
|
*/
|
||||||
@GenerateSql({ params: [DummyValue.UUID] })
|
@GenerateSql({ params: [DummyValue.UUID] })
|
||||||
async getNotShared(ownerId: string): Promise<AlbumEntity[]> {
|
async getNotShared(ownerId: string) {
|
||||||
return this.db
|
return this.db
|
||||||
.selectFrom('albums')
|
.selectFrom('albums')
|
||||||
.selectAll('albums')
|
.selectAll('albums')
|
||||||
@ -203,7 +205,7 @@ export class AlbumRepository {
|
|||||||
)
|
)
|
||||||
.select(withOwner)
|
.select(withOwner)
|
||||||
.orderBy('albums.createdAt', 'desc')
|
.orderBy('albums.createdAt', 'desc')
|
||||||
.execute() as unknown as Promise<AlbumEntity[]>;
|
.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
async restoreAll(userId: string): Promise<void> {
|
async restoreAll(userId: string): Promise<void> {
|
||||||
@ -262,7 +264,7 @@ export class AlbumRepository {
|
|||||||
await this.addAssets(this.db, albumId, assetIds);
|
await this.addAssets(this.db, albumId, assetIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
create(album: Insertable<Albums>, assetIds: string[], albumUsers: AlbumUserCreateDto[]): Promise<AlbumEntity> {
|
create(album: Insertable<Albums>, assetIds: string[], albumUsers: AlbumUserCreateDto[]) {
|
||||||
return this.db.transaction().execute(async (tx) => {
|
return this.db.transaction().execute(async (tx) => {
|
||||||
const newAlbum = await tx.insertInto('albums').values(album).returning('albums.id').executeTakeFirst();
|
const newAlbum = await tx.insertInto('albums').values(album).returning('albums.id').executeTakeFirst();
|
||||||
|
|
||||||
@ -290,11 +292,12 @@ export class AlbumRepository {
|
|||||||
.select(withOwner)
|
.select(withOwner)
|
||||||
.select(withAssets)
|
.select(withAssets)
|
||||||
.select(withAlbumUsers)
|
.select(withAlbumUsers)
|
||||||
.executeTakeFirst() as unknown as Promise<AlbumEntity>;
|
.$narrowType<{ assets: NotNull }>()
|
||||||
|
.executeTakeFirstOrThrow();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
update(id: string, album: Updateable<Albums>): Promise<AlbumEntity> {
|
update(id: string, album: Updateable<Albums>) {
|
||||||
return this.db
|
return this.db
|
||||||
.updateTable('albums')
|
.updateTable('albums')
|
||||||
.set(album)
|
.set(album)
|
||||||
@ -303,7 +306,7 @@ export class AlbumRepository {
|
|||||||
.returning(withOwner)
|
.returning(withOwner)
|
||||||
.returning(withSharedLink)
|
.returning(withSharedLink)
|
||||||
.returning(withAlbumUsers)
|
.returning(withAlbumUsers)
|
||||||
.executeTakeFirst() as unknown as Promise<AlbumEntity>;
|
.executeTakeFirstOrThrow();
|
||||||
}
|
}
|
||||||
|
|
||||||
async delete(id: string): Promise<void> {
|
async delete(id: string): Promise<void> {
|
||||||
|
@ -5,10 +5,9 @@ import { InjectKysely } from 'nestjs-kysely';
|
|||||||
import { columns } from 'src/database';
|
import { columns } from 'src/database';
|
||||||
import { DB } from 'src/db';
|
import { DB } from 'src/db';
|
||||||
import { DummyValue, GenerateSql } from 'src/decorators';
|
import { DummyValue, GenerateSql } from 'src/decorators';
|
||||||
import { withExifInner, withFaces, withFiles } from 'src/entities/asset.entity';
|
|
||||||
import { AssetFileType } from 'src/enum';
|
import { AssetFileType } from 'src/enum';
|
||||||
import { StorageAsset } from 'src/types';
|
import { StorageAsset } from 'src/types';
|
||||||
import { asUuid } from 'src/utils/database';
|
import { anyUuid, asUuid, withExifInner, withFaces, withFiles } from 'src/utils/database';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AssetJobRepository {
|
export class AssetJobRepository {
|
||||||
@ -149,6 +148,21 @@ export class AssetJobRepository {
|
|||||||
.executeTakeFirst();
|
.executeTakeFirst();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getForSyncAssets(ids: string[]) {
|
||||||
|
return this.db
|
||||||
|
.selectFrom('assets')
|
||||||
|
.select([
|
||||||
|
'assets.id',
|
||||||
|
'assets.isOffline',
|
||||||
|
'assets.libraryId',
|
||||||
|
'assets.originalPath',
|
||||||
|
'assets.status',
|
||||||
|
'assets.fileModifiedAt',
|
||||||
|
])
|
||||||
|
.where('assets.id', '=', anyUuid(ids))
|
||||||
|
.execute();
|
||||||
|
}
|
||||||
|
|
||||||
private storageTemplateAssetQuery() {
|
private storageTemplateAssetQuery() {
|
||||||
return this.db
|
return this.db
|
||||||
.selectFrom('assets')
|
.selectFrom('assets')
|
||||||
|
@ -1,14 +1,22 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { Insertable, Kysely, Selectable, UpdateResult, Updateable, sql } from 'kysely';
|
import { Insertable, Kysely, NotNull, Selectable, UpdateResult, Updateable, sql } from 'kysely';
|
||||||
import { isEmpty, isUndefined, omitBy, round } from 'lodash';
|
import { isEmpty, isUndefined, omitBy, round } from 'lodash';
|
||||||
import { InjectKysely } from 'nestjs-kysely';
|
import { InjectKysely } from 'nestjs-kysely';
|
||||||
|
import { Stack } from 'src/database';
|
||||||
import { AssetFiles, AssetJobStatus, Assets, DB, Exif } from 'src/db';
|
import { AssetFiles, AssetJobStatus, Assets, DB, Exif } from 'src/db';
|
||||||
import { Chunked, ChunkedArray, DummyValue, GenerateSql } from 'src/decorators';
|
import { Chunked, ChunkedArray, DummyValue, GenerateSql } from 'src/decorators';
|
||||||
|
import { MapAsset } from 'src/dtos/asset-response.dto';
|
||||||
|
import { AssetFileType, AssetOrder, AssetStatus, AssetType } from 'src/enum';
|
||||||
|
import { AssetSearchOptions, SearchExploreItem, SearchExploreItemSet } from 'src/repositories/search.repository';
|
||||||
import {
|
import {
|
||||||
AssetEntity,
|
anyUuid,
|
||||||
|
asUuid,
|
||||||
hasPeople,
|
hasPeople,
|
||||||
|
hasPeopleNoJoin,
|
||||||
|
removeUndefinedKeys,
|
||||||
searchAssetBuilder,
|
searchAssetBuilder,
|
||||||
truncatedDate,
|
truncatedDate,
|
||||||
|
unnest,
|
||||||
withExif,
|
withExif,
|
||||||
withFaces,
|
withFaces,
|
||||||
withFacesAndPeople,
|
withFacesAndPeople,
|
||||||
@ -17,13 +25,11 @@ import {
|
|||||||
withOwner,
|
withOwner,
|
||||||
withSmartSearch,
|
withSmartSearch,
|
||||||
withTagId,
|
withTagId,
|
||||||
|
withTagIdNoWhere,
|
||||||
withTags,
|
withTags,
|
||||||
} from 'src/entities/asset.entity';
|
} from 'src/utils/database';
|
||||||
import { AssetFileType, AssetOrder, AssetStatus, AssetType } from 'src/enum';
|
|
||||||
import { AssetSearchOptions, SearchExploreItem, SearchExploreItemSet } from 'src/repositories/search.repository';
|
|
||||||
import { anyUuid, asUuid, removeUndefinedKeys, unnest } from 'src/utils/database';
|
|
||||||
import { globToSqlPattern } from 'src/utils/misc';
|
import { globToSqlPattern } from 'src/utils/misc';
|
||||||
import { Paginated, PaginationOptions, paginationHelper } from 'src/utils/pagination';
|
import { PaginationOptions, paginationHelper } from 'src/utils/pagination';
|
||||||
|
|
||||||
import { TimeBucketAssets } from 'src/services/timeline.service.types';
|
import { TimeBucketAssets } from 'src/services/timeline.service.types';
|
||||||
import { isFlipped } from 'src/utils/asset.util';
|
import { isFlipped } from 'src/utils/asset.util';
|
||||||
@ -130,8 +136,6 @@ export interface AssetGetByChecksumOptions {
|
|||||||
libraryId?: string;
|
libraryId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AssetPathEntity = Pick<AssetEntity, 'id' | 'originalPath' | 'isOffline'>;
|
|
||||||
|
|
||||||
export interface GetByIdsRelations {
|
export interface GetByIdsRelations {
|
||||||
exifInfo?: boolean;
|
exifInfo?: boolean;
|
||||||
faces?: { person?: boolean; withDeleted?: boolean };
|
faces?: { person?: boolean; withDeleted?: boolean };
|
||||||
@ -145,12 +149,12 @@ export interface GetByIdsRelations {
|
|||||||
|
|
||||||
export interface DuplicateGroup {
|
export interface DuplicateGroup {
|
||||||
duplicateId: string;
|
duplicateId: string;
|
||||||
assets: AssetEntity[];
|
assets: MapAsset[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DayOfYearAssets {
|
export interface DayOfYearAssets {
|
||||||
yearsAgo: number;
|
yearsAgo: number;
|
||||||
assets: AssetEntity[];
|
assets: MapAsset[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@ -238,12 +242,12 @@ export class AssetRepository {
|
|||||||
.execute();
|
.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
create(asset: Insertable<Assets>): Promise<AssetEntity> {
|
create(asset: Insertable<Assets>) {
|
||||||
return this.db.insertInto('assets').values(asset).returningAll().executeTakeFirst() as any as Promise<AssetEntity>;
|
return this.db.insertInto('assets').values(asset).returningAll().executeTakeFirstOrThrow();
|
||||||
}
|
}
|
||||||
|
|
||||||
createAll(assets: Insertable<Assets>[]): Promise<AssetEntity[]> {
|
createAll(assets: Insertable<Assets>[]) {
|
||||||
return this.db.insertInto('assets').values(assets).returningAll().execute() as any as Promise<AssetEntity[]>;
|
return this.db.insertInto('assets').values(assets).returningAll().execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
@GenerateSql({ params: [DummyValue.UUID, { day: 1, month: 1 }] })
|
@GenerateSql({ params: [DummyValue.UUID, { day: 1, month: 1 }] })
|
||||||
@ -303,56 +307,13 @@ export class AssetRepository {
|
|||||||
|
|
||||||
@GenerateSql({ params: [[DummyValue.UUID]] })
|
@GenerateSql({ params: [[DummyValue.UUID]] })
|
||||||
@ChunkedArray()
|
@ChunkedArray()
|
||||||
async getByIds(
|
getByIds(ids: string[]) {
|
||||||
ids: string[],
|
return this.db.selectFrom('assets').selectAll('assets').where('assets.id', '=', anyUuid(ids)).execute();
|
||||||
{ exifInfo, faces, files, library, owner, smartSearch, stack, tags }: GetByIdsRelations = {},
|
|
||||||
): Promise<AssetEntity[]> {
|
|
||||||
const res = await this.db
|
|
||||||
.selectFrom('assets')
|
|
||||||
.selectAll('assets')
|
|
||||||
.where('assets.id', '=', anyUuid(ids))
|
|
||||||
.$if(!!exifInfo, withExif)
|
|
||||||
.$if(!!faces, (qb) =>
|
|
||||||
qb.select((eb) =>
|
|
||||||
faces?.person ? withFacesAndPeople(eb, faces.withDeleted) : withFaces(eb, faces?.withDeleted),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.$if(!!files, (qb) => qb.select(withFiles))
|
|
||||||
.$if(!!library, (qb) => qb.select(withLibrary))
|
|
||||||
.$if(!!owner, (qb) => qb.select(withOwner))
|
|
||||||
.$if(!!smartSearch, withSmartSearch)
|
|
||||||
.$if(!!stack, (qb) =>
|
|
||||||
qb
|
|
||||||
.leftJoin('asset_stack', 'asset_stack.id', 'assets.stackId')
|
|
||||||
.$if(!stack!.assets, (qb) => qb.select((eb) => eb.fn.toJson(eb.table('asset_stack')).as('stack')))
|
|
||||||
.$if(!!stack!.assets, (qb) =>
|
|
||||||
qb
|
|
||||||
.leftJoinLateral(
|
|
||||||
(eb) =>
|
|
||||||
eb
|
|
||||||
.selectFrom('assets as stacked')
|
|
||||||
.selectAll('asset_stack')
|
|
||||||
.select((eb) => eb.fn('array_agg', [eb.table('stacked')]).as('assets'))
|
|
||||||
.whereRef('stacked.stackId', '=', 'asset_stack.id')
|
|
||||||
.whereRef('stacked.id', '!=', 'asset_stack.primaryAssetId')
|
|
||||||
.where('stacked.deletedAt', 'is', null)
|
|
||||||
.where('stacked.isArchived', '=', false)
|
|
||||||
.groupBy('asset_stack.id')
|
|
||||||
.as('stacked_assets'),
|
|
||||||
(join) => join.on('asset_stack.id', 'is not', null),
|
|
||||||
)
|
|
||||||
.select((eb) => eb.fn.toJson(eb.table('stacked_assets')).as('stack')),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.$if(!!tags, (qb) => qb.select(withTags))
|
|
||||||
.execute();
|
|
||||||
|
|
||||||
return res as any as AssetEntity[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@GenerateSql({ params: [[DummyValue.UUID]] })
|
@GenerateSql({ params: [[DummyValue.UUID]] })
|
||||||
@ChunkedArray()
|
@ChunkedArray()
|
||||||
getByIdsWithAllRelations(ids: string[]): Promise<AssetEntity[]> {
|
getByIdsWithAllRelationsButStacks(ids: string[]) {
|
||||||
return this.db
|
return this.db
|
||||||
.selectFrom('assets')
|
.selectFrom('assets')
|
||||||
.selectAll('assets')
|
.selectAll('assets')
|
||||||
@ -360,23 +321,8 @@ export class AssetRepository {
|
|||||||
.select(withTags)
|
.select(withTags)
|
||||||
.$call(withExif)
|
.$call(withExif)
|
||||||
.leftJoin('asset_stack', 'asset_stack.id', 'assets.stackId')
|
.leftJoin('asset_stack', 'asset_stack.id', 'assets.stackId')
|
||||||
.leftJoinLateral(
|
|
||||||
(eb) =>
|
|
||||||
eb
|
|
||||||
.selectFrom('assets as stacked')
|
|
||||||
.selectAll('asset_stack')
|
|
||||||
.select((eb) => eb.fn('array_agg', [eb.table('stacked')]).as('assets'))
|
|
||||||
.whereRef('stacked.stackId', '=', 'asset_stack.id')
|
|
||||||
.whereRef('stacked.id', '!=', 'asset_stack.primaryAssetId')
|
|
||||||
.where('stacked.deletedAt', 'is', null)
|
|
||||||
.where('stacked.isArchived', '=', false)
|
|
||||||
.groupBy('asset_stack.id')
|
|
||||||
.as('stacked_assets'),
|
|
||||||
(join) => join.on('asset_stack.id', 'is not', null),
|
|
||||||
)
|
|
||||||
.select((eb) => eb.fn.toJson(eb.table('stacked_assets')).as('stack'))
|
|
||||||
.where('assets.id', '=', anyUuid(ids))
|
.where('assets.id', '=', anyUuid(ids))
|
||||||
.execute() as any as Promise<AssetEntity[]>;
|
.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
@GenerateSql({ params: [DummyValue.UUID] })
|
@GenerateSql({ params: [DummyValue.UUID] })
|
||||||
@ -396,36 +342,29 @@ export class AssetRepository {
|
|||||||
return assets.map((asset) => asset.deviceAssetId);
|
return assets.map((asset) => asset.deviceAssetId);
|
||||||
}
|
}
|
||||||
|
|
||||||
getByUserId(
|
getByUserId(pagination: PaginationOptions, userId: string, options: Omit<AssetSearchOptions, 'userIds'> = {}) {
|
||||||
pagination: PaginationOptions,
|
|
||||||
userId: string,
|
|
||||||
options: Omit<AssetSearchOptions, 'userIds'> = {},
|
|
||||||
): Paginated<AssetEntity> {
|
|
||||||
return this.getAll(pagination, { ...options, userIds: [userId] });
|
return this.getAll(pagination, { ...options, userIds: [userId] });
|
||||||
}
|
}
|
||||||
|
|
||||||
@GenerateSql({ params: [DummyValue.UUID, DummyValue.STRING] })
|
@GenerateSql({ params: [DummyValue.UUID, DummyValue.STRING] })
|
||||||
getByLibraryIdAndOriginalPath(libraryId: string, originalPath: string): Promise<AssetEntity | undefined> {
|
getByLibraryIdAndOriginalPath(libraryId: string, originalPath: string) {
|
||||||
return this.db
|
return this.db
|
||||||
.selectFrom('assets')
|
.selectFrom('assets')
|
||||||
.selectAll('assets')
|
.selectAll('assets')
|
||||||
.where('libraryId', '=', asUuid(libraryId))
|
.where('libraryId', '=', asUuid(libraryId))
|
||||||
.where('originalPath', '=', originalPath)
|
.where('originalPath', '=', originalPath)
|
||||||
.limit(1)
|
.limit(1)
|
||||||
.executeTakeFirst() as any as Promise<AssetEntity | undefined>;
|
.executeTakeFirst();
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAll(
|
async getAll(pagination: PaginationOptions, { orderDirection, ...options }: AssetSearchOptions = {}) {
|
||||||
pagination: PaginationOptions,
|
|
||||||
{ orderDirection, ...options }: AssetSearchOptions = {},
|
|
||||||
): Paginated<AssetEntity> {
|
|
||||||
const builder = searchAssetBuilder(this.db, options)
|
const builder = searchAssetBuilder(this.db, options)
|
||||||
.select(withFiles)
|
.select(withFiles)
|
||||||
.orderBy('assets.createdAt', orderDirection ?? 'asc')
|
.orderBy('assets.createdAt', orderDirection ?? 'asc')
|
||||||
.limit(pagination.take + 1)
|
.limit(pagination.take + 1)
|
||||||
.offset(pagination.skip ?? 0);
|
.offset(pagination.skip ?? 0);
|
||||||
const items = await builder.execute();
|
const items = await builder.execute();
|
||||||
return paginationHelper(items as any as AssetEntity[], pagination.take);
|
return paginationHelper(items, pagination.take);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -460,23 +399,22 @@ export class AssetRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@GenerateSql({ params: [DummyValue.UUID] })
|
@GenerateSql({ params: [DummyValue.UUID] })
|
||||||
getById(
|
getById(id: string, { exifInfo, faces, files, library, owner, smartSearch, stack, tags }: GetByIdsRelations = {}) {
|
||||||
id: string,
|
|
||||||
{ exifInfo, faces, files, library, owner, smartSearch, stack, tags }: GetByIdsRelations = {},
|
|
||||||
): Promise<AssetEntity | undefined> {
|
|
||||||
return this.db
|
return this.db
|
||||||
.selectFrom('assets')
|
.selectFrom('assets')
|
||||||
.selectAll('assets')
|
.selectAll('assets')
|
||||||
.where('assets.id', '=', asUuid(id))
|
.where('assets.id', '=', asUuid(id))
|
||||||
.$if(!!exifInfo, withExif)
|
.$if(!!exifInfo, withExif)
|
||||||
.$if(!!faces, (qb) => qb.select(faces?.person ? withFacesAndPeople : withFaces))
|
.$if(!!faces, (qb) => qb.select(faces?.person ? withFacesAndPeople : withFaces).$narrowType<{ faces: NotNull }>())
|
||||||
.$if(!!library, (qb) => qb.select(withLibrary))
|
.$if(!!library, (qb) => qb.select(withLibrary))
|
||||||
.$if(!!owner, (qb) => qb.select(withOwner))
|
.$if(!!owner, (qb) => qb.select(withOwner))
|
||||||
.$if(!!smartSearch, withSmartSearch)
|
.$if(!!smartSearch, withSmartSearch)
|
||||||
.$if(!!stack, (qb) =>
|
.$if(!!stack, (qb) =>
|
||||||
qb
|
qb
|
||||||
.leftJoin('asset_stack', 'asset_stack.id', 'assets.stackId')
|
.leftJoin('asset_stack', 'asset_stack.id', 'assets.stackId')
|
||||||
.$if(!stack!.assets, (qb) => qb.select((eb) => eb.fn.toJson(eb.table('asset_stack')).as('stack')))
|
.$if(!stack!.assets, (qb) =>
|
||||||
|
qb.select((eb) => eb.fn.toJson(eb.table('asset_stack')).$castTo<Stack | null>().as('stack')),
|
||||||
|
)
|
||||||
.$if(!!stack!.assets, (qb) =>
|
.$if(!!stack!.assets, (qb) =>
|
||||||
qb
|
qb
|
||||||
.leftJoinLateral(
|
.leftJoinLateral(
|
||||||
@ -493,13 +431,13 @@ export class AssetRepository {
|
|||||||
.as('stacked_assets'),
|
.as('stacked_assets'),
|
||||||
(join) => join.on('asset_stack.id', 'is not', null),
|
(join) => join.on('asset_stack.id', 'is not', null),
|
||||||
)
|
)
|
||||||
.select((eb) => eb.fn.toJson(eb.table('stacked_assets')).as('stack')),
|
.select((eb) => eb.fn.toJson(eb.table('stacked_assets')).$castTo<Stack | null>().as('stack')),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.$if(!!files, (qb) => qb.select(withFiles))
|
.$if(!!files, (qb) => qb.select(withFiles))
|
||||||
.$if(!!tags, (qb) => qb.select(withTags))
|
.$if(!!tags, (qb) => qb.select(withTags))
|
||||||
.limit(1)
|
.limit(1)
|
||||||
.executeTakeFirst() as any as Promise<AssetEntity | undefined>;
|
.executeTakeFirst();
|
||||||
}
|
}
|
||||||
|
|
||||||
@GenerateSql({ params: [[DummyValue.UUID], { deviceId: DummyValue.STRING }] })
|
@GenerateSql({ params: [[DummyValue.UUID], { deviceId: DummyValue.STRING }] })
|
||||||
@ -528,7 +466,7 @@ export class AssetRepository {
|
|||||||
.execute();
|
.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
async update(asset: Updateable<Assets> & { id: string }): Promise<AssetEntity> {
|
async update(asset: Updateable<Assets> & { id: string }) {
|
||||||
const value = omitBy(asset, isUndefined);
|
const value = omitBy(asset, isUndefined);
|
||||||
delete value.id;
|
delete value.id;
|
||||||
if (!isEmpty(value)) {
|
if (!isEmpty(value)) {
|
||||||
@ -538,10 +476,10 @@ export class AssetRepository {
|
|||||||
.selectAll('assets')
|
.selectAll('assets')
|
||||||
.$call(withExif)
|
.$call(withExif)
|
||||||
.$call((qb) => qb.select(withFacesAndPeople))
|
.$call((qb) => qb.select(withFacesAndPeople))
|
||||||
.executeTakeFirst() as Promise<AssetEntity>;
|
.executeTakeFirst();
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.getById(asset.id, { exifInfo: true, faces: { person: true } }) as Promise<AssetEntity>;
|
return this.getById(asset.id, { exifInfo: true, faces: { person: true } });
|
||||||
}
|
}
|
||||||
|
|
||||||
async remove(asset: { id: string }): Promise<void> {
|
async remove(asset: { id: string }): Promise<void> {
|
||||||
@ -549,7 +487,7 @@ export class AssetRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@GenerateSql({ params: [{ ownerId: DummyValue.UUID, libraryId: DummyValue.UUID, checksum: DummyValue.BUFFER }] })
|
@GenerateSql({ params: [{ ownerId: DummyValue.UUID, libraryId: DummyValue.UUID, checksum: DummyValue.BUFFER }] })
|
||||||
getByChecksum({ ownerId, libraryId, checksum }: AssetGetByChecksumOptions): Promise<AssetEntity | undefined> {
|
getByChecksum({ ownerId, libraryId, checksum }: AssetGetByChecksumOptions) {
|
||||||
return this.db
|
return this.db
|
||||||
.selectFrom('assets')
|
.selectFrom('assets')
|
||||||
.selectAll('assets')
|
.selectAll('assets')
|
||||||
@ -557,17 +495,17 @@ export class AssetRepository {
|
|||||||
.where('checksum', '=', checksum)
|
.where('checksum', '=', checksum)
|
||||||
.$call((qb) => (libraryId ? qb.where('libraryId', '=', asUuid(libraryId)) : qb.where('libraryId', 'is', null)))
|
.$call((qb) => (libraryId ? qb.where('libraryId', '=', asUuid(libraryId)) : qb.where('libraryId', 'is', null)))
|
||||||
.limit(1)
|
.limit(1)
|
||||||
.executeTakeFirst() as Promise<AssetEntity | undefined>;
|
.executeTakeFirst();
|
||||||
}
|
}
|
||||||
|
|
||||||
@GenerateSql({ params: [DummyValue.UUID, [DummyValue.BUFFER]] })
|
@GenerateSql({ params: [DummyValue.UUID, [DummyValue.BUFFER]] })
|
||||||
getByChecksums(userId: string, checksums: Buffer[]): Promise<AssetEntity[]> {
|
getByChecksums(userId: string, checksums: Buffer[]) {
|
||||||
return this.db
|
return this.db
|
||||||
.selectFrom('assets')
|
.selectFrom('assets')
|
||||||
.select(['id', 'checksum', 'deletedAt'])
|
.select(['id', 'checksum', 'deletedAt'])
|
||||||
.where('ownerId', '=', asUuid(userId))
|
.where('ownerId', '=', asUuid(userId))
|
||||||
.where('checksum', 'in', checksums)
|
.where('checksum', 'in', checksums)
|
||||||
.execute() as any as Promise<AssetEntity[]>;
|
.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
@GenerateSql({ params: [DummyValue.UUID, DummyValue.BUFFER] })
|
@GenerateSql({ params: [DummyValue.UUID, DummyValue.BUFFER] })
|
||||||
@ -584,7 +522,7 @@ export class AssetRepository {
|
|||||||
return asset?.id;
|
return asset?.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
findLivePhotoMatch(options: LivePhotoSearchOptions): Promise<AssetEntity | undefined> {
|
findLivePhotoMatch(options: LivePhotoSearchOptions) {
|
||||||
const { ownerId, otherAssetId, livePhotoCID, type } = options;
|
const { ownerId, otherAssetId, livePhotoCID, type } = options;
|
||||||
return this.db
|
return this.db
|
||||||
.selectFrom('assets')
|
.selectFrom('assets')
|
||||||
@ -595,7 +533,7 @@ export class AssetRepository {
|
|||||||
.where('type', '=', type)
|
.where('type', '=', type)
|
||||||
.where('exif.livePhotoCID', '=', livePhotoCID)
|
.where('exif.livePhotoCID', '=', livePhotoCID)
|
||||||
.limit(1)
|
.limit(1)
|
||||||
.executeTakeFirst() as Promise<AssetEntity | undefined>;
|
.executeTakeFirst();
|
||||||
}
|
}
|
||||||
|
|
||||||
@GenerateSql(
|
@GenerateSql(
|
||||||
@ -604,7 +542,7 @@ export class AssetRepository {
|
|||||||
params: [DummyValue.PAGINATION, property],
|
params: [DummyValue.PAGINATION, property],
|
||||||
})),
|
})),
|
||||||
)
|
)
|
||||||
async getWithout(pagination: PaginationOptions, property: WithoutProperty): Paginated<AssetEntity> {
|
async getWithout(pagination: PaginationOptions, property: WithoutProperty) {
|
||||||
const items = await this.db
|
const items = await this.db
|
||||||
.selectFrom('assets')
|
.selectFrom('assets')
|
||||||
.selectAll('assets')
|
.selectAll('assets')
|
||||||
@ -666,7 +604,7 @@ export class AssetRepository {
|
|||||||
.orderBy('createdAt')
|
.orderBy('createdAt')
|
||||||
.execute();
|
.execute();
|
||||||
|
|
||||||
return paginationHelper(items as any as AssetEntity[], pagination.take);
|
return paginationHelper(items, pagination.take);
|
||||||
}
|
}
|
||||||
|
|
||||||
getStatistics(ownerId: string, { isArchived, isFavorite, isTrashed }: AssetStatsOptions): Promise<AssetStats> {
|
getStatistics(ownerId: string, { isArchived, isFavorite, isTrashed }: AssetStatsOptions): Promise<AssetStats> {
|
||||||
@ -685,7 +623,7 @@ export class AssetRepository {
|
|||||||
.executeTakeFirstOrThrow();
|
.executeTakeFirstOrThrow();
|
||||||
}
|
}
|
||||||
|
|
||||||
getRandom(userIds: string[], take: number): Promise<AssetEntity[]> {
|
getRandom(userIds: string[], take: number) {
|
||||||
return this.db
|
return this.db
|
||||||
.selectFrom('assets')
|
.selectFrom('assets')
|
||||||
.selectAll('assets')
|
.selectAll('assets')
|
||||||
@ -695,50 +633,54 @@ export class AssetRepository {
|
|||||||
.where('deletedAt', 'is', null)
|
.where('deletedAt', 'is', null)
|
||||||
.orderBy((eb) => eb.fn('random'))
|
.orderBy((eb) => eb.fn('random'))
|
||||||
.limit(take)
|
.limit(take)
|
||||||
.execute() as any as Promise<AssetEntity[]>;
|
.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
@GenerateSql({ params: [{ size: TimeBucketSize.MONTH }] })
|
@GenerateSql({ params: [{ size: TimeBucketSize.MONTH }] })
|
||||||
async getTimeBuckets(options: TimeBucketOptions): Promise<TimeBucketItem[]> {
|
async getTimeBuckets(options: TimeBucketOptions): Promise<TimeBucketItem[]> {
|
||||||
return (
|
return (
|
||||||
this.db
|
this.db
|
||||||
.with('assets', (qb) =>
|
.with(
|
||||||
qb
|
'assets',
|
||||||
.selectFrom('assets')
|
(qb) =>
|
||||||
.select(truncatedDate<Date>(options.size).as('timeBucket'))
|
qb
|
||||||
.$if(!!options.isTrashed, (qb) => qb.where('assets.status', '!=', AssetStatus.DELETED))
|
.selectFrom('assets')
|
||||||
.where('assets.deletedAt', options.isTrashed ? 'is not' : 'is', null)
|
.select(truncatedDate<Date>(options.size).as('timeBucket'))
|
||||||
.where('assets.isVisible', '=', true)
|
.$if(!!options.isTrashed, (qb) => qb.where('assets.status', '!=', AssetStatus.DELETED))
|
||||||
.$if(!!options.albumId, (qb) =>
|
.where('assets.deletedAt', options.isTrashed ? 'is not' : 'is', null)
|
||||||
qb
|
.where('assets.isVisible', '=', true)
|
||||||
.innerJoin('albums_assets_assets', 'assets.id', 'albums_assets_assets.assetsId')
|
.$if(!!options.albumId, (qb) =>
|
||||||
.where('albums_assets_assets.albumsId', '=', asUuid(options.albumId!)),
|
qb
|
||||||
)
|
.innerJoin('albums_assets_assets', 'assets.id', 'albums_assets_assets.assetsId')
|
||||||
// .$if(!!options.personId, (qb) => hasPeople(qb, [options.personId!]))
|
.where('albums_assets_assets.albumsId', '=', asUuid(options.albumId!)),
|
||||||
.$if(!!options.personId, (qb) =>
|
)
|
||||||
qb.innerJoin(
|
.$if(!!options.personId, (qb) => hasPeople(qb, [options.personId!]))
|
||||||
() => hasPeople([options.personId!]),
|
// .$if(!!options.personId, (qb) =>
|
||||||
(join) => join.onRef('has_people.assetId', '=', 'assets.id'),
|
// qb.innerJoin(
|
||||||
),
|
// () => hasPeople([options.personId!]),
|
||||||
)
|
// (join) => join.onRef('has_people.assetId', '=', 'assets.id'),
|
||||||
.$if(!!options.withStacked, (qb) =>
|
// ),
|
||||||
qb
|
// )
|
||||||
.leftJoin('asset_stack', (join) =>
|
.$if(!!options.withStacked, (qb) =>
|
||||||
join
|
qb
|
||||||
.onRef('asset_stack.id', '=', 'assets.stackId')
|
.leftJoin('asset_stack', (join) =>
|
||||||
.onRef('asset_stack.primaryAssetId', '=', 'assets.id'),
|
join
|
||||||
)
|
.onRef('asset_stack.id', '=', 'assets.stackId')
|
||||||
.where((eb) => eb.or([eb('assets.stackId', 'is', null), eb(eb.table('asset_stack'), 'is not', null)])),
|
.onRef('asset_stack.primaryAssetId', '=', 'assets.id'),
|
||||||
)
|
)
|
||||||
.$if(!!options.userIds, (qb) => qb.where('assets.ownerId', '=', anyUuid(options.userIds!)))
|
.where((eb) =>
|
||||||
.$if(options.isArchived !== undefined, (qb) => qb.where('assets.isArchived', '=', options.isArchived!))
|
eb.or([eb('assets.stackId', 'is', null), eb(eb.table('asset_stack'), 'is not', null)]),
|
||||||
.$if(options.isFavorite !== undefined, (qb) => qb.where('assets.isFavorite', '=', options.isFavorite!))
|
),
|
||||||
.$if(!!options.assetType, (qb) => qb.where('assets.type', '=', options.assetType!))
|
)
|
||||||
.$if(options.isDuplicate !== undefined, (qb) =>
|
.$if(!!options.userIds, (qb) => qb.where('assets.ownerId', '=', anyUuid(options.userIds!)))
|
||||||
qb.where('assets.duplicateId', options.isDuplicate ? 'is not' : 'is', null),
|
.$if(options.isArchived !== undefined, (qb) => qb.where('assets.isArchived', '=', options.isArchived!))
|
||||||
)
|
.$if(options.isFavorite !== undefined, (qb) => qb.where('assets.isFavorite', '=', options.isFavorite!))
|
||||||
// .$if(!!options.tagId, (qb) => withTagId(qb, options.tagId!)),
|
.$if(!!options.assetType, (qb) => qb.where('assets.type', '=', options.assetType!))
|
||||||
.$if(!!options.tagId, (qb) => qb.where((eb) => withTagId(options.tagId!, eb.ref('assets.id')))),
|
.$if(options.isDuplicate !== undefined, (qb) =>
|
||||||
|
qb.where('assets.duplicateId', options.isDuplicate ? 'is not' : 'is', null),
|
||||||
|
)
|
||||||
|
.$if(!!options.tagId, (qb) => withTagId(qb, options.tagId!)),
|
||||||
|
// .$if(!!options.tagId, (qb) => qb.where((eb) => withTagId(options.tagId!, eb.ref('assets.id')))),
|
||||||
)
|
)
|
||||||
.selectFrom('assets')
|
.selectFrom('assets')
|
||||||
.select('timeBucket')
|
.select('timeBucket')
|
||||||
@ -782,7 +724,7 @@ export class AssetRepository {
|
|||||||
// .$if(!!options.personId, (qb) => hasPeople(qb, [options.personId!]))
|
// .$if(!!options.personId, (qb) => hasPeople(qb, [options.personId!]))
|
||||||
.$if(!!options.personId, (qb) =>
|
.$if(!!options.personId, (qb) =>
|
||||||
qb.innerJoin(
|
qb.innerJoin(
|
||||||
() => hasPeople([options.personId!]),
|
() => hasPeopleNoJoin([options.personId!]),
|
||||||
(join) => join.onRef('has_people.assetId', '=', 'assets.id'),
|
(join) => join.onRef('has_people.assetId', '=', 'assets.id'),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@ -808,14 +750,14 @@ export class AssetRepository {
|
|||||||
.as('stacked_assets'),
|
.as('stacked_assets'),
|
||||||
(join) => join.on('asset_stack.id', 'is not', null),
|
(join) => join.on('asset_stack.id', 'is not', null),
|
||||||
)
|
)
|
||||||
.select((eb) => eb.fn.toJson(eb.table('stacked_assets')).as('stack')),
|
.select((eb) => eb.fn.toJson(eb.table('stacked_assets').$castTo<Stack | null>()).as('stack')),
|
||||||
)
|
)
|
||||||
.$if(!!options.assetType, (qb) => qb.where('assets.type', '=', options.assetType!))
|
.$if(!!options.assetType, (qb) => qb.where('assets.type', '=', options.assetType!))
|
||||||
.$if(options.isDuplicate !== undefined, (qb) =>
|
.$if(options.isDuplicate !== undefined, (qb) =>
|
||||||
qb.where('assets.duplicateId', options.isDuplicate ? 'is not' : 'is', null),
|
qb.where('assets.duplicateId', options.isDuplicate ? 'is not' : 'is', null),
|
||||||
)
|
)
|
||||||
.$if(!!options.isTrashed, (qb) => qb.where('assets.status', '!=', AssetStatus.DELETED))
|
.$if(!!options.isTrashed, (qb) => qb.where('assets.status', '!=', AssetStatus.DELETED))
|
||||||
.$if(!!options.tagId, (qb) => qb.where((eb) => withTagId(options.tagId!, eb.ref('assets.id'))))
|
.$if(!!options.tagId, (qb) => qb.where((eb) => withTagIdNoWhere(options.tagId!, eb.ref('assets.id'))))
|
||||||
.where('assets.deletedAt', options.isTrashed ? 'is not' : 'is', null)
|
.where('assets.deletedAt', options.isTrashed ? 'is not' : 'is', null)
|
||||||
.where('assets.isVisible', '=', true)
|
.where('assets.isVisible', '=', true)
|
||||||
.where(truncatedDate(options.size), '=', timeBucket.replace(/^[+-]/, ''))
|
.where(truncatedDate(options.size), '=', timeBucket.replace(/^[+-]/, ''))
|
||||||
@ -877,7 +819,7 @@ export class AssetRepository {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
mapStack(entity?: { id: string | null; primaryAssetId: string | null; assetCount: string | number | bigint | null }) {
|
mapStack(entity?: Stack | null) {
|
||||||
if (!entity) {
|
if (!entity) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -890,7 +832,7 @@ export class AssetRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@GenerateSql({ params: [DummyValue.UUID] })
|
@GenerateSql({ params: [DummyValue.UUID] })
|
||||||
getDuplicates(userId: string): Promise<DuplicateGroup[]> {
|
getDuplicates(userId: string) {
|
||||||
return (
|
return (
|
||||||
this.db
|
this.db
|
||||||
.with('duplicates', (qb) =>
|
.with('duplicates', (qb) =>
|
||||||
@ -907,9 +849,15 @@ export class AssetRepository {
|
|||||||
(join) => join.onTrue(),
|
(join) => join.onTrue(),
|
||||||
)
|
)
|
||||||
.select('assets.duplicateId')
|
.select('assets.duplicateId')
|
||||||
.select((eb) => eb.fn('jsonb_agg', [eb.table('asset')]).as('assets'))
|
.select((eb) =>
|
||||||
|
eb
|
||||||
|
.fn('jsonb_agg', [eb.table('asset')])
|
||||||
|
.$castTo<MapAsset[]>()
|
||||||
|
.as('assets'),
|
||||||
|
)
|
||||||
.where('assets.ownerId', '=', asUuid(userId))
|
.where('assets.ownerId', '=', asUuid(userId))
|
||||||
.where('assets.duplicateId', 'is not', null)
|
.where('assets.duplicateId', 'is not', null)
|
||||||
|
.$narrowType<{ duplicateId: NotNull }>()
|
||||||
.where('assets.deletedAt', 'is', null)
|
.where('assets.deletedAt', 'is', null)
|
||||||
.where('assets.isVisible', '=', true)
|
.where('assets.isVisible', '=', true)
|
||||||
.where('assets.stackId', 'is', null)
|
.where('assets.stackId', 'is', null)
|
||||||
@ -934,7 +882,7 @@ export class AssetRepository {
|
|||||||
.where(({ not, exists }) =>
|
.where(({ not, exists }) =>
|
||||||
not(exists((eb) => eb.selectFrom('unique').whereRef('unique.duplicateId', '=', 'duplicates.duplicateId'))),
|
not(exists((eb) => eb.selectFrom('unique').whereRef('unique.duplicateId', '=', 'duplicates.duplicateId'))),
|
||||||
)
|
)
|
||||||
.execute() as any as Promise<DuplicateGroup[]>
|
.execute()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -978,7 +926,7 @@ export class AssetRepository {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
getAllForUserFullSync(options: AssetFullSyncOptions): Promise<AssetEntity[]> {
|
getAllForUserFullSync(options: AssetFullSyncOptions) {
|
||||||
const { ownerId, lastId, updatedUntil, limit } = options;
|
const { ownerId, lastId, updatedUntil, limit } = options;
|
||||||
return this.db
|
return this.db
|
||||||
.selectFrom('assets')
|
.selectFrom('assets')
|
||||||
@ -996,18 +944,18 @@ export class AssetRepository {
|
|||||||
.as('stacked_assets'),
|
.as('stacked_assets'),
|
||||||
(join) => join.on('asset_stack.id', 'is not', null),
|
(join) => join.on('asset_stack.id', 'is not', null),
|
||||||
)
|
)
|
||||||
.select((eb) => eb.fn.toJson(eb.table('stacked_assets')).as('stack'))
|
.select((eb) => eb.fn.toJson(eb.table('stacked_assets')).$castTo<Stack | null>().as('stack'))
|
||||||
.where('assets.ownerId', '=', asUuid(ownerId))
|
.where('assets.ownerId', '=', asUuid(ownerId))
|
||||||
.where('assets.isVisible', '=', true)
|
.where('assets.isVisible', '=', true)
|
||||||
.where('assets.updatedAt', '<=', updatedUntil)
|
.where('assets.updatedAt', '<=', updatedUntil)
|
||||||
.$if(!!lastId, (qb) => qb.where('assets.id', '>', lastId!))
|
.$if(!!lastId, (qb) => qb.where('assets.id', '>', lastId!))
|
||||||
.orderBy('assets.id')
|
.orderBy('assets.id')
|
||||||
.limit(limit)
|
.limit(limit)
|
||||||
.execute() as any as Promise<AssetEntity[]>;
|
.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
@GenerateSql({ params: [{ userIds: [DummyValue.UUID], updatedAfter: DummyValue.DATE, limit: 100 }] })
|
@GenerateSql({ params: [{ userIds: [DummyValue.UUID], updatedAfter: DummyValue.DATE, limit: 100 }] })
|
||||||
async getChangedDeltaSync(options: AssetDeltaSyncOptions): Promise<AssetEntity[]> {
|
async getChangedDeltaSync(options: AssetDeltaSyncOptions) {
|
||||||
return this.db
|
return this.db
|
||||||
.selectFrom('assets')
|
.selectFrom('assets')
|
||||||
.selectAll('assets')
|
.selectAll('assets')
|
||||||
@ -1024,12 +972,12 @@ export class AssetRepository {
|
|||||||
.as('stacked_assets'),
|
.as('stacked_assets'),
|
||||||
(join) => join.on('asset_stack.id', 'is not', null),
|
(join) => join.on('asset_stack.id', 'is not', null),
|
||||||
)
|
)
|
||||||
.select((eb) => eb.fn.toJson(eb.table('stacked_assets')).as('stack'))
|
.select((eb) => eb.fn.toJson(eb.table('stacked_assets').$castTo<Stack | null>()).as('stack'))
|
||||||
.where('assets.ownerId', '=', anyUuid(options.userIds))
|
.where('assets.ownerId', '=', anyUuid(options.userIds))
|
||||||
.where('assets.isVisible', '=', true)
|
.where('assets.isVisible', '=', true)
|
||||||
.where('assets.updatedAt', '>', options.updatedAfter)
|
.where('assets.updatedAt', '>', options.updatedAfter)
|
||||||
.limit(options.limit)
|
.limit(options.limit)
|
||||||
.execute() as any as Promise<AssetEntity[]>;
|
.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
async upsertFile(file: Pick<Insertable<AssetFiles>, 'assetId' | 'path' | 'type'>): Promise<void> {
|
async upsertFile(file: Pick<Insertable<AssetFiles>, 'assetId' | 'path' | 'type'>): Promise<void> {
|
||||||
|
@ -2,7 +2,6 @@ import { Injectable } from '@nestjs/common';
|
|||||||
import AsyncLock from 'async-lock';
|
import AsyncLock from 'async-lock';
|
||||||
import { FileMigrationProvider, Kysely, Migrator, sql, Transaction } from 'kysely';
|
import { FileMigrationProvider, Kysely, Migrator, sql, Transaction } from 'kysely';
|
||||||
import { InjectKysely } from 'nestjs-kysely';
|
import { InjectKysely } from 'nestjs-kysely';
|
||||||
import { existsSync } from 'node:fs';
|
|
||||||
import { readdir } from 'node:fs/promises';
|
import { readdir } from 'node:fs/promises';
|
||||||
import { join } from 'node:path';
|
import { join } from 'node:path';
|
||||||
import semver from 'semver';
|
import semver from 'semver';
|
||||||
@ -197,62 +196,54 @@ export class DatabaseRepository {
|
|||||||
return dimSize;
|
return dimSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
async runMigrations(options?: { transaction?: 'all' | 'none' | 'each'; only?: 'kysely' | 'typeorm' }): Promise<void> {
|
async runMigrations(options?: { transaction?: 'all' | 'none' | 'each' }): Promise<void> {
|
||||||
const { database } = this.configRepository.getEnv();
|
const { database } = this.configRepository.getEnv();
|
||||||
if (options?.only !== 'kysely') {
|
|
||||||
const dataSource = new DataSource(database.config.typeorm);
|
|
||||||
|
|
||||||
this.logger.log('Running migrations, this may take a while');
|
this.logger.log('Running migrations, this may take a while');
|
||||||
|
|
||||||
|
const tableExists = sql<{ result: string | null }>`select to_regclass('migrations') as "result"`;
|
||||||
|
const { rows } = await tableExists.execute(this.db);
|
||||||
|
const hasTypeOrmMigrations = !!rows[0]?.result;
|
||||||
|
if (hasTypeOrmMigrations) {
|
||||||
this.logger.debug('Running typeorm migrations');
|
this.logger.debug('Running typeorm migrations');
|
||||||
|
const dataSource = new DataSource(database.config.typeorm);
|
||||||
await dataSource.initialize();
|
await dataSource.initialize();
|
||||||
await dataSource.runMigrations(options);
|
await dataSource.runMigrations(options);
|
||||||
await dataSource.destroy();
|
await dataSource.destroy();
|
||||||
|
|
||||||
this.logger.debug('Finished running typeorm migrations');
|
this.logger.debug('Finished running typeorm migrations');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options?.only !== 'typeorm') {
|
this.logger.debug('Running kysely migrations');
|
||||||
// eslint-disable-next-line unicorn/prefer-module
|
const migrator = new Migrator({
|
||||||
const migrationFolder = join(__dirname, '..', 'schema/migrations');
|
db: this.db,
|
||||||
|
migrationLockTableName: 'kysely_migrations_lock',
|
||||||
|
migrationTableName: 'kysely_migrations',
|
||||||
|
provider: new FileMigrationProvider({
|
||||||
|
fs: { readdir },
|
||||||
|
path: { join },
|
||||||
|
// eslint-disable-next-line unicorn/prefer-module
|
||||||
|
migrationFolder: join(__dirname, '..', 'schema/migrations'),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
// TODO remove after we have at least one kysely migration
|
const { error, results } = await migrator.migrateToLatest();
|
||||||
if (!existsSync(migrationFolder)) {
|
|
||||||
return;
|
for (const result of results ?? []) {
|
||||||
|
if (result.status === 'Success') {
|
||||||
|
this.logger.log(`Migration "${result.migrationName}" succeeded`);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger.debug('Running kysely migrations');
|
if (result.status === 'Error') {
|
||||||
const migrator = new Migrator({
|
this.logger.warn(`Migration "${result.migrationName}" failed`);
|
||||||
db: this.db,
|
|
||||||
migrationLockTableName: 'kysely_migrations_lock',
|
|
||||||
migrationTableName: 'kysely_migrations',
|
|
||||||
provider: new FileMigrationProvider({
|
|
||||||
fs: { readdir },
|
|
||||||
path: { join },
|
|
||||||
migrationFolder,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
const { error, results } = await migrator.migrateToLatest();
|
|
||||||
|
|
||||||
for (const result of results ?? []) {
|
|
||||||
if (result.status === 'Success') {
|
|
||||||
this.logger.log(`Migration "${result.migrationName}" succeeded`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result.status === 'Error') {
|
|
||||||
this.logger.warn(`Migration "${result.migrationName}" failed`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error) {
|
|
||||||
this.logger.error(`Kysely migrations failed: ${error}`);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.logger.debug('Finished running kysely migrations');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
this.logger.error(`Kysely migrations failed: ${error}`);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.debug('Finished running kysely migrations');
|
||||||
}
|
}
|
||||||
|
|
||||||
async withLock<R>(lock: DatabaseLock, callback: () => Promise<R>): Promise<R> {
|
async withLock<R>(lock: DatabaseLock, callback: () => Promise<R>): Promise<R> {
|
||||||
|
@ -74,11 +74,21 @@ export class MyConsoleLogger extends ConsoleLogger {
|
|||||||
export class LoggingRepository {
|
export class LoggingRepository {
|
||||||
private logger: MyConsoleLogger;
|
private logger: MyConsoleLogger;
|
||||||
|
|
||||||
constructor(@Inject(ClsService) cls: ClsService | undefined, configRepository: ConfigRepository) {
|
constructor(
|
||||||
const { noColor } = configRepository.getEnv();
|
@Inject(ClsService) cls: ClsService | undefined,
|
||||||
|
@Inject(ConfigRepository) configRepository: ConfigRepository | undefined,
|
||||||
|
) {
|
||||||
|
let noColor = false;
|
||||||
|
if (configRepository) {
|
||||||
|
noColor = configRepository.getEnv().noColor;
|
||||||
|
}
|
||||||
this.logger = new MyConsoleLogger(cls, { context: LoggingRepository.name, color: !noColor });
|
this.logger = new MyConsoleLogger(cls, { context: LoggingRepository.name, color: !noColor });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static create() {
|
||||||
|
return new LoggingRepository(undefined, undefined);
|
||||||
|
}
|
||||||
|
|
||||||
setAppName(name: string): void {
|
setAppName(name: string): void {
|
||||||
appName = name.charAt(0).toUpperCase() + name.slice(1);
|
appName = name.charAt(0).toUpperCase() + name.slice(1);
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,12 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { Kysely, OrderByDirection, sql } from 'kysely';
|
import { Kysely, OrderByDirection, Selectable, sql } from 'kysely';
|
||||||
import { InjectKysely } from 'nestjs-kysely';
|
import { InjectKysely } from 'nestjs-kysely';
|
||||||
import { randomUUID } from 'node:crypto';
|
import { randomUUID } from 'node:crypto';
|
||||||
import { DB } from 'src/db';
|
import { DB, Exif } from 'src/db';
|
||||||
import { DummyValue, GenerateSql } from 'src/decorators';
|
import { DummyValue, GenerateSql } from 'src/decorators';
|
||||||
import { AssetEntity, searchAssetBuilder } from 'src/entities/asset.entity';
|
import { MapAsset } from 'src/dtos/asset-response.dto';
|
||||||
import { AssetStatus, AssetType } from 'src/enum';
|
import { AssetStatus, AssetType } from 'src/enum';
|
||||||
import { anyUuid, asUuid } from 'src/utils/database';
|
import { anyUuid, asUuid, searchAssetBuilder } from 'src/utils/database';
|
||||||
import { Paginated } from 'src/utils/pagination';
|
|
||||||
import { isValidInteger } from 'src/validation';
|
import { isValidInteger } from 'src/validation';
|
||||||
|
|
||||||
export interface SearchResult<T> {
|
export interface SearchResult<T> {
|
||||||
@ -216,7 +215,7 @@ export class SearchRepository {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
async searchMetadata(pagination: SearchPaginationOptions, options: AssetSearchOptions): Paginated<AssetEntity> {
|
async searchMetadata(pagination: SearchPaginationOptions, options: AssetSearchOptions) {
|
||||||
const orderDirection = (options.orderDirection?.toLowerCase() || 'desc') as OrderByDirection;
|
const orderDirection = (options.orderDirection?.toLowerCase() || 'desc') as OrderByDirection;
|
||||||
const items = await searchAssetBuilder(this.db, options)
|
const items = await searchAssetBuilder(this.db, options)
|
||||||
.orderBy('assets.fileCreatedAt', orderDirection)
|
.orderBy('assets.fileCreatedAt', orderDirection)
|
||||||
@ -225,7 +224,7 @@ export class SearchRepository {
|
|||||||
.execute();
|
.execute();
|
||||||
const hasNextPage = items.length > pagination.size;
|
const hasNextPage = items.length > pagination.size;
|
||||||
items.splice(pagination.size);
|
items.splice(pagination.size);
|
||||||
return { items: items as any as AssetEntity[], hasNextPage };
|
return { items, hasNextPage };
|
||||||
}
|
}
|
||||||
|
|
||||||
@GenerateSql({
|
@GenerateSql({
|
||||||
@ -240,7 +239,7 @@ export class SearchRepository {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
async searchRandom(size: number, options: AssetSearchOptions): Promise<AssetEntity[]> {
|
async searchRandom(size: number, options: AssetSearchOptions) {
|
||||||
const uuid = randomUUID();
|
const uuid = randomUUID();
|
||||||
const builder = searchAssetBuilder(this.db, options);
|
const builder = searchAssetBuilder(this.db, options);
|
||||||
const lessThan = builder
|
const lessThan = builder
|
||||||
@ -251,8 +250,8 @@ export class SearchRepository {
|
|||||||
.where('assets.id', '>', uuid)
|
.where('assets.id', '>', uuid)
|
||||||
.orderBy(sql`random()`)
|
.orderBy(sql`random()`)
|
||||||
.limit(size);
|
.limit(size);
|
||||||
const { rows } = await sql`${lessThan} union all ${greaterThan} limit ${size}`.execute(this.db);
|
const { rows } = await sql<MapAsset>`${lessThan} union all ${greaterThan} limit ${size}`.execute(this.db);
|
||||||
return rows as any as AssetEntity[];
|
return rows;
|
||||||
}
|
}
|
||||||
|
|
||||||
@GenerateSql({
|
@GenerateSql({
|
||||||
@ -268,17 +267,17 @@ export class SearchRepository {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
async searchSmart(pagination: SearchPaginationOptions, options: SmartSearchOptions): Paginated<AssetEntity> {
|
async searchSmart(pagination: SearchPaginationOptions, options: SmartSearchOptions) {
|
||||||
if (!isValidInteger(pagination.size, { min: 1, max: 1000 })) {
|
if (!isValidInteger(pagination.size, { min: 1, max: 1000 })) {
|
||||||
throw new Error(`Invalid value for 'size': ${pagination.size}`);
|
throw new Error(`Invalid value for 'size': ${pagination.size}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const items = (await searchAssetBuilder(this.db, options)
|
const items = await searchAssetBuilder(this.db, options)
|
||||||
.innerJoin('smart_search', 'assets.id', 'smart_search.assetId')
|
.innerJoin('smart_search', 'assets.id', 'smart_search.assetId')
|
||||||
.orderBy(sql`smart_search.embedding <=> ${options.embedding}`)
|
.orderBy(sql`smart_search.embedding <=> ${options.embedding}`)
|
||||||
.limit(pagination.size + 1)
|
.limit(pagination.size + 1)
|
||||||
.offset((pagination.page - 1) * pagination.size)
|
.offset((pagination.page - 1) * pagination.size)
|
||||||
.execute()) as any as AssetEntity[];
|
.execute();
|
||||||
|
|
||||||
const hasNextPage = items.length > pagination.size;
|
const hasNextPage = items.length > pagination.size;
|
||||||
items.splice(pagination.size);
|
items.splice(pagination.size);
|
||||||
@ -392,7 +391,7 @@ export class SearchRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@GenerateSql({ params: [[DummyValue.UUID]] })
|
@GenerateSql({ params: [[DummyValue.UUID]] })
|
||||||
getAssetsByCity(userIds: string[]): Promise<AssetEntity[]> {
|
getAssetsByCity(userIds: string[]) {
|
||||||
return this.db
|
return this.db
|
||||||
.withRecursive('cte', (qb) => {
|
.withRecursive('cte', (qb) => {
|
||||||
const base = qb
|
const base = qb
|
||||||
@ -434,9 +433,14 @@ export class SearchRepository {
|
|||||||
.innerJoin('exif', 'assets.id', 'exif.assetId')
|
.innerJoin('exif', 'assets.id', 'exif.assetId')
|
||||||
.innerJoin('cte', 'assets.id', 'cte.assetId')
|
.innerJoin('cte', 'assets.id', 'cte.assetId')
|
||||||
.selectAll('assets')
|
.selectAll('assets')
|
||||||
.select((eb) => eb.fn('to_jsonb', [eb.table('exif')]).as('exifInfo'))
|
.select((eb) =>
|
||||||
|
eb
|
||||||
|
.fn('to_jsonb', [eb.table('exif')])
|
||||||
|
.$castTo<Selectable<Exif>>()
|
||||||
|
.as('exifInfo'),
|
||||||
|
)
|
||||||
.orderBy('exif.city')
|
.orderBy('exif.city')
|
||||||
.execute() as any as Promise<AssetEntity[]>;
|
.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
async upsert(assetId: string, embedding: string): Promise<void> {
|
async upsert(assetId: string, embedding: string): Promise<void> {
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { Insertable, Kysely, sql, Updateable } from 'kysely';
|
import { Insertable, Kysely, NotNull, sql, Updateable } from 'kysely';
|
||||||
import { jsonObjectFrom } from 'kysely/helpers/postgres';
|
import { jsonObjectFrom } from 'kysely/helpers/postgres';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { InjectKysely } from 'nestjs-kysely';
|
import { InjectKysely } from 'nestjs-kysely';
|
||||||
import { columns } from 'src/database';
|
import { Album, columns } from 'src/database';
|
||||||
import { DB, SharedLinks } from 'src/db';
|
import { DB, SharedLinks } from 'src/db';
|
||||||
import { DummyValue, GenerateSql } from 'src/decorators';
|
import { DummyValue, GenerateSql } from 'src/decorators';
|
||||||
import { SharedLinkEntity } from 'src/entities/shared-link.entity';
|
import { MapAsset } from 'src/dtos/asset-response.dto';
|
||||||
import { SharedLinkType } from 'src/enum';
|
import { SharedLinkType } from 'src/enum';
|
||||||
|
|
||||||
export type SharedLinkSearchOptions = {
|
export type SharedLinkSearchOptions = {
|
||||||
@ -19,7 +19,7 @@ export class SharedLinkRepository {
|
|||||||
constructor(@InjectKysely() private db: Kysely<DB>) {}
|
constructor(@InjectKysely() private db: Kysely<DB>) {}
|
||||||
|
|
||||||
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID] })
|
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID] })
|
||||||
get(userId: string, id: string): Promise<SharedLinkEntity | undefined> {
|
get(userId: string, id: string) {
|
||||||
return this.db
|
return this.db
|
||||||
.selectFrom('shared_links')
|
.selectFrom('shared_links')
|
||||||
.selectAll('shared_links')
|
.selectAll('shared_links')
|
||||||
@ -87,18 +87,23 @@ export class SharedLinkRepository {
|
|||||||
.as('album'),
|
.as('album'),
|
||||||
(join) => join.onTrue(),
|
(join) => join.onTrue(),
|
||||||
)
|
)
|
||||||
.select((eb) => eb.fn.coalesce(eb.fn.jsonAgg('a').filterWhere('a.id', 'is not', null), sql`'[]'`).as('assets'))
|
.select((eb) =>
|
||||||
|
eb.fn
|
||||||
|
.coalesce(eb.fn.jsonAgg('a').filterWhere('a.id', 'is not', null), sql`'[]'`)
|
||||||
|
.$castTo<MapAsset[]>()
|
||||||
|
.as('assets'),
|
||||||
|
)
|
||||||
.groupBy(['shared_links.id', sql`"album".*`])
|
.groupBy(['shared_links.id', sql`"album".*`])
|
||||||
.select((eb) => eb.fn.toJson('album').as('album'))
|
.select((eb) => eb.fn.toJson('album').$castTo<Album | null>().as('album'))
|
||||||
.where('shared_links.id', '=', id)
|
.where('shared_links.id', '=', id)
|
||||||
.where('shared_links.userId', '=', userId)
|
.where('shared_links.userId', '=', userId)
|
||||||
.where((eb) => eb.or([eb('shared_links.type', '=', SharedLinkType.INDIVIDUAL), eb('album.id', 'is not', null)]))
|
.where((eb) => eb.or([eb('shared_links.type', '=', SharedLinkType.INDIVIDUAL), eb('album.id', 'is not', null)]))
|
||||||
.orderBy('shared_links.createdAt', 'desc')
|
.orderBy('shared_links.createdAt', 'desc')
|
||||||
.executeTakeFirst() as Promise<SharedLinkEntity | undefined>;
|
.executeTakeFirst();
|
||||||
}
|
}
|
||||||
|
|
||||||
@GenerateSql({ params: [{ userId: DummyValue.UUID, albumId: DummyValue.UUID }] })
|
@GenerateSql({ params: [{ userId: DummyValue.UUID, albumId: DummyValue.UUID }] })
|
||||||
getAll({ userId, albumId }: SharedLinkSearchOptions): Promise<SharedLinkEntity[]> {
|
getAll({ userId, albumId }: SharedLinkSearchOptions) {
|
||||||
return this.db
|
return this.db
|
||||||
.selectFrom('shared_links')
|
.selectFrom('shared_links')
|
||||||
.selectAll('shared_links')
|
.selectAll('shared_links')
|
||||||
@ -115,6 +120,7 @@ export class SharedLinkRepository {
|
|||||||
(join) => join.onTrue(),
|
(join) => join.onTrue(),
|
||||||
)
|
)
|
||||||
.select('assets.assets')
|
.select('assets.assets')
|
||||||
|
.$narrowType<{ assets: NotNull }>()
|
||||||
.leftJoinLateral(
|
.leftJoinLateral(
|
||||||
(eb) =>
|
(eb) =>
|
||||||
eb
|
eb
|
||||||
@ -152,12 +158,12 @@ export class SharedLinkRepository {
|
|||||||
.as('album'),
|
.as('album'),
|
||||||
(join) => join.onTrue(),
|
(join) => join.onTrue(),
|
||||||
)
|
)
|
||||||
.select((eb) => eb.fn.toJson('album').as('album'))
|
.select((eb) => eb.fn.toJson('album').$castTo<Album | null>().as('album'))
|
||||||
.where((eb) => eb.or([eb('shared_links.type', '=', SharedLinkType.INDIVIDUAL), eb('album.id', 'is not', null)]))
|
.where((eb) => eb.or([eb('shared_links.type', '=', SharedLinkType.INDIVIDUAL), eb('album.id', 'is not', null)]))
|
||||||
.$if(!!albumId, (eb) => eb.where('shared_links.albumId', '=', albumId!))
|
.$if(!!albumId, (eb) => eb.where('shared_links.albumId', '=', albumId!))
|
||||||
.orderBy('shared_links.createdAt', 'desc')
|
.orderBy('shared_links.createdAt', 'desc')
|
||||||
.distinctOn(['shared_links.createdAt'])
|
.distinctOn(['shared_links.createdAt'])
|
||||||
.execute() as unknown as Promise<SharedLinkEntity[]>;
|
.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
@GenerateSql({ params: [DummyValue.BUFFER] })
|
@GenerateSql({ params: [DummyValue.BUFFER] })
|
||||||
@ -177,7 +183,7 @@ export class SharedLinkRepository {
|
|||||||
.executeTakeFirst();
|
.executeTakeFirst();
|
||||||
}
|
}
|
||||||
|
|
||||||
async create(entity: Insertable<SharedLinks> & { assetIds?: string[] }): Promise<SharedLinkEntity> {
|
async create(entity: Insertable<SharedLinks> & { assetIds?: string[] }) {
|
||||||
const { id } = await this.db
|
const { id } = await this.db
|
||||||
.insertInto('shared_links')
|
.insertInto('shared_links')
|
||||||
.values(_.omit(entity, 'assetIds'))
|
.values(_.omit(entity, 'assetIds'))
|
||||||
@ -194,7 +200,7 @@ export class SharedLinkRepository {
|
|||||||
return this.getSharedLinks(id);
|
return this.getSharedLinks(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
async update(entity: Updateable<SharedLinks> & { id: string; assetIds?: string[] }): Promise<SharedLinkEntity> {
|
async update(entity: Updateable<SharedLinks> & { id: string; assetIds?: string[] }) {
|
||||||
const { id } = await this.db
|
const { id } = await this.db
|
||||||
.updateTable('shared_links')
|
.updateTable('shared_links')
|
||||||
.set(_.omit(entity, 'assets', 'album', 'assetIds'))
|
.set(_.omit(entity, 'assets', 'album', 'assetIds'))
|
||||||
@ -212,8 +218,8 @@ export class SharedLinkRepository {
|
|||||||
return this.getSharedLinks(id);
|
return this.getSharedLinks(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
async remove(entity: SharedLinkEntity): Promise<void> {
|
async remove(id: string): Promise<void> {
|
||||||
await this.db.deleteFrom('shared_links').where('shared_links.id', '=', entity.id).execute();
|
await this.db.deleteFrom('shared_links').where('shared_links.id', '=', id).execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
private getSharedLinks(id: string) {
|
private getSharedLinks(id: string) {
|
||||||
@ -236,9 +242,12 @@ export class SharedLinkRepository {
|
|||||||
(join) => join.onTrue(),
|
(join) => join.onTrue(),
|
||||||
)
|
)
|
||||||
.select((eb) =>
|
.select((eb) =>
|
||||||
eb.fn.coalesce(eb.fn.jsonAgg('assets').filterWhere('assets.id', 'is not', null), sql`'[]'`).as('assets'),
|
eb.fn
|
||||||
|
.coalesce(eb.fn.jsonAgg('assets').filterWhere('assets.id', 'is not', null), sql`'[]'`)
|
||||||
|
.$castTo<MapAsset[]>()
|
||||||
|
.as('assets'),
|
||||||
)
|
)
|
||||||
.groupBy('shared_links.id')
|
.groupBy('shared_links.id')
|
||||||
.executeTakeFirstOrThrow() as Promise<SharedLinkEntity>;
|
.executeTakeFirstOrThrow();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,6 @@ import { InjectKysely } from 'nestjs-kysely';
|
|||||||
import { columns } from 'src/database';
|
import { columns } from 'src/database';
|
||||||
import { AssetStack, DB } from 'src/db';
|
import { AssetStack, DB } from 'src/db';
|
||||||
import { DummyValue, GenerateSql } from 'src/decorators';
|
import { DummyValue, GenerateSql } from 'src/decorators';
|
||||||
import { AssetEntity } from 'src/entities/asset.entity';
|
|
||||||
import { asUuid } from 'src/utils/database';
|
import { asUuid } from 'src/utils/database';
|
||||||
|
|
||||||
export interface StackSearch {
|
export interface StackSearch {
|
||||||
@ -36,9 +35,7 @@ const withAssets = (eb: ExpressionBuilder<DB, 'asset_stack'>, withTags = false)
|
|||||||
.select((eb) => eb.fn.toJson('exifInfo').as('exifInfo'))
|
.select((eb) => eb.fn.toJson('exifInfo').as('exifInfo'))
|
||||||
.where('assets.deletedAt', 'is', null)
|
.where('assets.deletedAt', 'is', null)
|
||||||
.whereRef('assets.stackId', '=', 'asset_stack.id'),
|
.whereRef('assets.stackId', '=', 'asset_stack.id'),
|
||||||
)
|
).as('assets');
|
||||||
.$castTo<AssetEntity[]>()
|
|
||||||
.as('assets');
|
|
||||||
};
|
};
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
|
@ -2,8 +2,7 @@ import { Kysely } from 'kysely';
|
|||||||
import { InjectKysely } from 'nestjs-kysely';
|
import { InjectKysely } from 'nestjs-kysely';
|
||||||
import { DB } from 'src/db';
|
import { DB } from 'src/db';
|
||||||
import { DummyValue, GenerateSql } from 'src/decorators';
|
import { DummyValue, GenerateSql } from 'src/decorators';
|
||||||
import { withExif } from 'src/entities/asset.entity';
|
import { asUuid, withExif } from 'src/utils/database';
|
||||||
import { asUuid } from 'src/utils/database';
|
|
||||||
|
|
||||||
export class ViewRepository {
|
export class ViewRepository {
|
||||||
constructor(@InjectKysely() private db: Kysely<DB>) {}
|
constructor(@InjectKysely() private db: Kysely<DB>) {}
|
||||||
|
410
server/src/schema/migrations/1744910873969-InitialMigration.ts
Normal file
410
server/src/schema/migrations/1744910873969-InitialMigration.ts
Normal file
@ -0,0 +1,410 @@
|
|||||||
|
import { Kysely, sql } from 'kysely';
|
||||||
|
import { DatabaseExtension } from 'src/enum';
|
||||||
|
import { ConfigRepository } from 'src/repositories/config.repository';
|
||||||
|
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||||
|
|
||||||
|
const vectorExtension = new ConfigRepository().getEnv().database.vectorExtension;
|
||||||
|
const lastMigrationSql = sql<{ name: string }>`SELECT "name" FROM "migrations" ORDER BY "timestamp" DESC LIMIT 1;`;
|
||||||
|
const tableExists = sql<{ result: string | null }>`select to_regclass('migrations') as "result"`;
|
||||||
|
const logger = LoggingRepository.create();
|
||||||
|
|
||||||
|
export async function up(db: Kysely<any>): Promise<void> {
|
||||||
|
const { rows } = await tableExists.execute(db);
|
||||||
|
const hasTypeOrmMigrations = !!rows[0]?.result;
|
||||||
|
if (hasTypeOrmMigrations) {
|
||||||
|
const {
|
||||||
|
rows: [lastMigration],
|
||||||
|
} = await lastMigrationSql.execute(db);
|
||||||
|
if (lastMigration?.name !== 'AddMissingIndex1744910873956') {
|
||||||
|
throw new Error(
|
||||||
|
'Invalid upgrade path. For more information, see https://immich.app/errors#typeorm-upgrade',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
logger.log('Database has up to date TypeORM migrations, skipping initial Kysely migration');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await sql`CREATE EXTENSION IF NOT EXISTS "uuid-ossp";`.execute(db);
|
||||||
|
await sql`CREATE EXTENSION IF NOT EXISTS "unaccent";`.execute(db);
|
||||||
|
await sql`CREATE EXTENSION IF NOT EXISTS "cube";`.execute(db);
|
||||||
|
await sql`CREATE EXTENSION IF NOT EXISTS "earthdistance";`.execute(db);
|
||||||
|
await sql`CREATE EXTENSION IF NOT EXISTS "pg_trgm";`.execute(db);
|
||||||
|
await sql`CREATE EXTENSION IF NOT EXISTS "vectors";`.execute(db);
|
||||||
|
await sql`CREATE OR REPLACE FUNCTION immich_uuid_v7(p_timestamp timestamp with time zone default clock_timestamp())
|
||||||
|
RETURNS uuid
|
||||||
|
VOLATILE LANGUAGE SQL
|
||||||
|
AS $$
|
||||||
|
select encode(
|
||||||
|
set_bit(
|
||||||
|
set_bit(
|
||||||
|
overlay(uuid_send(gen_random_uuid())
|
||||||
|
placing substring(int8send(floor(extract(epoch from p_timestamp) * 1000)::bigint) from 3)
|
||||||
|
from 1 for 6
|
||||||
|
),
|
||||||
|
52, 1
|
||||||
|
),
|
||||||
|
53, 1
|
||||||
|
),
|
||||||
|
'hex')::uuid;
|
||||||
|
$$;`.execute(db);
|
||||||
|
await sql`CREATE OR REPLACE FUNCTION updated_at()
|
||||||
|
RETURNS TRIGGER
|
||||||
|
LANGUAGE PLPGSQL
|
||||||
|
AS $$
|
||||||
|
DECLARE
|
||||||
|
clock_timestamp TIMESTAMP := clock_timestamp();
|
||||||
|
BEGIN
|
||||||
|
new."updatedAt" = clock_timestamp;
|
||||||
|
new."updateId" = immich_uuid_v7(clock_timestamp);
|
||||||
|
return new;
|
||||||
|
END;
|
||||||
|
$$;`.execute(db);
|
||||||
|
await sql`CREATE OR REPLACE FUNCTION f_concat_ws(text, text[])
|
||||||
|
RETURNS text
|
||||||
|
PARALLEL SAFE IMMUTABLE LANGUAGE SQL
|
||||||
|
AS $$SELECT array_to_string($2, $1)$$;`.execute(db);
|
||||||
|
await sql`CREATE OR REPLACE FUNCTION f_unaccent(text)
|
||||||
|
RETURNS text
|
||||||
|
PARALLEL SAFE STRICT IMMUTABLE LANGUAGE SQL
|
||||||
|
RETURN unaccent('unaccent', $1)`.execute(db);
|
||||||
|
await sql`CREATE OR REPLACE FUNCTION ll_to_earth_public(latitude double precision, longitude double precision)
|
||||||
|
RETURNS public.earth
|
||||||
|
PARALLEL SAFE STRICT IMMUTABLE LANGUAGE SQL
|
||||||
|
AS $$
|
||||||
|
SELECT public.cube(public.cube(public.cube(public.earth()*cos(radians(latitude))*cos(radians(longitude))),public.earth()*cos(radians(latitude))*sin(radians(longitude))),public.earth()*sin(radians(latitude)))::public.earth
|
||||||
|
$$;`.execute(db);
|
||||||
|
await sql`CREATE OR REPLACE FUNCTION users_delete_audit()
|
||||||
|
RETURNS TRIGGER
|
||||||
|
LANGUAGE PLPGSQL
|
||||||
|
AS $$
|
||||||
|
BEGIN
|
||||||
|
INSERT INTO users_audit ("userId")
|
||||||
|
SELECT "id"
|
||||||
|
FROM OLD;
|
||||||
|
RETURN NULL;
|
||||||
|
END;
|
||||||
|
$$;`.execute(db);
|
||||||
|
await sql`CREATE OR REPLACE FUNCTION partners_delete_audit()
|
||||||
|
RETURNS TRIGGER
|
||||||
|
LANGUAGE PLPGSQL
|
||||||
|
AS $$
|
||||||
|
BEGIN
|
||||||
|
INSERT INTO partners_audit ("sharedById", "sharedWithId")
|
||||||
|
SELECT "sharedById", "sharedWithId"
|
||||||
|
FROM OLD;
|
||||||
|
RETURN NULL;
|
||||||
|
END;
|
||||||
|
$$;`.execute(db);
|
||||||
|
await sql`CREATE OR REPLACE FUNCTION assets_delete_audit()
|
||||||
|
RETURNS TRIGGER
|
||||||
|
LANGUAGE PLPGSQL
|
||||||
|
AS $$
|
||||||
|
BEGIN
|
||||||
|
INSERT INTO assets_audit ("assetId", "ownerId")
|
||||||
|
SELECT "id", "ownerId"
|
||||||
|
FROM OLD;
|
||||||
|
RETURN NULL;
|
||||||
|
END;
|
||||||
|
$$;`.execute(db);
|
||||||
|
if (vectorExtension === DatabaseExtension.VECTORS) {
|
||||||
|
await sql`SET search_path TO "$user", public, vectors`.execute(db);
|
||||||
|
await sql`SET vectors.pgvector_compatibility=on`.execute(db);
|
||||||
|
}
|
||||||
|
await sql`CREATE TYPE "assets_status_enum" AS ENUM ('active','trashed','deleted');`.execute(db);
|
||||||
|
await sql`CREATE TYPE "sourcetype" AS ENUM ('machine-learning','exif','manual');`.execute(db);
|
||||||
|
await sql`CREATE TABLE "users" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "email" character varying NOT NULL, "password" character varying NOT NULL DEFAULT '', "createdAt" timestamp with time zone NOT NULL DEFAULT now(), "profileImagePath" character varying NOT NULL DEFAULT '', "isAdmin" boolean NOT NULL DEFAULT false, "shouldChangePassword" boolean NOT NULL DEFAULT true, "deletedAt" timestamp with time zone, "oauthId" character varying NOT NULL DEFAULT '', "updatedAt" timestamp with time zone NOT NULL DEFAULT now(), "storageLabel" character varying, "name" character varying NOT NULL DEFAULT '', "quotaSizeInBytes" bigint, "quotaUsageInBytes" bigint NOT NULL DEFAULT 0, "status" character varying NOT NULL DEFAULT 'active', "profileChangedAt" timestamp with time zone NOT NULL DEFAULT now(), "updateId" uuid NOT NULL DEFAULT immich_uuid_v7());`.execute(db);
|
||||||
|
await sql`CREATE TABLE "libraries" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "name" character varying NOT NULL, "ownerId" uuid NOT NULL, "importPaths" text[] NOT NULL, "exclusionPatterns" text[] NOT NULL, "createdAt" timestamp with time zone NOT NULL DEFAULT now(), "updatedAt" timestamp with time zone NOT NULL DEFAULT now(), "deletedAt" timestamp with time zone, "refreshedAt" timestamp with time zone, "updateId" uuid NOT NULL DEFAULT immich_uuid_v7());`.execute(db);
|
||||||
|
await sql`CREATE TABLE "asset_stack" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "primaryAssetId" uuid NOT NULL, "ownerId" uuid NOT NULL);`.execute(db);
|
||||||
|
await sql`CREATE TABLE "assets" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "deviceAssetId" character varying NOT NULL, "ownerId" uuid NOT NULL, "deviceId" character varying NOT NULL, "type" character varying NOT NULL, "originalPath" character varying NOT NULL, "fileCreatedAt" timestamp with time zone NOT NULL, "fileModifiedAt" timestamp with time zone NOT NULL, "isFavorite" boolean NOT NULL DEFAULT false, "duration" character varying, "encodedVideoPath" character varying DEFAULT '', "checksum" bytea NOT NULL, "isVisible" boolean NOT NULL DEFAULT true, "livePhotoVideoId" uuid, "updatedAt" timestamp with time zone NOT NULL DEFAULT now(), "createdAt" timestamp with time zone NOT NULL DEFAULT now(), "isArchived" boolean NOT NULL DEFAULT false, "originalFileName" character varying NOT NULL, "sidecarPath" character varying, "thumbhash" bytea, "isOffline" boolean NOT NULL DEFAULT false, "libraryId" uuid, "isExternal" boolean NOT NULL DEFAULT false, "deletedAt" timestamp with time zone, "localDateTime" timestamp with time zone NOT NULL, "stackId" uuid, "duplicateId" uuid, "status" assets_status_enum NOT NULL DEFAULT 'active', "updateId" uuid NOT NULL DEFAULT immich_uuid_v7());`.execute(db);
|
||||||
|
await sql`CREATE TABLE "albums" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "ownerId" uuid NOT NULL, "albumName" character varying NOT NULL DEFAULT 'Untitled Album', "createdAt" timestamp with time zone NOT NULL DEFAULT now(), "albumThumbnailAssetId" uuid, "updatedAt" timestamp with time zone NOT NULL DEFAULT now(), "description" text NOT NULL DEFAULT '', "deletedAt" timestamp with time zone, "isActivityEnabled" boolean NOT NULL DEFAULT true, "order" character varying NOT NULL DEFAULT 'desc', "updateId" uuid NOT NULL DEFAULT immich_uuid_v7());`.execute(db);
|
||||||
|
await sql`COMMENT ON COLUMN "albums"."albumThumbnailAssetId" IS 'Asset ID to be used as thumbnail';`.execute(db);
|
||||||
|
await sql`CREATE TABLE "activity" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "createdAt" timestamp with time zone NOT NULL DEFAULT now(), "updatedAt" timestamp with time zone NOT NULL DEFAULT now(), "albumId" uuid NOT NULL, "userId" uuid NOT NULL, "assetId" uuid, "comment" text, "isLiked" boolean NOT NULL DEFAULT false, "updateId" uuid NOT NULL DEFAULT immich_uuid_v7());`.execute(db);
|
||||||
|
await sql`CREATE TABLE "albums_assets_assets" ("albumsId" uuid NOT NULL, "assetsId" uuid NOT NULL, "createdAt" timestamp with time zone NOT NULL DEFAULT now());`.execute(db);
|
||||||
|
await sql`CREATE TABLE "albums_shared_users_users" ("albumsId" uuid NOT NULL, "usersId" uuid NOT NULL, "role" character varying NOT NULL DEFAULT 'editor');`.execute(db);
|
||||||
|
await sql`CREATE TABLE "api_keys" ("name" character varying NOT NULL, "key" character varying NOT NULL, "userId" uuid NOT NULL, "createdAt" timestamp with time zone NOT NULL DEFAULT now(), "updatedAt" timestamp with time zone NOT NULL DEFAULT now(), "id" uuid NOT NULL DEFAULT uuid_generate_v4(), "permissions" character varying[] NOT NULL, "updateId" uuid NOT NULL DEFAULT immich_uuid_v7());`.execute(db);
|
||||||
|
await sql`CREATE TABLE "assets_audit" ("id" uuid NOT NULL DEFAULT immich_uuid_v7(), "assetId" uuid NOT NULL, "ownerId" uuid NOT NULL, "deletedAt" timestamp with time zone NOT NULL DEFAULT clock_timestamp());`.execute(db);
|
||||||
|
await sql`CREATE TABLE "person" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "createdAt" timestamp with time zone NOT NULL DEFAULT now(), "updatedAt" timestamp with time zone NOT NULL DEFAULT now(), "ownerId" uuid NOT NULL, "name" character varying NOT NULL DEFAULT '', "thumbnailPath" character varying NOT NULL DEFAULT '', "isHidden" boolean NOT NULL DEFAULT false, "birthDate" date, "faceAssetId" uuid, "isFavorite" boolean NOT NULL DEFAULT false, "color" character varying, "updateId" uuid NOT NULL DEFAULT immich_uuid_v7());`.execute(db);
|
||||||
|
await sql`CREATE TABLE "asset_faces" ("assetId" uuid NOT NULL, "personId" uuid, "imageWidth" integer NOT NULL DEFAULT 0, "imageHeight" integer NOT NULL DEFAULT 0, "boundingBoxX1" integer NOT NULL DEFAULT 0, "boundingBoxY1" integer NOT NULL DEFAULT 0, "boundingBoxX2" integer NOT NULL DEFAULT 0, "boundingBoxY2" integer NOT NULL DEFAULT 0, "id" uuid NOT NULL DEFAULT uuid_generate_v4(), "sourceType" sourcetype NOT NULL DEFAULT 'machine-learning', "deletedAt" timestamp with time zone);`.execute(db);
|
||||||
|
await sql`CREATE TABLE "asset_files" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "assetId" uuid NOT NULL, "createdAt" timestamp with time zone NOT NULL DEFAULT now(), "updatedAt" timestamp with time zone NOT NULL DEFAULT now(), "type" character varying NOT NULL, "path" character varying NOT NULL, "updateId" uuid NOT NULL DEFAULT immich_uuid_v7());`.execute(db);
|
||||||
|
await sql`CREATE TABLE "asset_job_status" ("assetId" uuid NOT NULL, "facesRecognizedAt" timestamp with time zone, "metadataExtractedAt" timestamp with time zone, "duplicatesDetectedAt" timestamp with time zone, "previewAt" timestamp with time zone, "thumbnailAt" timestamp with time zone);`.execute(db);
|
||||||
|
await sql`CREATE TABLE "audit" ("id" serial NOT NULL, "entityType" character varying NOT NULL, "entityId" uuid NOT NULL, "action" character varying NOT NULL, "ownerId" uuid NOT NULL, "createdAt" timestamp with time zone NOT NULL DEFAULT now());`.execute(db);
|
||||||
|
await sql`CREATE TABLE "exif" ("assetId" uuid NOT NULL, "make" character varying, "model" character varying, "exifImageWidth" integer, "exifImageHeight" integer, "fileSizeInByte" bigint, "orientation" character varying, "dateTimeOriginal" timestamp with time zone, "modifyDate" timestamp with time zone, "lensModel" character varying, "fNumber" double precision, "focalLength" double precision, "iso" integer, "latitude" double precision, "longitude" double precision, "city" character varying, "state" character varying, "country" character varying, "description" text NOT NULL DEFAULT '', "fps" double precision, "exposureTime" character varying, "livePhotoCID" character varying, "timeZone" character varying, "projectionType" character varying, "profileDescription" character varying, "colorspace" character varying, "bitsPerSample" integer, "autoStackId" character varying, "rating" integer, "updatedAt" timestamp with time zone NOT NULL DEFAULT clock_timestamp(), "updateId" uuid NOT NULL DEFAULT immich_uuid_v7());`.execute(db);
|
||||||
|
await sql`CREATE TABLE "face_search" ("faceId" uuid NOT NULL, "embedding" vector(512) NOT NULL);`.execute(db);
|
||||||
|
await sql`CREATE TABLE "geodata_places" ("id" integer NOT NULL, "name" character varying(200) NOT NULL, "longitude" double precision NOT NULL, "latitude" double precision NOT NULL, "countryCode" character(2) NOT NULL, "admin1Code" character varying(20), "admin2Code" character varying(80), "modificationDate" date NOT NULL, "admin1Name" character varying, "admin2Name" character varying, "alternateNames" character varying);`.execute(db);
|
||||||
|
await sql`CREATE TABLE "memories" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "createdAt" timestamp with time zone NOT NULL DEFAULT now(), "updatedAt" timestamp with time zone NOT NULL DEFAULT now(), "deletedAt" timestamp with time zone, "ownerId" uuid NOT NULL, "type" character varying NOT NULL, "data" jsonb NOT NULL, "isSaved" boolean NOT NULL DEFAULT false, "memoryAt" timestamp with time zone NOT NULL, "seenAt" timestamp with time zone, "showAt" timestamp with time zone, "hideAt" timestamp with time zone, "updateId" uuid NOT NULL DEFAULT immich_uuid_v7());`.execute(db);
|
||||||
|
await sql`CREATE TABLE "memories_assets_assets" ("memoriesId" uuid NOT NULL, "assetsId" uuid NOT NULL);`.execute(db);
|
||||||
|
await sql`CREATE TABLE "move_history" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "entityId" uuid NOT NULL, "pathType" character varying NOT NULL, "oldPath" character varying NOT NULL, "newPath" character varying NOT NULL);`.execute(db);
|
||||||
|
await sql`CREATE TABLE "naturalearth_countries" ("id" integer NOT NULL GENERATED ALWAYS AS IDENTITY, "admin" character varying(50) NOT NULL, "admin_a3" character varying(3) NOT NULL, "type" character varying(50) NOT NULL, "coordinates" polygon NOT NULL);`.execute(db);
|
||||||
|
await sql`CREATE TABLE "partners_audit" ("id" uuid NOT NULL DEFAULT immich_uuid_v7(), "sharedById" uuid NOT NULL, "sharedWithId" uuid NOT NULL, "deletedAt" timestamp with time zone NOT NULL DEFAULT clock_timestamp());`.execute(db);
|
||||||
|
await sql`CREATE TABLE "partners" ("sharedById" uuid NOT NULL, "sharedWithId" uuid NOT NULL, "createdAt" timestamp with time zone NOT NULL DEFAULT now(), "updatedAt" timestamp with time zone NOT NULL DEFAULT now(), "inTimeline" boolean NOT NULL DEFAULT false, "updateId" uuid NOT NULL DEFAULT immich_uuid_v7());`.execute(db);
|
||||||
|
await sql`CREATE TABLE "sessions" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "token" character varying NOT NULL, "createdAt" timestamp with time zone NOT NULL DEFAULT now(), "updatedAt" timestamp with time zone NOT NULL DEFAULT now(), "userId" uuid NOT NULL, "deviceType" character varying NOT NULL DEFAULT '', "deviceOS" character varying NOT NULL DEFAULT '', "updateId" uuid NOT NULL DEFAULT immich_uuid_v7());`.execute(db);
|
||||||
|
await sql`CREATE TABLE "shared_links" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "description" character varying, "userId" uuid NOT NULL, "key" bytea NOT NULL, "type" character varying NOT NULL, "createdAt" timestamp with time zone NOT NULL DEFAULT now(), "expiresAt" timestamp with time zone, "allowUpload" boolean NOT NULL DEFAULT false, "albumId" uuid, "allowDownload" boolean NOT NULL DEFAULT true, "showExif" boolean NOT NULL DEFAULT true, "password" character varying);`.execute(db);
|
||||||
|
await sql`CREATE TABLE "shared_link__asset" ("assetsId" uuid NOT NULL, "sharedLinksId" uuid NOT NULL);`.execute(db);
|
||||||
|
await sql`CREATE TABLE "smart_search" ("assetId" uuid NOT NULL, "embedding" vector(512) NOT NULL);`.execute(db);
|
||||||
|
await sql`ALTER TABLE "smart_search" ALTER COLUMN "embedding" SET STORAGE EXTERNAL;`.execute(db);
|
||||||
|
await sql`CREATE TABLE "session_sync_checkpoints" ("sessionId" uuid NOT NULL, "type" character varying NOT NULL, "createdAt" timestamp with time zone NOT NULL DEFAULT now(), "updatedAt" timestamp with time zone NOT NULL DEFAULT now(), "ack" character varying NOT NULL, "updateId" uuid NOT NULL DEFAULT immich_uuid_v7());`.execute(db);
|
||||||
|
await sql`CREATE TABLE "system_metadata" ("key" character varying NOT NULL, "value" jsonb NOT NULL);`.execute(db);
|
||||||
|
await sql`CREATE TABLE "tags" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "userId" uuid NOT NULL, "value" character varying NOT NULL, "createdAt" timestamp with time zone NOT NULL DEFAULT now(), "updatedAt" timestamp with time zone NOT NULL DEFAULT now(), "color" character varying, "parentId" uuid, "updateId" uuid NOT NULL DEFAULT immich_uuid_v7());`.execute(db);
|
||||||
|
await sql`CREATE TABLE "tag_asset" ("assetsId" uuid NOT NULL, "tagsId" uuid NOT NULL);`.execute(db);
|
||||||
|
await sql`CREATE TABLE "tags_closure" ("id_ancestor" uuid NOT NULL, "id_descendant" uuid NOT NULL);`.execute(db);
|
||||||
|
await sql`CREATE TABLE "users_audit" ("userId" uuid NOT NULL, "deletedAt" timestamp with time zone NOT NULL DEFAULT clock_timestamp(), "id" uuid NOT NULL DEFAULT immich_uuid_v7());`.execute(db);
|
||||||
|
await sql`CREATE TABLE "user_metadata" ("userId" uuid NOT NULL, "key" character varying NOT NULL, "value" jsonb NOT NULL);`.execute(db);
|
||||||
|
await sql`CREATE TABLE "version_history" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "createdAt" timestamp with time zone NOT NULL DEFAULT now(), "version" character varying NOT NULL);`.execute(db);
|
||||||
|
await sql`ALTER TABLE "users" ADD CONSTRAINT "PK_a3ffb1c0c8416b9fc6f907b7433" PRIMARY KEY ("id");`.execute(db);
|
||||||
|
await sql`ALTER TABLE "libraries" ADD CONSTRAINT "PK_505fedfcad00a09b3734b4223de" PRIMARY KEY ("id");`.execute(db);
|
||||||
|
await sql`ALTER TABLE "asset_stack" ADD CONSTRAINT "PK_74a27e7fcbd5852463d0af3034b" PRIMARY KEY ("id");`.execute(db);
|
||||||
|
await sql`ALTER TABLE "assets" ADD CONSTRAINT "PK_da96729a8b113377cfb6a62439c" PRIMARY KEY ("id");`.execute(db);
|
||||||
|
await sql`ALTER TABLE "albums" ADD CONSTRAINT "PK_7f71c7b5bc7c87b8f94c9a93a00" PRIMARY KEY ("id");`.execute(db);
|
||||||
|
await sql`ALTER TABLE "activity" ADD CONSTRAINT "PK_24625a1d6b1b089c8ae206fe467" PRIMARY KEY ("id");`.execute(db);
|
||||||
|
await sql`ALTER TABLE "albums_assets_assets" ADD CONSTRAINT "PK_c67bc36fa845fb7b18e0e398180" PRIMARY KEY ("albumsId", "assetsId");`.execute(db);
|
||||||
|
await sql`ALTER TABLE "albums_shared_users_users" ADD CONSTRAINT "PK_7df55657e0b2e8b626330a0ebc8" PRIMARY KEY ("albumsId", "usersId");`.execute(db);
|
||||||
|
await sql`ALTER TABLE "api_keys" ADD CONSTRAINT "PK_5c8a79801b44bd27b79228e1dad" PRIMARY KEY ("id");`.execute(db);
|
||||||
|
await sql`ALTER TABLE "assets_audit" ADD CONSTRAINT "PK_99bd5c015f81a641927a32b4212" PRIMARY KEY ("id");`.execute(db);
|
||||||
|
await sql`ALTER TABLE "person" ADD CONSTRAINT "PK_5fdaf670315c4b7e70cce85daa3" PRIMARY KEY ("id");`.execute(db);
|
||||||
|
await sql`ALTER TABLE "asset_faces" ADD CONSTRAINT "PK_6df76ab2eb6f5b57b7c2f1fc684" PRIMARY KEY ("id");`.execute(db);
|
||||||
|
await sql`ALTER TABLE "asset_files" ADD CONSTRAINT "PK_c41dc3e9ef5e1c57ca5a08a0004" PRIMARY KEY ("id");`.execute(db);
|
||||||
|
await sql`ALTER TABLE "asset_job_status" ADD CONSTRAINT "PK_420bec36fc02813bddf5c8b73d4" PRIMARY KEY ("assetId");`.execute(db);
|
||||||
|
await sql`ALTER TABLE "audit" ADD CONSTRAINT "PK_1d3d120ddaf7bc9b1ed68ed463a" PRIMARY KEY ("id");`.execute(db);
|
||||||
|
await sql`ALTER TABLE "exif" ADD CONSTRAINT "PK_c0117fdbc50b917ef9067740c44" PRIMARY KEY ("assetId");`.execute(db);
|
||||||
|
await sql`ALTER TABLE "face_search" ADD CONSTRAINT "face_search_pkey" PRIMARY KEY ("faceId");`.execute(db);
|
||||||
|
await sql`ALTER TABLE "geodata_places" ADD CONSTRAINT "PK_c29918988912ef4036f3d7fbff4" PRIMARY KEY ("id");`.execute(db);
|
||||||
|
await sql`ALTER TABLE "memories" ADD CONSTRAINT "PK_aaa0692d9496fe827b0568612f8" PRIMARY KEY ("id");`.execute(db);
|
||||||
|
await sql`ALTER TABLE "memories_assets_assets" ADD CONSTRAINT "PK_fcaf7112a013d1703c011c6793d" PRIMARY KEY ("memoriesId", "assetsId");`.execute(db);
|
||||||
|
await sql`ALTER TABLE "move_history" ADD CONSTRAINT "PK_af608f132233acf123f2949678d" PRIMARY KEY ("id");`.execute(db);
|
||||||
|
await sql`ALTER TABLE "naturalearth_countries" ADD CONSTRAINT "PK_21a6d86d1ab5d841648212e5353" PRIMARY KEY ("id");`.execute(db);
|
||||||
|
await sql`ALTER TABLE "partners_audit" ADD CONSTRAINT "PK_952b50217ff78198a7e380f0359" PRIMARY KEY ("id");`.execute(db);
|
||||||
|
await sql`ALTER TABLE "partners" ADD CONSTRAINT "PK_f1cc8f73d16b367f426261a8736" PRIMARY KEY ("sharedById", "sharedWithId");`.execute(db);
|
||||||
|
await sql`ALTER TABLE "sessions" ADD CONSTRAINT "PK_48cb6b5c20faa63157b3c1baf7f" PRIMARY KEY ("id");`.execute(db);
|
||||||
|
await sql`ALTER TABLE "shared_links" ADD CONSTRAINT "PK_642e2b0f619e4876e5f90a43465" PRIMARY KEY ("id");`.execute(db);
|
||||||
|
await sql`ALTER TABLE "shared_link__asset" ADD CONSTRAINT "PK_9b4f3687f9b31d1e311336b05e3" PRIMARY KEY ("assetsId", "sharedLinksId");`.execute(db);
|
||||||
|
await sql`ALTER TABLE "smart_search" ADD CONSTRAINT "smart_search_pkey" PRIMARY KEY ("assetId");`.execute(db);
|
||||||
|
await sql`ALTER TABLE "session_sync_checkpoints" ADD CONSTRAINT "PK_b846ab547a702863ef7cd9412fb" PRIMARY KEY ("sessionId", "type");`.execute(db);
|
||||||
|
await sql`ALTER TABLE "system_metadata" ADD CONSTRAINT "PK_fa94f6857470fb5b81ec6084465" PRIMARY KEY ("key");`.execute(db);
|
||||||
|
await sql`ALTER TABLE "tags" ADD CONSTRAINT "PK_e7dc17249a1148a1970748eda99" PRIMARY KEY ("id");`.execute(db);
|
||||||
|
await sql`ALTER TABLE "tag_asset" ADD CONSTRAINT "PK_ef5346fe522b5fb3bc96454747e" PRIMARY KEY ("assetsId", "tagsId");`.execute(db);
|
||||||
|
await sql`ALTER TABLE "tags_closure" ADD CONSTRAINT "PK_eab38eb12a3ec6df8376c95477c" PRIMARY KEY ("id_ancestor", "id_descendant");`.execute(db);
|
||||||
|
await sql`ALTER TABLE "users_audit" ADD CONSTRAINT "PK_e9b2bdfd90e7eb5961091175180" PRIMARY KEY ("id");`.execute(db);
|
||||||
|
await sql`ALTER TABLE "user_metadata" ADD CONSTRAINT "PK_5931462150b3438cbc83277fe5a" PRIMARY KEY ("userId", "key");`.execute(db);
|
||||||
|
await sql`ALTER TABLE "version_history" ADD CONSTRAINT "PK_5db259cbb09ce82c0d13cfd1b23" PRIMARY KEY ("id");`.execute(db);
|
||||||
|
await sql`ALTER TABLE "libraries" ADD CONSTRAINT "FK_0f6fc2fb195f24d19b0fb0d57c1" FOREIGN KEY ("ownerId") REFERENCES "users" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db);
|
||||||
|
await sql`ALTER TABLE "asset_stack" ADD CONSTRAINT "FK_91704e101438fd0653f582426dc" FOREIGN KEY ("primaryAssetId") REFERENCES "assets" ("id") ON UPDATE NO ACTION ON DELETE NO ACTION;`.execute(db);
|
||||||
|
await sql`ALTER TABLE "asset_stack" ADD CONSTRAINT "FK_c05079e542fd74de3b5ecb5c1c8" FOREIGN KEY ("ownerId") REFERENCES "users" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db);
|
||||||
|
await sql`ALTER TABLE "assets" ADD CONSTRAINT "FK_2c5ac0d6fb58b238fd2068de67d" FOREIGN KEY ("ownerId") REFERENCES "users" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db);
|
||||||
|
await sql`ALTER TABLE "assets" ADD CONSTRAINT "FK_16294b83fa8c0149719a1f631ef" FOREIGN KEY ("livePhotoVideoId") REFERENCES "assets" ("id") ON UPDATE CASCADE ON DELETE SET NULL;`.execute(db);
|
||||||
|
await sql`ALTER TABLE "assets" ADD CONSTRAINT "FK_9977c3c1de01c3d848039a6b90c" FOREIGN KEY ("libraryId") REFERENCES "libraries" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db);
|
||||||
|
await sql`ALTER TABLE "assets" ADD CONSTRAINT "FK_f15d48fa3ea5e4bda05ca8ab207" FOREIGN KEY ("stackId") REFERENCES "asset_stack" ("id") ON UPDATE CASCADE ON DELETE SET NULL;`.execute(db);
|
||||||
|
await sql`ALTER TABLE "albums" ADD CONSTRAINT "FK_b22c53f35ef20c28c21637c85f4" FOREIGN KEY ("ownerId") REFERENCES "users" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db);
|
||||||
|
await sql`ALTER TABLE "albums" ADD CONSTRAINT "FK_05895aa505a670300d4816debce" FOREIGN KEY ("albumThumbnailAssetId") REFERENCES "assets" ("id") ON UPDATE CASCADE ON DELETE SET NULL;`.execute(db);
|
||||||
|
await sql`ALTER TABLE "activity" ADD CONSTRAINT "FK_1af8519996fbfb3684b58df280b" FOREIGN KEY ("albumId") REFERENCES "albums" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db);
|
||||||
|
await sql`ALTER TABLE "activity" ADD CONSTRAINT "FK_3571467bcbe021f66e2bdce96ea" FOREIGN KEY ("userId") REFERENCES "users" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db);
|
||||||
|
await sql`ALTER TABLE "activity" ADD CONSTRAINT "FK_8091ea76b12338cb4428d33d782" FOREIGN KEY ("assetId") REFERENCES "assets" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db);
|
||||||
|
await sql`ALTER TABLE "albums_assets_assets" ADD CONSTRAINT "FK_e590fa396c6898fcd4a50e40927" FOREIGN KEY ("albumsId") REFERENCES "albums" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db);
|
||||||
|
await sql`ALTER TABLE "albums_assets_assets" ADD CONSTRAINT "FK_4bd1303d199f4e72ccdf998c621" FOREIGN KEY ("assetsId") REFERENCES "assets" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db);
|
||||||
|
await sql`ALTER TABLE "albums_shared_users_users" ADD CONSTRAINT "FK_427c350ad49bd3935a50baab737" FOREIGN KEY ("albumsId") REFERENCES "albums" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db);
|
||||||
|
await sql`ALTER TABLE "albums_shared_users_users" ADD CONSTRAINT "FK_f48513bf9bccefd6ff3ad30bd06" FOREIGN KEY ("usersId") REFERENCES "users" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db);
|
||||||
|
await sql`ALTER TABLE "api_keys" ADD CONSTRAINT "FK_6c2e267ae764a9413b863a29342" FOREIGN KEY ("userId") REFERENCES "users" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db);
|
||||||
|
await sql`ALTER TABLE "person" ADD CONSTRAINT "FK_5527cc99f530a547093f9e577b6" FOREIGN KEY ("ownerId") REFERENCES "users" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db);
|
||||||
|
await sql`ALTER TABLE "person" ADD CONSTRAINT "FK_2bbabe31656b6778c6b87b61023" FOREIGN KEY ("faceAssetId") REFERENCES "asset_faces" ("id") ON UPDATE NO ACTION ON DELETE SET NULL;`.execute(db);
|
||||||
|
await sql`ALTER TABLE "asset_faces" ADD CONSTRAINT "FK_02a43fd0b3c50fb6d7f0cb7282c" FOREIGN KEY ("assetId") REFERENCES "assets" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db);
|
||||||
|
await sql`ALTER TABLE "asset_faces" ADD CONSTRAINT "FK_95ad7106dd7b484275443f580f9" FOREIGN KEY ("personId") REFERENCES "person" ("id") ON UPDATE CASCADE ON DELETE SET NULL;`.execute(db);
|
||||||
|
await sql`ALTER TABLE "asset_files" ADD CONSTRAINT "FK_e3e103a5f1d8bc8402999286040" FOREIGN KEY ("assetId") REFERENCES "assets" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db);
|
||||||
|
await sql`ALTER TABLE "asset_job_status" ADD CONSTRAINT "FK_420bec36fc02813bddf5c8b73d4" FOREIGN KEY ("assetId") REFERENCES "assets" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db);
|
||||||
|
await sql`ALTER TABLE "exif" ADD CONSTRAINT "FK_c0117fdbc50b917ef9067740c44" FOREIGN KEY ("assetId") REFERENCES "assets" ("id") ON UPDATE NO ACTION ON DELETE CASCADE;`.execute(db);
|
||||||
|
await sql`ALTER TABLE "face_search" ADD CONSTRAINT "face_search_faceId_fkey" FOREIGN KEY ("faceId") REFERENCES "asset_faces" ("id") ON UPDATE NO ACTION ON DELETE CASCADE;`.execute(db);
|
||||||
|
await sql`ALTER TABLE "memories" ADD CONSTRAINT "FK_575842846f0c28fa5da46c99b19" FOREIGN KEY ("ownerId") REFERENCES "users" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db);
|
||||||
|
await sql`ALTER TABLE "memories_assets_assets" ADD CONSTRAINT "FK_984e5c9ab1f04d34538cd32334e" FOREIGN KEY ("memoriesId") REFERENCES "memories" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db);
|
||||||
|
await sql`ALTER TABLE "memories_assets_assets" ADD CONSTRAINT "FK_6942ecf52d75d4273de19d2c16f" FOREIGN KEY ("assetsId") REFERENCES "assets" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db);
|
||||||
|
await sql`ALTER TABLE "partners" ADD CONSTRAINT "FK_7e077a8b70b3530138610ff5e04" FOREIGN KEY ("sharedById") REFERENCES "users" ("id") ON UPDATE NO ACTION ON DELETE CASCADE;`.execute(db);
|
||||||
|
await sql`ALTER TABLE "partners" ADD CONSTRAINT "FK_d7e875c6c60e661723dbf372fd3" FOREIGN KEY ("sharedWithId") REFERENCES "users" ("id") ON UPDATE NO ACTION ON DELETE CASCADE;`.execute(db);
|
||||||
|
await sql`ALTER TABLE "sessions" ADD CONSTRAINT "FK_57de40bc620f456c7311aa3a1e6" FOREIGN KEY ("userId") REFERENCES "users" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db);
|
||||||
|
await sql`ALTER TABLE "shared_links" ADD CONSTRAINT "FK_66fe3837414c5a9f1c33ca49340" FOREIGN KEY ("userId") REFERENCES "users" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db);
|
||||||
|
await sql`ALTER TABLE "shared_links" ADD CONSTRAINT "FK_0c6ce9058c29f07cdf7014eac66" FOREIGN KEY ("albumId") REFERENCES "albums" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db);
|
||||||
|
await sql`ALTER TABLE "shared_link__asset" ADD CONSTRAINT "FK_5b7decce6c8d3db9593d6111a66" FOREIGN KEY ("assetsId") REFERENCES "assets" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db);
|
||||||
|
await sql`ALTER TABLE "shared_link__asset" ADD CONSTRAINT "FK_c9fab4aa97ffd1b034f3d6581ab" FOREIGN KEY ("sharedLinksId") REFERENCES "shared_links" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db);
|
||||||
|
await sql`ALTER TABLE "smart_search" ADD CONSTRAINT "smart_search_assetId_fkey" FOREIGN KEY ("assetId") REFERENCES "assets" ("id") ON UPDATE NO ACTION ON DELETE CASCADE;`.execute(db);
|
||||||
|
await sql`ALTER TABLE "session_sync_checkpoints" ADD CONSTRAINT "FK_d8ddd9d687816cc490432b3d4bc" FOREIGN KEY ("sessionId") REFERENCES "sessions" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db);
|
||||||
|
await sql`ALTER TABLE "tags" ADD CONSTRAINT "FK_92e67dc508c705dd66c94615576" FOREIGN KEY ("userId") REFERENCES "users" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db);
|
||||||
|
await sql`ALTER TABLE "tags" ADD CONSTRAINT "FK_9f9590cc11561f1f48ff034ef99" FOREIGN KEY ("parentId") REFERENCES "tags" ("id") ON UPDATE NO ACTION ON DELETE CASCADE;`.execute(db);
|
||||||
|
await sql`ALTER TABLE "tag_asset" ADD CONSTRAINT "FK_f8e8a9e893cb5c54907f1b798e9" FOREIGN KEY ("assetsId") REFERENCES "assets" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db);
|
||||||
|
await sql`ALTER TABLE "tag_asset" ADD CONSTRAINT "FK_e99f31ea4cdf3a2c35c7287eb42" FOREIGN KEY ("tagsId") REFERENCES "tags" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db);
|
||||||
|
await sql`ALTER TABLE "tags_closure" ADD CONSTRAINT "FK_15fbcbc67663c6bfc07b354c22c" FOREIGN KEY ("id_ancestor") REFERENCES "tags" ("id") ON UPDATE NO ACTION ON DELETE CASCADE;`.execute(db);
|
||||||
|
await sql`ALTER TABLE "tags_closure" ADD CONSTRAINT "FK_b1a2a7ed45c29179b5ad51548a1" FOREIGN KEY ("id_descendant") REFERENCES "tags" ("id") ON UPDATE NO ACTION ON DELETE CASCADE;`.execute(db);
|
||||||
|
await sql`ALTER TABLE "user_metadata" ADD CONSTRAINT "FK_6afb43681a21cf7815932bc38ac" FOREIGN KEY ("userId") REFERENCES "users" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db);
|
||||||
|
await sql`ALTER TABLE "users" ADD CONSTRAINT "UQ_97672ac88f789774dd47f7c8be3" UNIQUE ("email");`.execute(db);
|
||||||
|
await sql`ALTER TABLE "users" ADD CONSTRAINT "UQ_b309cf34fa58137c416b32cea3a" UNIQUE ("storageLabel");`.execute(db);
|
||||||
|
await sql`ALTER TABLE "asset_stack" ADD CONSTRAINT "REL_91704e101438fd0653f582426d" UNIQUE ("primaryAssetId");`.execute(db);
|
||||||
|
await sql`ALTER TABLE "asset_files" ADD CONSTRAINT "UQ_assetId_type" UNIQUE ("assetId", "type");`.execute(db);
|
||||||
|
await sql`ALTER TABLE "move_history" ADD CONSTRAINT "UQ_newPath" UNIQUE ("newPath");`.execute(db);
|
||||||
|
await sql`ALTER TABLE "move_history" ADD CONSTRAINT "UQ_entityId_pathType" UNIQUE ("entityId", "pathType");`.execute(db);
|
||||||
|
await sql`ALTER TABLE "shared_links" ADD CONSTRAINT "UQ_sharedlink_key" UNIQUE ("key");`.execute(db);
|
||||||
|
await sql`ALTER TABLE "tags" ADD CONSTRAINT "UQ_79d6f16e52bb2c7130375246793" UNIQUE ("userId", "value");`.execute(db);
|
||||||
|
await sql`ALTER TABLE "activity" ADD CONSTRAINT "CHK_2ab1e70f113f450eb40c1e3ec8" CHECK (("comment" IS NULL AND "isLiked" = true) OR ("comment" IS NOT NULL AND "isLiked" = false));`.execute(db);
|
||||||
|
await sql`ALTER TABLE "person" ADD CONSTRAINT "CHK_b0f82b0ed662bfc24fbb58bb45" CHECK ("birthDate" <= CURRENT_DATE);`.execute(db);
|
||||||
|
await sql`CREATE INDEX "IDX_users_updated_at_asc_id_asc" ON "users" ("updatedAt", "id")`.execute(db);
|
||||||
|
await sql`CREATE INDEX "IDX_users_update_id" ON "users" ("updateId")`.execute(db);
|
||||||
|
await sql`CREATE INDEX "IDX_0f6fc2fb195f24d19b0fb0d57c" ON "libraries" ("ownerId")`.execute(db);
|
||||||
|
await sql`CREATE INDEX "IDX_libraries_update_id" ON "libraries" ("updateId")`.execute(db);
|
||||||
|
await sql`CREATE INDEX "IDX_91704e101438fd0653f582426d" ON "asset_stack" ("primaryAssetId")`.execute(db);
|
||||||
|
await sql`CREATE INDEX "IDX_c05079e542fd74de3b5ecb5c1c" ON "asset_stack" ("ownerId")`.execute(db);
|
||||||
|
await sql`CREATE INDEX "idx_originalfilename_trigram" ON "assets" USING gin (f_unaccent("originalFileName") gin_trgm_ops)`.execute(db);
|
||||||
|
await sql`CREATE INDEX "IDX_asset_id_stackId" ON "assets" ("id", "stackId")`.execute(db);
|
||||||
|
await sql`CREATE INDEX "IDX_originalPath_libraryId" ON "assets" ("originalPath", "libraryId")`.execute(db);
|
||||||
|
await sql`CREATE INDEX "idx_local_date_time_month" ON "assets" ((date_trunc('MONTH'::text, ("localDateTime" AT TIME ZONE 'UTC'::text)) AT TIME ZONE 'UTC'::text))`.execute(db);
|
||||||
|
await sql`CREATE INDEX "idx_local_date_time" ON "assets" ((("localDateTime" at time zone 'UTC')::date))`.execute(db);
|
||||||
|
await sql`CREATE UNIQUE INDEX "UQ_assets_owner_library_checksum" ON "assets" ("ownerId", "libraryId", "checksum") WHERE ("libraryId" IS NOT NULL)`.execute(db);
|
||||||
|
await sql`CREATE UNIQUE INDEX "UQ_assets_owner_checksum" ON "assets" ("ownerId", "checksum") WHERE ("libraryId" IS NULL)`.execute(db);
|
||||||
|
await sql`CREATE INDEX "IDX_2c5ac0d6fb58b238fd2068de67" ON "assets" ("ownerId")`.execute(db);
|
||||||
|
await sql`CREATE INDEX "idx_asset_file_created_at" ON "assets" ("fileCreatedAt")`.execute(db);
|
||||||
|
await sql`CREATE INDEX "IDX_8d3efe36c0755849395e6ea866" ON "assets" ("checksum")`.execute(db);
|
||||||
|
await sql`CREATE INDEX "IDX_16294b83fa8c0149719a1f631e" ON "assets" ("livePhotoVideoId")`.execute(db);
|
||||||
|
await sql`CREATE INDEX "IDX_4d66e76dada1ca180f67a205dc" ON "assets" ("originalFileName")`.execute(db);
|
||||||
|
await sql`CREATE INDEX "IDX_9977c3c1de01c3d848039a6b90" ON "assets" ("libraryId")`.execute(db);
|
||||||
|
await sql`CREATE INDEX "IDX_f15d48fa3ea5e4bda05ca8ab20" ON "assets" ("stackId")`.execute(db);
|
||||||
|
await sql`CREATE INDEX "IDX_assets_duplicateId" ON "assets" ("duplicateId")`.execute(db);
|
||||||
|
await sql`CREATE INDEX "IDX_assets_update_id" ON "assets" ("updateId")`.execute(db);
|
||||||
|
await sql`CREATE INDEX "IDX_b22c53f35ef20c28c21637c85f" ON "albums" ("ownerId")`.execute(db);
|
||||||
|
await sql`CREATE INDEX "IDX_05895aa505a670300d4816debc" ON "albums" ("albumThumbnailAssetId")`.execute(db);
|
||||||
|
await sql`CREATE INDEX "IDX_albums_update_id" ON "albums" ("updateId")`.execute(db);
|
||||||
|
await sql`CREATE UNIQUE INDEX "IDX_activity_like" ON "activity" ("assetId", "userId", "albumId") WHERE ("isLiked" = true)`.execute(db);
|
||||||
|
await sql`CREATE INDEX "IDX_1af8519996fbfb3684b58df280" ON "activity" ("albumId")`.execute(db);
|
||||||
|
await sql`CREATE INDEX "IDX_3571467bcbe021f66e2bdce96e" ON "activity" ("userId")`.execute(db);
|
||||||
|
await sql`CREATE INDEX "IDX_8091ea76b12338cb4428d33d78" ON "activity" ("assetId")`.execute(db);
|
||||||
|
await sql`CREATE INDEX "IDX_activity_update_id" ON "activity" ("updateId")`.execute(db);
|
||||||
|
await sql`CREATE INDEX "IDX_e590fa396c6898fcd4a50e4092" ON "albums_assets_assets" ("albumsId")`.execute(db);
|
||||||
|
await sql`CREATE INDEX "IDX_4bd1303d199f4e72ccdf998c62" ON "albums_assets_assets" ("assetsId")`.execute(db);
|
||||||
|
await sql`CREATE INDEX "IDX_f48513bf9bccefd6ff3ad30bd0" ON "albums_shared_users_users" ("usersId")`.execute(db);
|
||||||
|
await sql`CREATE INDEX "IDX_427c350ad49bd3935a50baab73" ON "albums_shared_users_users" ("albumsId")`.execute(db);
|
||||||
|
await sql`CREATE INDEX "IDX_6c2e267ae764a9413b863a2934" ON "api_keys" ("userId")`.execute(db);
|
||||||
|
await sql`CREATE INDEX "IDX_api_keys_update_id" ON "api_keys" ("updateId")`.execute(db);
|
||||||
|
await sql`CREATE INDEX "IDX_assets_audit_asset_id" ON "assets_audit" ("assetId")`.execute(db);
|
||||||
|
await sql`CREATE INDEX "IDX_assets_audit_owner_id" ON "assets_audit" ("ownerId")`.execute(db);
|
||||||
|
await sql`CREATE INDEX "IDX_assets_audit_deleted_at" ON "assets_audit" ("deletedAt")`.execute(db);
|
||||||
|
await sql`CREATE INDEX "IDX_5527cc99f530a547093f9e577b" ON "person" ("ownerId")`.execute(db);
|
||||||
|
await sql`CREATE INDEX "IDX_2bbabe31656b6778c6b87b6102" ON "person" ("faceAssetId")`.execute(db);
|
||||||
|
await sql`CREATE INDEX "IDX_person_update_id" ON "person" ("updateId")`.execute(db);
|
||||||
|
await sql`CREATE INDEX "IDX_bf339a24070dac7e71304ec530" ON "asset_faces" ("personId", "assetId")`.execute(db);
|
||||||
|
await sql`CREATE INDEX "IDX_asset_faces_assetId_personId" ON "asset_faces" ("assetId", "personId")`.execute(db);
|
||||||
|
await sql`CREATE INDEX "IDX_02a43fd0b3c50fb6d7f0cb7282" ON "asset_faces" ("assetId")`.execute(db);
|
||||||
|
await sql`CREATE INDEX "IDX_95ad7106dd7b484275443f580f" ON "asset_faces" ("personId")`.execute(db);
|
||||||
|
await sql`CREATE INDEX "IDX_asset_files_assetId" ON "asset_files" ("assetId")`.execute(db);
|
||||||
|
await sql`CREATE INDEX "IDX_asset_files_update_id" ON "asset_files" ("updateId")`.execute(db);
|
||||||
|
await sql`CREATE INDEX "IDX_ownerId_createdAt" ON "audit" ("ownerId", "createdAt")`.execute(db);
|
||||||
|
await sql`CREATE INDEX "exif_city" ON "exif" ("city")`.execute(db);
|
||||||
|
await sql`CREATE INDEX "IDX_live_photo_cid" ON "exif" ("livePhotoCID")`.execute(db);
|
||||||
|
await sql`CREATE INDEX "IDX_auto_stack_id" ON "exif" ("autoStackId")`.execute(db);
|
||||||
|
await sql`CREATE INDEX "IDX_asset_exif_update_id" ON "exif" ("updateId")`.execute(db);
|
||||||
|
await sql`CREATE INDEX "face_index" ON "face_search" USING hnsw (embedding vector_cosine_ops) WITH (ef_construction = 300, m = 16)`.execute(db);
|
||||||
|
await sql`CREATE INDEX "IDX_geodata_gist_earthcoord" ON "geodata_places" (ll_to_earth_public(latitude, longitude))`.execute(db);
|
||||||
|
await sql`CREATE INDEX "idx_geodata_places_name" ON "geodata_places" USING gin (f_unaccent("name") gin_trgm_ops)`.execute(db);
|
||||||
|
await sql`CREATE INDEX "idx_geodata_places_admin2_name" ON "geodata_places" USING gin (f_unaccent("admin2Name") gin_trgm_ops)`.execute(db);
|
||||||
|
await sql`CREATE INDEX "idx_geodata_places_admin1_name" ON "geodata_places" USING gin (f_unaccent("admin1Name") gin_trgm_ops)`.execute(db);
|
||||||
|
await sql`CREATE INDEX "idx_geodata_places_alternate_names" ON "geodata_places" USING gin (f_unaccent("alternateNames") gin_trgm_ops)`.execute(db);
|
||||||
|
await sql`CREATE INDEX "IDX_575842846f0c28fa5da46c99b1" ON "memories" ("ownerId")`.execute(db);
|
||||||
|
await sql`CREATE INDEX "IDX_memories_update_id" ON "memories" ("updateId")`.execute(db);
|
||||||
|
await sql`CREATE INDEX "IDX_984e5c9ab1f04d34538cd32334" ON "memories_assets_assets" ("memoriesId")`.execute(db);
|
||||||
|
await sql`CREATE INDEX "IDX_6942ecf52d75d4273de19d2c16" ON "memories_assets_assets" ("assetsId")`.execute(db);
|
||||||
|
await sql`CREATE INDEX "IDX_partners_audit_shared_by_id" ON "partners_audit" ("sharedById")`.execute(db);
|
||||||
|
await sql`CREATE INDEX "IDX_partners_audit_shared_with_id" ON "partners_audit" ("sharedWithId")`.execute(db);
|
||||||
|
await sql`CREATE INDEX "IDX_partners_audit_deleted_at" ON "partners_audit" ("deletedAt")`.execute(db);
|
||||||
|
await sql`CREATE INDEX "IDX_7e077a8b70b3530138610ff5e0" ON "partners" ("sharedById")`.execute(db);
|
||||||
|
await sql`CREATE INDEX "IDX_d7e875c6c60e661723dbf372fd" ON "partners" ("sharedWithId")`.execute(db);
|
||||||
|
await sql`CREATE INDEX "IDX_partners_update_id" ON "partners" ("updateId")`.execute(db);
|
||||||
|
await sql`CREATE INDEX "IDX_57de40bc620f456c7311aa3a1e" ON "sessions" ("userId")`.execute(db);
|
||||||
|
await sql`CREATE INDEX "IDX_sessions_update_id" ON "sessions" ("updateId")`.execute(db);
|
||||||
|
await sql`CREATE INDEX "IDX_66fe3837414c5a9f1c33ca4934" ON "shared_links" ("userId")`.execute(db);
|
||||||
|
await sql`CREATE INDEX "IDX_sharedlink_key" ON "shared_links" ("key")`.execute(db);
|
||||||
|
await sql`CREATE INDEX "IDX_sharedlink_albumId" ON "shared_links" ("albumId")`.execute(db);
|
||||||
|
await sql`CREATE INDEX "IDX_5b7decce6c8d3db9593d6111a6" ON "shared_link__asset" ("assetsId")`.execute(db);
|
||||||
|
await sql`CREATE INDEX "IDX_c9fab4aa97ffd1b034f3d6581a" ON "shared_link__asset" ("sharedLinksId")`.execute(db);
|
||||||
|
await sql`CREATE INDEX "clip_index" ON "smart_search" USING hnsw (embedding vector_cosine_ops) WITH (ef_construction = 300, m = 16)`.execute(db);
|
||||||
|
await sql`CREATE INDEX "IDX_d8ddd9d687816cc490432b3d4b" ON "session_sync_checkpoints" ("sessionId")`.execute(db);
|
||||||
|
await sql`CREATE INDEX "IDX_session_sync_checkpoints_update_id" ON "session_sync_checkpoints" ("updateId")`.execute(db);
|
||||||
|
await sql`CREATE INDEX "IDX_92e67dc508c705dd66c9461557" ON "tags" ("userId")`.execute(db);
|
||||||
|
await sql`CREATE INDEX "IDX_9f9590cc11561f1f48ff034ef9" ON "tags" ("parentId")`.execute(db);
|
||||||
|
await sql`CREATE INDEX "IDX_tags_update_id" ON "tags" ("updateId")`.execute(db);
|
||||||
|
await sql`CREATE INDEX "IDX_tag_asset_assetsId_tagsId" ON "tag_asset" ("assetsId", "tagsId")`.execute(db);
|
||||||
|
await sql`CREATE INDEX "IDX_f8e8a9e893cb5c54907f1b798e" ON "tag_asset" ("assetsId")`.execute(db);
|
||||||
|
await sql`CREATE INDEX "IDX_e99f31ea4cdf3a2c35c7287eb4" ON "tag_asset" ("tagsId")`.execute(db);
|
||||||
|
await sql`CREATE INDEX "IDX_15fbcbc67663c6bfc07b354c22" ON "tags_closure" ("id_ancestor")`.execute(db);
|
||||||
|
await sql`CREATE INDEX "IDX_b1a2a7ed45c29179b5ad51548a" ON "tags_closure" ("id_descendant")`.execute(db);
|
||||||
|
await sql`CREATE INDEX "IDX_users_audit_deleted_at" ON "users_audit" ("deletedAt")`.execute(db);
|
||||||
|
await sql`CREATE INDEX "IDX_6afb43681a21cf7815932bc38a" ON "user_metadata" ("userId")`.execute(db);
|
||||||
|
await sql`CREATE OR REPLACE TRIGGER "users_delete_audit"
|
||||||
|
AFTER DELETE ON "users"
|
||||||
|
REFERENCING OLD TABLE AS "old"
|
||||||
|
FOR EACH STATEMENT
|
||||||
|
WHEN (pg_trigger_depth() = 0)
|
||||||
|
EXECUTE FUNCTION users_delete_audit();`.execute(db);
|
||||||
|
await sql`CREATE OR REPLACE TRIGGER "users_updated_at"
|
||||||
|
BEFORE UPDATE ON "users"
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION updated_at();`.execute(db);
|
||||||
|
await sql`CREATE OR REPLACE TRIGGER "libraries_updated_at"
|
||||||
|
BEFORE UPDATE ON "libraries"
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION updated_at();`.execute(db);
|
||||||
|
await sql`CREATE OR REPLACE TRIGGER "assets_delete_audit"
|
||||||
|
AFTER DELETE ON "assets"
|
||||||
|
REFERENCING OLD TABLE AS "old"
|
||||||
|
FOR EACH STATEMENT
|
||||||
|
WHEN (pg_trigger_depth() = 0)
|
||||||
|
EXECUTE FUNCTION assets_delete_audit();`.execute(db);
|
||||||
|
await sql`CREATE OR REPLACE TRIGGER "assets_updated_at"
|
||||||
|
BEFORE UPDATE ON "assets"
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION updated_at();`.execute(db);
|
||||||
|
await sql`CREATE OR REPLACE TRIGGER "albums_updated_at"
|
||||||
|
BEFORE UPDATE ON "albums"
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION updated_at();`.execute(db);
|
||||||
|
await sql`CREATE OR REPLACE TRIGGER "activity_updated_at"
|
||||||
|
BEFORE UPDATE ON "activity"
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION updated_at();`.execute(db);
|
||||||
|
await sql`CREATE OR REPLACE TRIGGER "api_keys_updated_at"
|
||||||
|
BEFORE UPDATE ON "api_keys"
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION updated_at();`.execute(db);
|
||||||
|
await sql`CREATE OR REPLACE TRIGGER "person_updated_at"
|
||||||
|
BEFORE UPDATE ON "person"
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION updated_at();`.execute(db);
|
||||||
|
await sql`CREATE OR REPLACE TRIGGER "asset_files_updated_at"
|
||||||
|
BEFORE UPDATE ON "asset_files"
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION updated_at();`.execute(db);
|
||||||
|
await sql`CREATE OR REPLACE TRIGGER "asset_exif_updated_at"
|
||||||
|
BEFORE UPDATE ON "exif"
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION updated_at();`.execute(db);
|
||||||
|
await sql`CREATE OR REPLACE TRIGGER "memories_updated_at"
|
||||||
|
BEFORE UPDATE ON "memories"
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION updated_at();`.execute(db);
|
||||||
|
await sql`CREATE OR REPLACE TRIGGER "partners_delete_audit"
|
||||||
|
AFTER DELETE ON "partners"
|
||||||
|
REFERENCING OLD TABLE AS "old"
|
||||||
|
FOR EACH STATEMENT
|
||||||
|
WHEN (pg_trigger_depth() = 0)
|
||||||
|
EXECUTE FUNCTION partners_delete_audit();`.execute(db);
|
||||||
|
await sql`CREATE OR REPLACE TRIGGER "partners_updated_at"
|
||||||
|
BEFORE UPDATE ON "partners"
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION updated_at();`.execute(db);
|
||||||
|
await sql`CREATE OR REPLACE TRIGGER "sessions_updated_at"
|
||||||
|
BEFORE UPDATE ON "sessions"
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION updated_at();`.execute(db);
|
||||||
|
await sql`CREATE OR REPLACE TRIGGER "session_sync_checkpoints_updated_at"
|
||||||
|
BEFORE UPDATE ON "session_sync_checkpoints"
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION updated_at();`.execute(db);
|
||||||
|
await sql`CREATE OR REPLACE TRIGGER "tags_updated_at"
|
||||||
|
BEFORE UPDATE ON "tags"
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION updated_at();`.execute(db);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(): Promise<void> {
|
||||||
|
// not implemented
|
||||||
|
}
|
@ -5,7 +5,6 @@ import { UserTable } from 'src/schema/tables/user.table';
|
|||||||
import {
|
import {
|
||||||
Check,
|
Check,
|
||||||
Column,
|
Column,
|
||||||
ColumnIndex,
|
|
||||||
CreateDateColumn,
|
CreateDateColumn,
|
||||||
ForeignKeyColumn,
|
ForeignKeyColumn,
|
||||||
Index,
|
Index,
|
||||||
@ -51,7 +50,6 @@ export class ActivityTable {
|
|||||||
@Column({ type: 'boolean', default: false })
|
@Column({ type: 'boolean', default: false })
|
||||||
isLiked!: boolean;
|
isLiked!: boolean;
|
||||||
|
|
||||||
@ColumnIndex('IDX_activity_update_id')
|
@UpdateIdColumn({ indexName: 'IDX_activity_update_id' })
|
||||||
@UpdateIdColumn()
|
|
||||||
updateId!: string;
|
updateId!: string;
|
||||||
}
|
}
|
||||||
|
@ -1,25 +1,13 @@
|
|||||||
import { AlbumTable } from 'src/schema/tables/album.table';
|
import { AlbumTable } from 'src/schema/tables/album.table';
|
||||||
import { AssetTable } from 'src/schema/tables/asset.table';
|
import { AssetTable } from 'src/schema/tables/asset.table';
|
||||||
import { ColumnIndex, CreateDateColumn, ForeignKeyColumn, Table } from 'src/sql-tools';
|
import { CreateDateColumn, ForeignKeyColumn, Table } from 'src/sql-tools';
|
||||||
|
|
||||||
@Table({ name: 'albums_assets_assets', primaryConstraintName: 'PK_c67bc36fa845fb7b18e0e398180' })
|
@Table({ name: 'albums_assets_assets', primaryConstraintName: 'PK_c67bc36fa845fb7b18e0e398180' })
|
||||||
export class AlbumAssetTable {
|
export class AlbumAssetTable {
|
||||||
@ForeignKeyColumn(() => AlbumTable, {
|
@ForeignKeyColumn(() => AlbumTable, { onDelete: 'CASCADE', onUpdate: 'CASCADE', nullable: false, primary: true })
|
||||||
onDelete: 'CASCADE',
|
|
||||||
onUpdate: 'CASCADE',
|
|
||||||
nullable: false,
|
|
||||||
primary: true,
|
|
||||||
})
|
|
||||||
@ColumnIndex()
|
|
||||||
albumsId!: string;
|
albumsId!: string;
|
||||||
|
|
||||||
@ForeignKeyColumn(() => AssetTable, {
|
@ForeignKeyColumn(() => AssetTable, { onDelete: 'CASCADE', onUpdate: 'CASCADE', nullable: false, primary: true })
|
||||||
onDelete: 'CASCADE',
|
|
||||||
onUpdate: 'CASCADE',
|
|
||||||
nullable: false,
|
|
||||||
primary: true,
|
|
||||||
})
|
|
||||||
@ColumnIndex()
|
|
||||||
assetsId!: string;
|
assetsId!: string;
|
||||||
|
|
||||||
@CreateDateColumn()
|
@CreateDateColumn()
|
||||||
|
@ -4,7 +4,6 @@ import { AssetTable } from 'src/schema/tables/asset.table';
|
|||||||
import { UserTable } from 'src/schema/tables/user.table';
|
import { UserTable } from 'src/schema/tables/user.table';
|
||||||
import {
|
import {
|
||||||
Column,
|
Column,
|
||||||
ColumnIndex,
|
|
||||||
CreateDateColumn,
|
CreateDateColumn,
|
||||||
DeleteDateColumn,
|
DeleteDateColumn,
|
||||||
ForeignKeyColumn,
|
ForeignKeyColumn,
|
||||||
@ -51,7 +50,6 @@ export class AlbumTable {
|
|||||||
@Column({ default: AssetOrder.DESC })
|
@Column({ default: AssetOrder.DESC })
|
||||||
order!: AssetOrder;
|
order!: AssetOrder;
|
||||||
|
|
||||||
@ColumnIndex('IDX_albums_update_id')
|
@UpdateIdColumn({ indexName: 'IDX_albums_update_id' })
|
||||||
@UpdateIdColumn()
|
|
||||||
updateId?: string;
|
updateId?: string;
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,6 @@ import { Permission } from 'src/enum';
|
|||||||
import { UserTable } from 'src/schema/tables/user.table';
|
import { UserTable } from 'src/schema/tables/user.table';
|
||||||
import {
|
import {
|
||||||
Column,
|
Column,
|
||||||
ColumnIndex,
|
|
||||||
CreateDateColumn,
|
CreateDateColumn,
|
||||||
ForeignKeyColumn,
|
ForeignKeyColumn,
|
||||||
PrimaryGeneratedColumn,
|
PrimaryGeneratedColumn,
|
||||||
@ -35,7 +34,6 @@ export class APIKeyTable {
|
|||||||
@Column({ array: true, type: 'character varying' })
|
@Column({ array: true, type: 'character varying' })
|
||||||
permissions!: Permission[];
|
permissions!: Permission[];
|
||||||
|
|
||||||
@ColumnIndex({ name: 'IDX_api_keys_update_id' })
|
@UpdateIdColumn({ indexName: 'IDX_api_keys_update_id' })
|
||||||
@UpdateIdColumn()
|
|
||||||
updateId?: string;
|
updateId?: string;
|
||||||
}
|
}
|
||||||
|
@ -1,20 +1,17 @@
|
|||||||
import { PrimaryGeneratedUuidV7Column } from 'src/decorators';
|
import { PrimaryGeneratedUuidV7Column } from 'src/decorators';
|
||||||
import { Column, ColumnIndex, CreateDateColumn, Table } from 'src/sql-tools';
|
import { Column, CreateDateColumn, Table } from 'src/sql-tools';
|
||||||
|
|
||||||
@Table('assets_audit')
|
@Table('assets_audit')
|
||||||
export class AssetAuditTable {
|
export class AssetAuditTable {
|
||||||
@PrimaryGeneratedUuidV7Column()
|
@PrimaryGeneratedUuidV7Column()
|
||||||
id!: string;
|
id!: string;
|
||||||
|
|
||||||
@ColumnIndex('IDX_assets_audit_asset_id')
|
@Column({ type: 'uuid', indexName: 'IDX_assets_audit_asset_id' })
|
||||||
@Column({ type: 'uuid' })
|
|
||||||
assetId!: string;
|
assetId!: string;
|
||||||
|
|
||||||
@ColumnIndex('IDX_assets_audit_owner_id')
|
@Column({ type: 'uuid', indexName: 'IDX_assets_audit_owner_id' })
|
||||||
@Column({ type: 'uuid' })
|
|
||||||
ownerId!: string;
|
ownerId!: string;
|
||||||
|
|
||||||
@ColumnIndex('IDX_assets_audit_deleted_at')
|
@CreateDateColumn({ default: () => 'clock_timestamp()', indexName: 'IDX_assets_audit_deleted_at' })
|
||||||
@CreateDateColumn({ default: () => 'clock_timestamp()' })
|
|
||||||
deletedAt!: Date;
|
deletedAt!: Date;
|
||||||
}
|
}
|
||||||
|
@ -8,10 +8,21 @@ import { Column, DeleteDateColumn, ForeignKeyColumn, Index, PrimaryGeneratedColu
|
|||||||
@Index({ name: 'IDX_asset_faces_assetId_personId', columns: ['assetId', 'personId'] })
|
@Index({ name: 'IDX_asset_faces_assetId_personId', columns: ['assetId', 'personId'] })
|
||||||
@Index({ columns: ['personId', 'assetId'] })
|
@Index({ columns: ['personId', 'assetId'] })
|
||||||
export class AssetFaceTable {
|
export class AssetFaceTable {
|
||||||
@ForeignKeyColumn(() => AssetTable, { onDelete: 'CASCADE', onUpdate: 'CASCADE' })
|
@ForeignKeyColumn(() => AssetTable, {
|
||||||
|
onDelete: 'CASCADE',
|
||||||
|
onUpdate: 'CASCADE',
|
||||||
|
// [assetId, personId] is the PK constraint
|
||||||
|
index: false,
|
||||||
|
})
|
||||||
assetId!: string;
|
assetId!: string;
|
||||||
|
|
||||||
@ForeignKeyColumn(() => PersonTable, { onDelete: 'SET NULL', onUpdate: 'CASCADE', nullable: true })
|
@ForeignKeyColumn(() => PersonTable, {
|
||||||
|
onDelete: 'SET NULL',
|
||||||
|
onUpdate: 'CASCADE',
|
||||||
|
nullable: true,
|
||||||
|
// [personId, assetId] makes this redundant
|
||||||
|
index: false,
|
||||||
|
})
|
||||||
personId!: string | null;
|
personId!: string | null;
|
||||||
|
|
||||||
@Column({ default: 0, type: 'integer' })
|
@Column({ default: 0, type: 'integer' })
|
||||||
|
@ -3,7 +3,6 @@ import { AssetFileType } from 'src/enum';
|
|||||||
import { AssetTable } from 'src/schema/tables/asset.table';
|
import { AssetTable } from 'src/schema/tables/asset.table';
|
||||||
import {
|
import {
|
||||||
Column,
|
Column,
|
||||||
ColumnIndex,
|
|
||||||
CreateDateColumn,
|
CreateDateColumn,
|
||||||
ForeignKeyColumn,
|
ForeignKeyColumn,
|
||||||
PrimaryGeneratedColumn,
|
PrimaryGeneratedColumn,
|
||||||
@ -19,8 +18,11 @@ export class AssetFileTable {
|
|||||||
@PrimaryGeneratedColumn()
|
@PrimaryGeneratedColumn()
|
||||||
id!: string;
|
id!: string;
|
||||||
|
|
||||||
@ColumnIndex('IDX_asset_files_assetId')
|
@ForeignKeyColumn(() => AssetTable, {
|
||||||
@ForeignKeyColumn(() => AssetTable, { onDelete: 'CASCADE', onUpdate: 'CASCADE' })
|
onDelete: 'CASCADE',
|
||||||
|
onUpdate: 'CASCADE',
|
||||||
|
indexName: 'IDX_asset_files_assetId',
|
||||||
|
})
|
||||||
assetId?: string;
|
assetId?: string;
|
||||||
|
|
||||||
@CreateDateColumn()
|
@CreateDateColumn()
|
||||||
@ -35,7 +37,6 @@ export class AssetFileTable {
|
|||||||
@Column()
|
@Column()
|
||||||
path!: string;
|
path!: string;
|
||||||
|
|
||||||
@ColumnIndex('IDX_asset_files_update_id')
|
@UpdateIdColumn({ indexName: 'IDX_asset_files_update_id' })
|
||||||
@UpdateIdColumn()
|
|
||||||
updateId?: string;
|
updateId?: string;
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators';
|
import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators';
|
||||||
import { ASSET_CHECKSUM_CONSTRAINT } from 'src/entities/asset.entity';
|
|
||||||
import { AssetStatus, AssetType } from 'src/enum';
|
import { AssetStatus, AssetType } from 'src/enum';
|
||||||
import { assets_status_enum } from 'src/schema/enums';
|
import { assets_status_enum } from 'src/schema/enums';
|
||||||
import { assets_delete_audit } from 'src/schema/functions';
|
import { assets_delete_audit } from 'src/schema/functions';
|
||||||
@ -9,7 +8,6 @@ import { UserTable } from 'src/schema/tables/user.table';
|
|||||||
import {
|
import {
|
||||||
AfterDeleteTrigger,
|
AfterDeleteTrigger,
|
||||||
Column,
|
Column,
|
||||||
ColumnIndex,
|
|
||||||
CreateDateColumn,
|
CreateDateColumn,
|
||||||
DeleteDateColumn,
|
DeleteDateColumn,
|
||||||
ForeignKeyColumn,
|
ForeignKeyColumn,
|
||||||
@ -18,6 +16,7 @@ import {
|
|||||||
Table,
|
Table,
|
||||||
UpdateDateColumn,
|
UpdateDateColumn,
|
||||||
} from 'src/sql-tools';
|
} from 'src/sql-tools';
|
||||||
|
import { ASSET_CHECKSUM_CONSTRAINT } from 'src/utils/database';
|
||||||
|
|
||||||
@Table('assets')
|
@Table('assets')
|
||||||
@UpdatedAtTrigger('assets_updated_at')
|
@UpdatedAtTrigger('assets_updated_at')
|
||||||
@ -78,8 +77,7 @@ export class AssetTable {
|
|||||||
@Column()
|
@Column()
|
||||||
originalPath!: string;
|
originalPath!: string;
|
||||||
|
|
||||||
@ColumnIndex('idx_asset_file_created_at')
|
@Column({ type: 'timestamp with time zone', indexName: 'idx_asset_file_created_at' })
|
||||||
@Column({ type: 'timestamp with time zone' })
|
|
||||||
fileCreatedAt!: Date;
|
fileCreatedAt!: Date;
|
||||||
|
|
||||||
@Column({ type: 'timestamp with time zone' })
|
@Column({ type: 'timestamp with time zone' })
|
||||||
@ -94,8 +92,7 @@ export class AssetTable {
|
|||||||
@Column({ type: 'character varying', nullable: true, default: '' })
|
@Column({ type: 'character varying', nullable: true, default: '' })
|
||||||
encodedVideoPath!: string | null;
|
encodedVideoPath!: string | null;
|
||||||
|
|
||||||
@Column({ type: 'bytea' })
|
@Column({ type: 'bytea', index: true })
|
||||||
@ColumnIndex()
|
|
||||||
checksum!: Buffer; // sha1 checksum
|
checksum!: Buffer; // sha1 checksum
|
||||||
|
|
||||||
@Column({ type: 'boolean', default: true })
|
@Column({ type: 'boolean', default: true })
|
||||||
@ -113,8 +110,7 @@ export class AssetTable {
|
|||||||
@Column({ type: 'boolean', default: false })
|
@Column({ type: 'boolean', default: false })
|
||||||
isArchived!: boolean;
|
isArchived!: boolean;
|
||||||
|
|
||||||
@Column()
|
@Column({ index: true })
|
||||||
@ColumnIndex()
|
|
||||||
originalFileName!: string;
|
originalFileName!: string;
|
||||||
|
|
||||||
@Column({ nullable: true })
|
@Column({ nullable: true })
|
||||||
@ -141,14 +137,12 @@ export class AssetTable {
|
|||||||
@ForeignKeyColumn(() => StackTable, { nullable: true, onDelete: 'SET NULL', onUpdate: 'CASCADE' })
|
@ForeignKeyColumn(() => StackTable, { nullable: true, onDelete: 'SET NULL', onUpdate: 'CASCADE' })
|
||||||
stackId?: string | null;
|
stackId?: string | null;
|
||||||
|
|
||||||
@ColumnIndex('IDX_assets_duplicateId')
|
@Column({ type: 'uuid', nullable: true, indexName: 'IDX_assets_duplicateId' })
|
||||||
@Column({ type: 'uuid', nullable: true })
|
|
||||||
duplicateId!: string | null;
|
duplicateId!: string | null;
|
||||||
|
|
||||||
@Column({ enum: assets_status_enum, default: AssetStatus.ACTIVE })
|
@Column({ enum: assets_status_enum, default: AssetStatus.ACTIVE })
|
||||||
status!: AssetStatus;
|
status!: AssetStatus;
|
||||||
|
|
||||||
@ColumnIndex('IDX_assets_update_id')
|
@UpdateIdColumn({ indexName: 'IDX_assets_update_id' })
|
||||||
@UpdateIdColumn()
|
|
||||||
updateId?: string;
|
updateId?: string;
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators';
|
import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators';
|
||||||
import { AssetTable } from 'src/schema/tables/asset.table';
|
import { AssetTable } from 'src/schema/tables/asset.table';
|
||||||
import { Column, ColumnIndex, ForeignKeyColumn, Table, UpdateDateColumn } from 'src/sql-tools';
|
import { Column, ForeignKeyColumn, Table, UpdateDateColumn } from 'src/sql-tools';
|
||||||
|
|
||||||
@Table('exif')
|
@Table('exif')
|
||||||
@UpdatedAtTrigger('asset_exif_updated_at')
|
@UpdatedAtTrigger('asset_exif_updated_at')
|
||||||
@ -50,8 +50,7 @@ export class ExifTable {
|
|||||||
@Column({ type: 'double precision', nullable: true })
|
@Column({ type: 'double precision', nullable: true })
|
||||||
longitude!: number | null;
|
longitude!: number | null;
|
||||||
|
|
||||||
@ColumnIndex('exif_city')
|
@Column({ type: 'character varying', nullable: true, indexName: 'exif_city' })
|
||||||
@Column({ type: 'character varying', nullable: true })
|
|
||||||
city!: string | null;
|
city!: string | null;
|
||||||
|
|
||||||
@Column({ type: 'character varying', nullable: true })
|
@Column({ type: 'character varying', nullable: true })
|
||||||
@ -69,8 +68,7 @@ export class ExifTable {
|
|||||||
@Column({ type: 'character varying', nullable: true })
|
@Column({ type: 'character varying', nullable: true })
|
||||||
exposureTime!: string | null;
|
exposureTime!: string | null;
|
||||||
|
|
||||||
@ColumnIndex('IDX_live_photo_cid')
|
@Column({ type: 'character varying', nullable: true, indexName: 'IDX_live_photo_cid' })
|
||||||
@Column({ type: 'character varying', nullable: true })
|
|
||||||
livePhotoCID!: string | null;
|
livePhotoCID!: string | null;
|
||||||
|
|
||||||
@Column({ type: 'character varying', nullable: true })
|
@Column({ type: 'character varying', nullable: true })
|
||||||
@ -88,8 +86,7 @@ export class ExifTable {
|
|||||||
@Column({ type: 'integer', nullable: true })
|
@Column({ type: 'integer', nullable: true })
|
||||||
bitsPerSample!: number | null;
|
bitsPerSample!: number | null;
|
||||||
|
|
||||||
@ColumnIndex('IDX_auto_stack_id')
|
@Column({ type: 'character varying', nullable: true, indexName: 'IDX_auto_stack_id' })
|
||||||
@Column({ type: 'character varying', nullable: true })
|
|
||||||
autoStackId!: string | null;
|
autoStackId!: string | null;
|
||||||
|
|
||||||
@Column({ type: 'integer', nullable: true })
|
@Column({ type: 'integer', nullable: true })
|
||||||
@ -98,7 +95,6 @@ export class ExifTable {
|
|||||||
@UpdateDateColumn({ default: () => 'clock_timestamp()' })
|
@UpdateDateColumn({ default: () => 'clock_timestamp()' })
|
||||||
updatedAt?: Date;
|
updatedAt?: Date;
|
||||||
|
|
||||||
@ColumnIndex('IDX_asset_exif_update_id')
|
@UpdateIdColumn({ indexName: 'IDX_asset_exif_update_id' })
|
||||||
@UpdateIdColumn()
|
|
||||||
updateId?: string;
|
updateId?: string;
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { Column, Index, PrimaryColumn, Table } from 'src/sql-tools';
|
import { Column, Index, PrimaryColumn, Table } from 'src/sql-tools';
|
||||||
|
|
||||||
@Table({ name: 'geodata_places' })
|
@Table({ name: 'geodata_places', synchronize: false })
|
||||||
@Index({
|
@Index({
|
||||||
name: 'idx_geodata_places_alternate_names',
|
name: 'idx_geodata_places_alternate_names',
|
||||||
using: 'gin',
|
using: 'gin',
|
||||||
@ -26,11 +26,10 @@ import { Column, Index, PrimaryColumn, Table } from 'src/sql-tools';
|
|||||||
synchronize: false,
|
synchronize: false,
|
||||||
})
|
})
|
||||||
@Index({
|
@Index({
|
||||||
name: 'idx_geodata_places_gist_earthcoord',
|
name: 'IDX_geodata_gist_earthcoord',
|
||||||
expression: 'll_to_earth_public(latitude, longitude)',
|
expression: 'll_to_earth_public(latitude, longitude)',
|
||||||
synchronize: false,
|
synchronize: false,
|
||||||
})
|
})
|
||||||
@Table({ name: 'idx_geodata_places', synchronize: false })
|
|
||||||
export class GeodataPlacesTable {
|
export class GeodataPlacesTable {
|
||||||
@PrimaryColumn({ type: 'integer' })
|
@PrimaryColumn({ type: 'integer' })
|
||||||
id!: number;
|
id!: number;
|
||||||
|
@ -2,7 +2,6 @@ import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators';
|
|||||||
import { UserTable } from 'src/schema/tables/user.table';
|
import { UserTable } from 'src/schema/tables/user.table';
|
||||||
import {
|
import {
|
||||||
Column,
|
Column,
|
||||||
ColumnIndex,
|
|
||||||
CreateDateColumn,
|
CreateDateColumn,
|
||||||
DeleteDateColumn,
|
DeleteDateColumn,
|
||||||
ForeignKeyColumn,
|
ForeignKeyColumn,
|
||||||
@ -41,7 +40,6 @@ export class LibraryTable {
|
|||||||
@Column({ type: 'timestamp with time zone', nullable: true })
|
@Column({ type: 'timestamp with time zone', nullable: true })
|
||||||
refreshedAt!: Date | null;
|
refreshedAt!: Date | null;
|
||||||
|
|
||||||
@ColumnIndex('IDX_libraries_update_id')
|
@UpdateIdColumn({ indexName: 'IDX_libraries_update_id' })
|
||||||
@UpdateIdColumn()
|
|
||||||
updateId?: string;
|
updateId?: string;
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,6 @@ import { MemoryType } from 'src/enum';
|
|||||||
import { UserTable } from 'src/schema/tables/user.table';
|
import { UserTable } from 'src/schema/tables/user.table';
|
||||||
import {
|
import {
|
||||||
Column,
|
Column,
|
||||||
ColumnIndex,
|
|
||||||
CreateDateColumn,
|
CreateDateColumn,
|
||||||
DeleteDateColumn,
|
DeleteDateColumn,
|
||||||
ForeignKeyColumn,
|
ForeignKeyColumn,
|
||||||
@ -55,7 +54,6 @@ export class MemoryTable<T extends MemoryType = MemoryType> {
|
|||||||
@Column({ type: 'timestamp with time zone', nullable: true })
|
@Column({ type: 'timestamp with time zone', nullable: true })
|
||||||
hideAt?: Date;
|
hideAt?: Date;
|
||||||
|
|
||||||
@ColumnIndex('IDX_memories_update_id')
|
@UpdateIdColumn({ indexName: 'IDX_memories_update_id' })
|
||||||
@UpdateIdColumn()
|
|
||||||
updateId?: string;
|
updateId?: string;
|
||||||
}
|
}
|
||||||
|
@ -1,14 +1,12 @@
|
|||||||
import { AssetTable } from 'src/schema/tables/asset.table';
|
import { AssetTable } from 'src/schema/tables/asset.table';
|
||||||
import { MemoryTable } from 'src/schema/tables/memory.table';
|
import { MemoryTable } from 'src/schema/tables/memory.table';
|
||||||
import { ColumnIndex, ForeignKeyColumn, Table } from 'src/sql-tools';
|
import { ForeignKeyColumn, Table } from 'src/sql-tools';
|
||||||
|
|
||||||
@Table('memories_assets_assets')
|
@Table('memories_assets_assets')
|
||||||
export class MemoryAssetTable {
|
export class MemoryAssetTable {
|
||||||
@ColumnIndex()
|
|
||||||
@ForeignKeyColumn(() => MemoryTable, { onUpdate: 'CASCADE', onDelete: 'CASCADE', primary: true })
|
@ForeignKeyColumn(() => MemoryTable, { onUpdate: 'CASCADE', onDelete: 'CASCADE', primary: true })
|
||||||
memoriesId!: string;
|
memoriesId!: string;
|
||||||
|
|
||||||
@ColumnIndex()
|
|
||||||
@ForeignKeyColumn(() => AssetTable, { onUpdate: 'CASCADE', onDelete: 'CASCADE', primary: true })
|
@ForeignKeyColumn(() => AssetTable, { onUpdate: 'CASCADE', onDelete: 'CASCADE', primary: true })
|
||||||
assetsId!: string;
|
assetsId!: string;
|
||||||
}
|
}
|
||||||
|
@ -1,20 +1,17 @@
|
|||||||
import { PrimaryGeneratedUuidV7Column } from 'src/decorators';
|
import { PrimaryGeneratedUuidV7Column } from 'src/decorators';
|
||||||
import { Column, ColumnIndex, CreateDateColumn, Table } from 'src/sql-tools';
|
import { Column, CreateDateColumn, Table } from 'src/sql-tools';
|
||||||
|
|
||||||
@Table('partners_audit')
|
@Table('partners_audit')
|
||||||
export class PartnerAuditTable {
|
export class PartnerAuditTable {
|
||||||
@PrimaryGeneratedUuidV7Column()
|
@PrimaryGeneratedUuidV7Column()
|
||||||
id!: string;
|
id!: string;
|
||||||
|
|
||||||
@ColumnIndex('IDX_partners_audit_shared_by_id')
|
@Column({ type: 'uuid', indexName: 'IDX_partners_audit_shared_by_id' })
|
||||||
@Column({ type: 'uuid' })
|
|
||||||
sharedById!: string;
|
sharedById!: string;
|
||||||
|
|
||||||
@ColumnIndex('IDX_partners_audit_shared_with_id')
|
@Column({ type: 'uuid', indexName: 'IDX_partners_audit_shared_with_id' })
|
||||||
@Column({ type: 'uuid' })
|
|
||||||
sharedWithId!: string;
|
sharedWithId!: string;
|
||||||
|
|
||||||
@ColumnIndex('IDX_partners_audit_deleted_at')
|
@CreateDateColumn({ default: () => 'clock_timestamp()', indexName: 'IDX_partners_audit_deleted_at' })
|
||||||
@CreateDateColumn({ default: () => 'clock_timestamp()' })
|
|
||||||
deletedAt!: Date;
|
deletedAt!: Date;
|
||||||
}
|
}
|
||||||
|
@ -1,15 +1,7 @@
|
|||||||
import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators';
|
import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators';
|
||||||
import { partners_delete_audit } from 'src/schema/functions';
|
import { partners_delete_audit } from 'src/schema/functions';
|
||||||
import { UserTable } from 'src/schema/tables/user.table';
|
import { UserTable } from 'src/schema/tables/user.table';
|
||||||
import {
|
import { AfterDeleteTrigger, Column, CreateDateColumn, ForeignKeyColumn, Table, UpdateDateColumn } from 'src/sql-tools';
|
||||||
AfterDeleteTrigger,
|
|
||||||
Column,
|
|
||||||
ColumnIndex,
|
|
||||||
CreateDateColumn,
|
|
||||||
ForeignKeyColumn,
|
|
||||||
Table,
|
|
||||||
UpdateDateColumn,
|
|
||||||
} from 'src/sql-tools';
|
|
||||||
|
|
||||||
@Table('partners')
|
@Table('partners')
|
||||||
@UpdatedAtTrigger('partners_updated_at')
|
@UpdatedAtTrigger('partners_updated_at')
|
||||||
@ -21,7 +13,12 @@ import {
|
|||||||
when: 'pg_trigger_depth() = 0',
|
when: 'pg_trigger_depth() = 0',
|
||||||
})
|
})
|
||||||
export class PartnerTable {
|
export class PartnerTable {
|
||||||
@ForeignKeyColumn(() => UserTable, { onDelete: 'CASCADE', primary: true })
|
@ForeignKeyColumn(() => UserTable, {
|
||||||
|
onDelete: 'CASCADE',
|
||||||
|
primary: true,
|
||||||
|
// [sharedById, sharedWithId] is the PK constraint
|
||||||
|
index: false,
|
||||||
|
})
|
||||||
sharedById!: string;
|
sharedById!: string;
|
||||||
|
|
||||||
@ForeignKeyColumn(() => UserTable, { onDelete: 'CASCADE', primary: true })
|
@ForeignKeyColumn(() => UserTable, { onDelete: 'CASCADE', primary: true })
|
||||||
@ -36,7 +33,6 @@ export class PartnerTable {
|
|||||||
@Column({ type: 'boolean', default: false })
|
@Column({ type: 'boolean', default: false })
|
||||||
inTimeline!: boolean;
|
inTimeline!: boolean;
|
||||||
|
|
||||||
@ColumnIndex('IDX_partners_update_id')
|
@UpdateIdColumn({ indexName: 'IDX_partners_update_id' })
|
||||||
@UpdateIdColumn()
|
|
||||||
updateId!: string;
|
updateId!: string;
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,6 @@ import { UserTable } from 'src/schema/tables/user.table';
|
|||||||
import {
|
import {
|
||||||
Check,
|
Check,
|
||||||
Column,
|
Column,
|
||||||
ColumnIndex,
|
|
||||||
CreateDateColumn,
|
CreateDateColumn,
|
||||||
ForeignKeyColumn,
|
ForeignKeyColumn,
|
||||||
PrimaryGeneratedColumn,
|
PrimaryGeneratedColumn,
|
||||||
@ -49,7 +48,6 @@ export class PersonTable {
|
|||||||
@Column({ type: 'character varying', nullable: true, default: null })
|
@Column({ type: 'character varying', nullable: true, default: null })
|
||||||
color?: string | null;
|
color?: string | null;
|
||||||
|
|
||||||
@ColumnIndex('IDX_person_update_id')
|
@UpdateIdColumn({ indexName: 'IDX_person_update_id' })
|
||||||
@UpdateIdColumn()
|
|
||||||
updateId!: string;
|
updateId!: string;
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,6 @@ import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators';
|
|||||||
import { UserTable } from 'src/schema/tables/user.table';
|
import { UserTable } from 'src/schema/tables/user.table';
|
||||||
import {
|
import {
|
||||||
Column,
|
Column,
|
||||||
ColumnIndex,
|
|
||||||
CreateDateColumn,
|
CreateDateColumn,
|
||||||
ForeignKeyColumn,
|
ForeignKeyColumn,
|
||||||
PrimaryGeneratedColumn,
|
PrimaryGeneratedColumn,
|
||||||
@ -35,7 +34,6 @@ export class SessionTable {
|
|||||||
@Column({ default: '' })
|
@Column({ default: '' })
|
||||||
deviceOS!: string;
|
deviceOS!: string;
|
||||||
|
|
||||||
@ColumnIndex('IDX_sessions_update_id')
|
@UpdateIdColumn({ indexName: 'IDX_sessions_update_id' })
|
||||||
@UpdateIdColumn()
|
|
||||||
updateId!: string;
|
updateId!: string;
|
||||||
}
|
}
|
||||||
|
@ -1,14 +1,12 @@
|
|||||||
import { AssetTable } from 'src/schema/tables/asset.table';
|
import { AssetTable } from 'src/schema/tables/asset.table';
|
||||||
import { SharedLinkTable } from 'src/schema/tables/shared-link.table';
|
import { SharedLinkTable } from 'src/schema/tables/shared-link.table';
|
||||||
import { ColumnIndex, ForeignKeyColumn, Table } from 'src/sql-tools';
|
import { ForeignKeyColumn, Table } from 'src/sql-tools';
|
||||||
|
|
||||||
@Table('shared_link__asset')
|
@Table('shared_link__asset')
|
||||||
export class SharedLinkAssetTable {
|
export class SharedLinkAssetTable {
|
||||||
@ColumnIndex()
|
|
||||||
@ForeignKeyColumn(() => AssetTable, { onUpdate: 'CASCADE', onDelete: 'CASCADE', primary: true })
|
@ForeignKeyColumn(() => AssetTable, { onUpdate: 'CASCADE', onDelete: 'CASCADE', primary: true })
|
||||||
assetsId!: string;
|
assetsId!: string;
|
||||||
|
|
||||||
@ColumnIndex()
|
|
||||||
@ForeignKeyColumn(() => SharedLinkTable, { onUpdate: 'CASCADE', onDelete: 'CASCADE', primary: true })
|
@ForeignKeyColumn(() => SharedLinkTable, { onUpdate: 'CASCADE', onDelete: 'CASCADE', primary: true })
|
||||||
sharedLinksId!: string;
|
sharedLinksId!: string;
|
||||||
}
|
}
|
||||||
|
@ -1,15 +1,7 @@
|
|||||||
import { SharedLinkType } from 'src/enum';
|
import { SharedLinkType } from 'src/enum';
|
||||||
import { AlbumTable } from 'src/schema/tables/album.table';
|
import { AlbumTable } from 'src/schema/tables/album.table';
|
||||||
import { UserTable } from 'src/schema/tables/user.table';
|
import { UserTable } from 'src/schema/tables/user.table';
|
||||||
import {
|
import { Column, CreateDateColumn, ForeignKeyColumn, PrimaryGeneratedColumn, Table, Unique } from 'src/sql-tools';
|
||||||
Column,
|
|
||||||
ColumnIndex,
|
|
||||||
CreateDateColumn,
|
|
||||||
ForeignKeyColumn,
|
|
||||||
PrimaryGeneratedColumn,
|
|
||||||
Table,
|
|
||||||
Unique,
|
|
||||||
} from 'src/sql-tools';
|
|
||||||
|
|
||||||
@Table('shared_links')
|
@Table('shared_links')
|
||||||
@Unique({ name: 'UQ_sharedlink_key', columns: ['key'] })
|
@Unique({ name: 'UQ_sharedlink_key', columns: ['key'] })
|
||||||
@ -23,8 +15,7 @@ export class SharedLinkTable {
|
|||||||
@ForeignKeyColumn(() => UserTable, { onDelete: 'CASCADE', onUpdate: 'CASCADE' })
|
@ForeignKeyColumn(() => UserTable, { onDelete: 'CASCADE', onUpdate: 'CASCADE' })
|
||||||
userId!: string;
|
userId!: string;
|
||||||
|
|
||||||
@ColumnIndex('IDX_sharedlink_key')
|
@Column({ type: 'bytea', indexName: 'IDX_sharedlink_key' })
|
||||||
@Column({ type: 'bytea' })
|
|
||||||
key!: Buffer; // use to access the inidividual asset
|
key!: Buffer; // use to access the inidividual asset
|
||||||
|
|
||||||
@Column()
|
@Column()
|
||||||
@ -39,8 +30,12 @@ export class SharedLinkTable {
|
|||||||
@Column({ type: 'boolean', default: false })
|
@Column({ type: 'boolean', default: false })
|
||||||
allowUpload!: boolean;
|
allowUpload!: boolean;
|
||||||
|
|
||||||
@ColumnIndex('IDX_sharedlink_albumId')
|
@ForeignKeyColumn(() => AlbumTable, {
|
||||||
@ForeignKeyColumn(() => AlbumTable, { nullable: true, onDelete: 'CASCADE', onUpdate: 'CASCADE' })
|
nullable: true,
|
||||||
|
onDelete: 'CASCADE',
|
||||||
|
onUpdate: 'CASCADE',
|
||||||
|
indexName: 'IDX_sharedlink_albumId',
|
||||||
|
})
|
||||||
albumId!: string;
|
albumId!: string;
|
||||||
|
|
||||||
@Column({ type: 'boolean', default: true })
|
@Column({ type: 'boolean', default: true })
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user