mirror of
https://github.com/immich-app/immich.git
synced 2024-12-29 15:11:58 +00:00
wip
wip use prisma for paginated queries remove migration file redundant spread simplified extend use bigint for comparison handle deleted assets in extension Squashed commit of the following: commit64aac239f0
Author: Alex <alex.tran1502@gmail.com> Date: Thu Mar 21 18:00:22 2024 -0500 chore: consolidate readme files (#8171) commitd6823b128c
Author: Daniel Dietzler <36593685+danieldietzler@users.noreply.github.com> Date: Thu Mar 21 23:59:21 2024 +0100 fix(server): validation events actually throwing an error (#8172) * fix validation events * add e2e test commit508f32c08a
Author: martin <74269598+martabal@users.noreply.github.com> Date: Thu Mar 21 21:01:08 2024 +0100 feat(web): improvements to slideshow (#8032) * feat: improvements to slideshow * feat: pause video with slideshow bar * pr feedback * fix: remove dispatch * fix: simplify * pr feedback * pr feedback --------- Co-authored-by: Alex Tran <alex.tran1502@gmail.com> commit8ed6ed4d2b
Author: Ethan Margaillan <ethan.margaillan@gmail.com> Date: Thu Mar 21 19:39:33 2024 +0100 feat(web): rework context menus: add icons and reorder items (#8090) commit1abb0bdae8
Author: Fynn Petersen-Frey <10599762+fyfrey@users.noreply.github.com> Date: Thu Mar 21 17:51:03 2024 +0100 feat(mobile): faster image loader (#8140) Co-authored-by: Alex Tran <alex.tran1502@gmail.com> commit5ef6215546
Author: martyfuhry <martyfuhry@gmail.com> Date: Thu Mar 21 12:31:18 2024 -0400 chore(mobile): Bump to Flutter 3.19.0 (#7167) * Bump to Flutter 3.19.0 * Ran pub upgrade --major-versions and removed isar_version alias Wrong http version * Updated share_plus to fix android build * Updates github actions to 3.19.0 * upgrade to 3.19.3 * upgrade to 3.19.3 --------- Co-authored-by: Alex Tran <alex.tran1502@gmail.com> commit95fb9c4365
Author: waclaw66 <waclaw66@seznam.cz> Date: Thu Mar 21 18:23:06 2024 +0200 fix(mobile): spacing fixes of #8087 (#8163) fix(mobile): spacing fix of https://github.com/immich-app/immich/pull/8087 commitfa0a5107c2
Author: aviv926 <51673860+aviv926@users.noreply.github.com> Date: Thu Mar 21 17:05:45 2024 +0200 fix(docs): Immich quota claim note (#8151) * Add a note about immich_quota_claim. * Fix * PR feedback * npm run format:fix * use ¹ commitdc3c329431
Author: Jason Rasmussen <jrasm91@gmail.com> Date: Thu Mar 21 09:36:10 2024 -0500 chore: remove unused type (#8157) commit2a9f2b4515
Author: Jason Rasmussen <jrasm91@gmail.com> Date: Thu Mar 21 09:08:29 2024 -0500 refactor: app modules, main.ts (#8156) commit793049388b
Author: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com> Date: Thu Mar 21 14:44:54 2024 +0100 refactor(web): cleanup notification components (#8150) * refactor(web): cleanup notification components * use counter for ID commit382b63954c
Author: Jason Rasmussen <jrasm91@gmail.com> Date: Thu Mar 21 08:07:47 2024 -0500 refactor: asset v1, app.utils (#8152) commit87ccba7f9d
Author: Ben Basten <45583362+ben-basten@users.noreply.github.com> Date: Thu Mar 21 12:24:19 2024 +0000 feat(web): keyboard access for search dropdown, combobox fixes (#8079) * feat(web): keyboard access for search dropdown Also: fixing cosmetic issue with combobox component. * fix: revert changing required field * fix: create new focusChange action * fix: combobox usability improvements * handle escape key on the clear button * move focus to input when clear button is clicked * leave the dropdown closed if the user has already closed the dropdown and tabs over to the clear button * activate the combobox if a user tabs backwards onto the clear button * rename focusChange to focusOutside * small fixes * do not activate combobox on backwards tabbing * simplify classes in "No results" option * prevent dropdown option from being preselected when clear button is clicked * fix: remove unused event dispatcher interface commite21c96c0ef
Author: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Date: Thu Mar 21 07:14:44 2024 -0500 chore(deps): update redis:6.2-alpine docker digest to 3fcb624 (#8137) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> commit4de0b2f44e
Author: Ethan Margaillan <ethan.margaillan@gmail.com> Date: Thu Mar 21 13:14:13 2024 +0100 feat(web): add ctrl+a / ctrl+d shortcuts to select / deselect all assets (#8105) * feat(web): use ctrl+a / ctrl+d to select / deselect all assets * fix(web): use shortcutList for ctrl+a / ctrl+d * fix(web): remove useless get() * feat(web): asset interaction store can now select many assets at once commitb588a87d4a
Author: Daniel Dietzler <36593685+danieldietzler@users.noreply.github.com> Date: Thu Mar 21 12:59:49 2024 +0100 chore(server): rename domain `repositories` -> `interfaces` (#8147) rename domain repositories commit44ed1f0919
Author: Alex <alex.tran1502@gmail.com> Date: Thu Mar 21 00:18:38 2024 -0500 fix(web): asset-grid padding/margin left fix (#8125) use media query for grid padding/margin size commit16d0df796c
Author: Jason Rasmussen <jrasm91@gmail.com> Date: Wed Mar 20 22:15:09 2024 -0500 refactor: infra folder (#8138) commit9fd5d2ad9c
Author: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Date: Wed Mar 20 22:59:15 2024 -0400 fix(deps): update machine-learning (#8057) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> commit28ad004b01
Author: Kirill <44521162+kirilldem@users.noreply.github.com> Date: Thu Mar 21 03:58:52 2024 +0100 Update remote-machine-learning.md (#8038) * Update remote-machine-learning.md provide an example to use cuda or another container * Update docs/docs/guides/remote-machine-learning.md Co-authored-by: aviv926 <51673860+aviv926@users.noreply.github.com> * Update docs/docs/guides/remote-machine-learning.md --------- Co-authored-by: Mert <101130780+mertalev@users.noreply.github.com> Co-authored-by: aviv926 <51673860+aviv926@users.noreply.github.com> commitef4a492cb1
Author: Daniel Dietzler <36593685+danieldietzler@users.noreply.github.com> Date: Thu Mar 21 00:07:30 2024 +0100 chore(server): move services (#8133) move services commit6d9e7694b1
Author: Daniel Dietzler <36593685+danieldietzler@users.noreply.github.com> Date: Wed Mar 20 23:53:07 2024 +0100 chore(server): move dtos (#8131) move dtos commit0c13c63bb6
Author: Jason Rasmussen <jrasm91@gmail.com> Date: Wed Mar 20 16:46:59 2024 -0500 refactor: infra/domain module (#8130) commit907eb869bc
Author: Jason Rasmussen <jrasm91@gmail.com> Date: Wed Mar 20 16:22:47 2024 -0500 chore: move apps and test utils (#8129) commitc1402eee8e
Author: Jason Rasmussen <jrasm91@gmail.com> Date: Wed Mar 20 16:02:51 2024 -0500 chore: migrate database files (#8126) commit84f7ca855a
Author: Daniel Dietzler <36593685+danieldietzler@users.noreply.github.com> Date: Wed Mar 20 21:42:58 2024 +0100 chore(server): move domain interfaces (#8124) move domain interfaces commit2dcce03352
Author: Daniel Dietzler <36593685+danieldietzler@users.noreply.github.com> Date: Wed Mar 20 21:25:33 2024 +0100 chore(server): move commands (#8121) move commands commit96a22ec3c1
Author: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Date: Wed Mar 20 16:21:27 2024 -0400 chore(deps): update base-image to v20240319 (major) (#8115) chore(deps): update base-image to v20240319 Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> commit4b29bccc7c
Author: Daniel Dietzler <36593685+danieldietzler@users.noreply.github.com> Date: Wed Mar 20 21:20:38 2024 +0100 chore(server): move cores (#8120) move cores commit40e079a247
Author: Jason Rasmussen <jrasm91@gmail.com> Date: Wed Mar 20 15:15:01 2024 -0500 chore: move controllers and middleware (#8119) commit81f0265095
Author: Jason Rasmussen <jrasm91@gmail.com> Date: Wed Mar 20 15:04:03 2024 -0500 chore: organize config, validation, decorators (#8118) * refactor: validation * refactor: utilities * refactor: config commit92cc647cf6
Author: Jason Rasmussen <jrasm91@gmail.com> Date: Wed Mar 20 14:50:01 2024 -0500 chore: renovate grouping (#8113) commit048d437b0b
Author: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com> Date: Wed Mar 20 20:40:41 2024 +0100 fix(web): prevent duplicate time bucket loads (#8091) commitec9a6bca14
Author: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Date: Wed Mar 20 15:38:58 2024 -0400 chore(deps): update dependency socket.io-client to v4.7.5 (#8111) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> commitbd5952b943
Author: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Date: Wed Mar 20 15:35:07 2024 -0400 chore(deps): update vitest monorepo to v1.4.0 (#8112) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> commit3f0d54c752
Author: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Date: Wed Mar 20 15:34:12 2024 -0400 fix(deps): update server (#8067) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> commitdab4595a4e
Author: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Date: Wed Mar 20 14:09:10 2024 -0500 chore(deps): update redis:6.2-alpine docker digest to fd35357 (#8001) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> commit6d9ca82b19
Author: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Date: Wed Mar 20 14:08:01 2024 -0500 chore(deps): update web (#8066) * chore(deps): update web * fix: linting --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Jason Rasmussen <jrasm91@gmail.com> commit373a03e819
Author: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Date: Wed Mar 20 14:06:58 2024 -0500 chore(deps): update dependency @types/node to v20.11.28 (#8110) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> commitd97b0259fa
Author: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Date: Wed Mar 20 14:38:48 2024 -0400 chore(deps): update node.js to bf77dc2 (#8063) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> commit2267ca1949
Author: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Date: Wed Mar 20 14:38:28 2024 -0400 chore(deps): update node.js to 8765147 (#8058) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> commit29be53e70d
Author: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Date: Wed Mar 20 14:37:22 2024 -0400 chore(deps): update prom/prometheus docker digest to 5ccad47 (#8071) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> commit851fe4a49f
Author: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Date: Wed Mar 20 14:33:41 2024 -0400 chore(deps): update dependency @types/node to v20.11.28 (#8064) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> commit30f499cf2e
Author: Daniel Dietzler <36593685+danieldietzler@users.noreply.github.com> Date: Wed Mar 20 19:32:04 2024 +0100 chore(server): use absolute import paths (#8080) update server to use absolute import paths commit591a641d8d
Author: Alex <alex.tran1502@gmail.com> Date: Wed Mar 20 10:00:35 2024 -0500 chore: post release tasks commit5b314ffd46
Author: Alex The Bot <alex.tran1502@gmail.com> Date: Wed Mar 20 14:50:57 2024 +0000 Version v1.99.0 commit0b078c9f99
Author: Alex <alex.tran1502@gmail.com> Date: Wed Mar 20 09:46:31 2024 -0500 fix(web): Share button visible when viewing album has only shared link (#8100) commit0d5584ecbb
Author: Alex <alex.tran1502@gmail.com> Date: Wed Mar 20 09:28:19 2024 -0500 fix(web): shift-select again (#8098) commit5e090646ba
Author: waclaw66 <waclaw66@seznam.cz> Date: Wed Mar 20 16:26:09 2024 +0200 fix(mobile): missing "Add name" translation (#8087) fix(mobile): missing "Add name" translation, positioning commitc4e910dd3d
Author: Mert <101130780+mertalev@users.noreply.github.com> Date: Wed Mar 20 10:20:46 2024 -0400 docs(server): add documentation for prometheus metrics (#8084) * add monitoring doc * wording * indent * note instead of tip * Update docs/docs/features/monitoring.md Co-authored-by: bo0tzz <git@bo0tzz.me> * Update docs/docs/features/monitoring.md Co-authored-by: bo0tzz <git@bo0tzz.me> --------- Co-authored-by: Alex Tran <alex.tran1502@gmail.com> Co-authored-by: bo0tzz <git@bo0tzz.me> commit5a2394af7c
Author: Alex <alex.tran1502@gmail.com> Date: Wed Mar 20 09:16:20 2024 -0500 fix(web): shift-select (#8093) * fix(web): shift-select * remove unused code * proper fix commit48e32269f4
Author: Alex <alex.tran1502@gmail.com> Date: Wed Mar 20 09:16:00 2024 -0500 chore: add prometheus.yml to release artifact (#8096) commitdd9d90d21e
Author: Zack Pollard <zackpollard@ymail.com> Date: Wed Mar 20 06:31:52 2024 -0600 test: temporarily disable flaky audit e2e test until #7436 is fixed (#8089) commit0544c687b9
Author: Ethan Margaillan <ethan.margaillan@gmail.com> Date: Wed Mar 20 13:29:30 2024 +0100 fix(web): missing margin on people page (#8081) commite810aae212
Author: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com> Date: Wed Mar 20 13:24:08 2024 +0100 fix(web): show search page errors and use feature flag (#8088) commit9c6a26de9f
Author: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com> Date: Wed Mar 20 05:41:31 2024 +0100 chore(web): add asset store unit tests (#8077) chore(web): asset store unit tests commite6f2bb9f89
Author: Jonathan Jogenfors <jonathan@jogenfors.se> Date: Wed Mar 20 05:40:28 2024 +0100 fix(server): use extension in originalFileName for libraries (#8083) * use file base * fix: test * fix: e2e-job tests --------- Co-authored-by: Alex Tran <alex.tran1502@gmail.com> commitf908bd4a64
Author: Ethan Margaillan <ethan.margaillan@gmail.com> Date: Wed Mar 20 05:28:13 2024 +0100 fix(web): prevent drag-n-drop upload overlay from showing when not dragging files (#8082) commit7395b03b1f
Author: Thariq Shanavas <thariqshanavas@gmail.com> Date: Tue Mar 19 22:12:36 2024 -0600 fix(docs) minor security warning raised by Borg (#8075) * Fix minor borg security warning * Update template-backup-script.md * removed one unnecessary step * Clarified optional steps * Update template-backup-script.md commit63b4fc6f65
Author: Alex <alex.tran1502@gmail.com> Date: Tue Mar 19 23:07:26 2024 -0500 chore(mobile): svg logo (#8074) * chore(mobile): anti-aliasing logo * use svg * adjust height * better sizing commitf392fe7702
Author: Mert <101130780+mertalev@users.noreply.github.com> Date: Tue Mar 19 23:23:57 2024 -0400 fix(server): "view all" for cities only showing 12 cities (#8035) * view all cities * increase limit * rename endpoint * optimize query * remove pagination * update sql * linting * revert sort by count in explore page for now * fix query * fix * update sql * move to search, add partner support * update sql * pr feedback * euphemism * parameters as separate variable * move comment * update sql * linting commit2daed747cd
Author: Mert <101130780+mertalev@users.noreply.github.com> Date: Tue Mar 19 22:42:10 2024 -0400 chore(server): change `save` -> `update` in asset repository (#8055) * `save` -> `update` * change return type * include relations * fix tests * remove when mocks * fix * stricter typing * simpler type commit9e4bab7494
Author: shenlong <139912620+shenlong-tanwen@users.noreply.github.com> Date: Tue Mar 19 14:31:56 2024 +0000 feat(mobile): drag to select assets (#8004) fear(mobile): drag to select assets Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> Co-authored-by: Alex <alex.tran1502@gmail.com> commit9274c0701b
Author: waclaw66 <waclaw66@seznam.cz> Date: Tue Mar 19 16:22:44 2024 +0200 fix(mobile): do not show hidden people (#8072) * fix(mobile): do not show hidden people * dart format fix commit0bc773fd00
Author: Alex <alex.tran1502@gmail.com> Date: Tue Mar 19 08:40:14 2024 -0500 refactor(mobile): backup album selection (#8053) * feat(mobile): include album with 0 assets as album option for backup * Show icon instead of thumbnail * Handle backupProgress state transition correctly to always load the backup info * remove todo comment commitc6d2408517
Author: Ben Basten <45583362+ben-basten@users.noreply.github.com> Date: Tue Mar 19 12:56:41 2024 +0000 feat(web): combobox accessibility improvements (#8007) * bump skip link z index, to prevent overlap with the search box * combobox refactor initial commit * pull label into the combobox component * feat(web): combobox accessibility improvements * fix: replace crypto.randomUUID, fix border UI bug, simpler focus handling (#2) * fix: handle changes in the selected option * fix: better escape key handling in search bar * fix: remove broken tailwind classes Co-authored-by: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com> * fix: remove custom "outclick" handler logic * fix: use focusout instead of custom key handlers to detect focus change * fix: move escape key handling to the window Also add escape key handling to the input box, to make sure that the "recent searches" dropdown gets closed too. * fix: better input event handling Co-authored-by: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com> * fix: highlighting selected dropdown element --------- Co-authored-by: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com> commit033f83a55a
Author: Jan <17313367+JW-CH@users.noreply.github.com> Date: Tue Mar 19 13:47:33 2024 +0100 fix(docs): update authelia OIDC link (#8070) commit51841d627c
Author: Alex <alex.tran1502@gmail.com> Date: Mon Mar 18 22:39:49 2024 -0500 fix(web): load panorama in shared link (#8060) * fix(web): load panorama in shared link * remove console log commit50924f0b3d
Author: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Date: Mon Mar 18 19:49:31 2024 -0400 chore(deps): update dependency @types/node to v20.11.27 (#8012) * chore(deps): update dependency @types/node to v20.11.27 * fixes * fixes --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Daniel Dietzler <mail@ddietzler.dev> Co-authored-by: Marty Fuhry <martyfuhry@gmail.com> commit4aae1da841
Author: Daniel Dietzler <36593685+danieldietzler@users.noreply.github.com> Date: Mon Mar 18 22:56:39 2024 +0100 fix(web): repair page typo (#8051) fix typo commit1a2554548a
Author: bo0tzz <git@bo0tzz.me> Date: Mon Mar 18 22:54:30 2024 +0100 chore: Simplify install script (#8048) * chore: Simplify install script The default .env file now contains a set UPLOAD_LOCATION already * fix: Remove leftover line commit40262c30cb
Author: Jason Rasmussen <jrasm91@gmail.com> Date: Mon Mar 18 15:59:53 2024 -0500 refactor(server): library service (#8050) * refactor: library service * chore: open api * fix: checks commit761e7fdd2d
Author: Alex <alex.tran1502@gmail.com> Date: Mon Mar 18 14:46:52 2024 -0500 feat(server): memory includes partners assets on timeline (#7993) * feat(server): memory includes partners assets on timeline * remove unsued code, generate sql * fix test * add test commitcd8a124b25
Author: aviv926 <51673860+aviv926@users.noreply.github.com> Date: Mon Mar 18 16:00:11 2024 +0200 feat(docs): User management new options (#8029) * User Management * Add photo commit148428a564
Author: Daniel Dietzler <36593685+danieldietzler@users.noreply.github.com> Date: Sun Mar 17 20:16:02 2024 +0100 feat(server): use nestjs events to validate config (#7986) * use events for config validation * chore: better types * add unit tests --------- Co-authored-by: Jason Rasmussen <jrasm91@gmail.com> commit14da671bf9
Author: Tyler Brockett <tylerbrockett@users.noreply.github.com> Date: Sun Mar 17 11:41:55 2024 -0700 fix(docs): add microservices to IMMICH_CONFIG_FILE env var documentation (#8017) commite8f0f82db0
Author: Davide <22103897+dvdblg@users.noreply.github.com> Date: Sun Mar 17 18:48:59 2024 +0100 feat(ml): add cache_dir option to OpenVINO EP (#8018) * add cache_dir option to OpenVINO EP * update provider options test to include cache_dir * use forward slash instead of string concatenation * fix cache_dir placement in provider options assertion commitb8278404a0
Author: Alex <alex.tran1502@gmail.com> Date: Sun Mar 17 10:46:42 2024 -0500 chore(docs): update readme (#8021) commit45671b0b8b
Author: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Date: Sat Mar 16 15:34:49 2024 -0500 chore(deps): update typescript-eslint monorepo to v7.2.0 (#8008) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> refactor search, kysely extension set max use class provider directly fix remove deprecated endpoint add truncated date migration fix get by date query fix typing fix typing move with* helpers refactor timeline service fix facial recognition fix test we have `withDeleted` at home rebase
This commit is contained in:
parent
e54c18367b
commit
fbc695b46a
25 changed files with 4397 additions and 759 deletions
|
@ -10,13 +10,17 @@ RUN npm ci && \
|
||||||
rm -rf node_modules/@img/sharp-libvips* && \
|
rm -rf node_modules/@img/sharp-libvips* && \
|
||||||
rm -rf node_modules/@img/sharp-linuxmusl-x64
|
rm -rf node_modules/@img/sharp-linuxmusl-x64
|
||||||
COPY server .
|
COPY server .
|
||||||
|
|
||||||
|
WORKDIR /usr/src/app/server
|
||||||
|
RUN npm run prisma:generate
|
||||||
|
|
||||||
|
WORKDIR /usr/src/app
|
||||||
ENV PATH="${PATH}:/usr/src/app/bin" \
|
ENV PATH="${PATH}:/usr/src/app/bin" \
|
||||||
IMMICH_ENV=development \
|
IMMICH_ENV=development \
|
||||||
NVIDIA_DRIVER_CAPABILITIES=all \
|
NVIDIA_DRIVER_CAPABILITIES=all \
|
||||||
NVIDIA_VISIBLE_DEVICES=all
|
NVIDIA_VISIBLE_DEVICES=all
|
||||||
ENTRYPOINT ["tini", "--", "/bin/sh"]
|
ENTRYPOINT ["tini", "--", "/bin/sh"]
|
||||||
|
|
||||||
|
|
||||||
FROM dev AS prod
|
FROM dev AS prod
|
||||||
|
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|
2705
server/package-lock.json
generated
2705
server/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -32,7 +32,8 @@
|
||||||
"typeorm:schema:reset": "npm run typeorm:schema:drop && npm run typeorm:migrations:run",
|
"typeorm:schema:reset": "npm run typeorm:schema:drop && npm run typeorm:migrations:run",
|
||||||
"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",
|
||||||
"email:dev": "email dev -p 3050 --dir src/emails"
|
"email:dev": "email dev -p 3050 --dir src/emails",
|
||||||
|
"prisma:generate": "prisma generate --schema=./src/prisma/schema.prisma"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nestjs/bullmq": "^10.0.1",
|
"@nestjs/bullmq": "^10.0.1",
|
||||||
|
@ -51,6 +52,7 @@
|
||||||
"@opentelemetry/exporter-prometheus": "^0.52.0",
|
"@opentelemetry/exporter-prometheus": "^0.52.0",
|
||||||
"@opentelemetry/sdk-node": "^0.52.0",
|
"@opentelemetry/sdk-node": "^0.52.0",
|
||||||
"@react-email/components": "^0.0.19",
|
"@react-email/components": "^0.0.19",
|
||||||
|
"@prisma/client": "^5.11.0",
|
||||||
"@socket.io/postgres-adapter": "^0.3.1",
|
"@socket.io/postgres-adapter": "^0.3.1",
|
||||||
"archiver": "^7.0.0",
|
"archiver": "^7.0.0",
|
||||||
"async-lock": "^1.4.0",
|
"async-lock": "^1.4.0",
|
||||||
|
@ -69,6 +71,7 @@
|
||||||
"ioredis": "^5.3.2",
|
"ioredis": "^5.3.2",
|
||||||
"joi": "^17.10.0",
|
"joi": "^17.10.0",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
|
"kysely": "^0.27.3",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"luxon": "^3.4.2",
|
"luxon": "^3.4.2",
|
||||||
"mnemonist": "^0.39.8",
|
"mnemonist": "^0.39.8",
|
||||||
|
@ -79,6 +82,7 @@
|
||||||
"openid-client": "^5.4.3",
|
"openid-client": "^5.4.3",
|
||||||
"pg": "^8.11.3",
|
"pg": "^8.11.3",
|
||||||
"picomatch": "^4.0.0",
|
"picomatch": "^4.0.0",
|
||||||
|
"prisma-extension-kysely": "^2.1.0",
|
||||||
"react-email": "^2.1.2",
|
"react-email": "^2.1.2",
|
||||||
"reflect-metadata": "^0.2.0",
|
"reflect-metadata": "^0.2.0",
|
||||||
"rxjs": "^7.8.1",
|
"rxjs": "^7.8.1",
|
||||||
|
@ -122,6 +126,8 @@
|
||||||
"mock-fs": "^5.2.0",
|
"mock-fs": "^5.2.0",
|
||||||
"prettier": "^3.0.2",
|
"prettier": "^3.0.2",
|
||||||
"prettier-plugin-organize-imports": "^3.2.3",
|
"prettier-plugin-organize-imports": "^3.2.3",
|
||||||
|
"prisma": "^5.11.0",
|
||||||
|
"prisma-kysely": "^1.8.0",
|
||||||
"rimraf": "^5.0.1",
|
"rimraf": "^5.0.1",
|
||||||
"source-map-support": "^0.5.21",
|
"source-map-support": "^0.5.21",
|
||||||
"sql-formatter": "^15.0.0",
|
"sql-formatter": "^15.0.0",
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
|
import { Prisma } from '@prisma/client';
|
||||||
import { AssetOrder } from 'src/entities/album.entity';
|
import { AssetOrder } from 'src/entities/album.entity';
|
||||||
import { AssetJobStatusEntity } from 'src/entities/asset-job-status.entity';
|
import { AssetJobStatusEntity } from 'src/entities/asset-job-status.entity';
|
||||||
import { AssetEntity, AssetType } from 'src/entities/asset.entity';
|
import { AssetEntity, AssetType } from 'src/entities/asset.entity';
|
||||||
import { ExifEntity } from 'src/entities/exif.entity';
|
import { ExifEntity } from 'src/entities/exif.entity';
|
||||||
import { AssetSearchOptions, SearchExploreItem } from 'src/interfaces/search.interface';
|
import { AssetSearchOptions, SearchExploreItem } from 'src/interfaces/search.interface';
|
||||||
import { Paginated, PaginationOptions } from 'src/utils/pagination';
|
import { Paginated, PaginationOptions } from 'src/utils/pagination';
|
||||||
import { FindOptionsOrder, FindOptionsRelations, FindOptionsSelect } from 'typeorm';
|
|
||||||
|
|
||||||
export type AssetStats = Record<AssetType, number>;
|
export type AssetStats = Record<AssetType, number>;
|
||||||
|
|
||||||
|
@ -66,22 +66,6 @@ export interface TimeBucketItem {
|
||||||
count: number;
|
count: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AssetCreate = Pick<
|
|
||||||
AssetEntity,
|
|
||||||
| 'deviceAssetId'
|
|
||||||
| 'ownerId'
|
|
||||||
| 'libraryId'
|
|
||||||
| 'deviceId'
|
|
||||||
| 'type'
|
|
||||||
| 'originalPath'
|
|
||||||
| 'fileCreatedAt'
|
|
||||||
| 'localDateTime'
|
|
||||||
| 'fileModifiedAt'
|
|
||||||
| 'checksum'
|
|
||||||
| 'originalFileName'
|
|
||||||
> &
|
|
||||||
Partial<AssetEntity>;
|
|
||||||
|
|
||||||
export type AssetWithoutRelations = Omit<
|
export type AssetWithoutRelations = Omit<
|
||||||
AssetEntity,
|
AssetEntity,
|
||||||
| 'livePhotoVideo'
|
| 'livePhotoVideo'
|
||||||
|
@ -97,10 +81,25 @@ export type AssetWithoutRelations = Omit<
|
||||||
| 'tags'
|
| 'tags'
|
||||||
>;
|
>;
|
||||||
|
|
||||||
type AssetUpdateWithoutRelations = Pick<AssetWithoutRelations, 'id'> & Partial<AssetWithoutRelations>;
|
export type AssetCreate = Pick<
|
||||||
type AssetUpdateWithLivePhotoRelation = Pick<AssetWithoutRelations, 'id'> & Pick<AssetEntity, 'livePhotoVideo'>;
|
AssetEntity,
|
||||||
|
| 'deviceAssetId'
|
||||||
|
| 'ownerId'
|
||||||
|
| 'libraryId'
|
||||||
|
| 'deviceId'
|
||||||
|
| 'type'
|
||||||
|
| 'originalPath'
|
||||||
|
| 'fileCreatedAt'
|
||||||
|
| 'localDateTime'
|
||||||
|
| 'fileModifiedAt'
|
||||||
|
| 'checksum'
|
||||||
|
| 'originalFileName'
|
||||||
|
> &
|
||||||
|
Partial<AssetWithoutRelations>;
|
||||||
|
|
||||||
export type AssetUpdateOptions = AssetUpdateWithoutRelations | AssetUpdateWithLivePhotoRelation;
|
type AssetUpdateWithoutRelations = Pick<AssetEntity, 'id'> & Partial<AssetWithoutRelations>;
|
||||||
|
|
||||||
|
export type AssetUpdateOptions = AssetUpdateWithoutRelations;
|
||||||
|
|
||||||
export type AssetUpdateAllOptions = Omit<Partial<AssetWithoutRelations>, 'id'>;
|
export type AssetUpdateAllOptions = Omit<Partial<AssetWithoutRelations>, 'id'>;
|
||||||
|
|
||||||
|
@ -139,30 +138,28 @@ export interface AssetUpdateDuplicateOptions {
|
||||||
duplicateIds: string[];
|
duplicateIds: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AssetGetByChecksumOptions {
|
||||||
|
ownerId: string;
|
||||||
|
checksum: Buffer;
|
||||||
|
libraryId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export type AssetPathEntity = Pick<AssetEntity, 'id' | 'originalPath' | 'isOffline'>;
|
export type AssetPathEntity = Pick<AssetEntity, 'id' | 'originalPath' | 'isOffline'>;
|
||||||
|
|
||||||
export const IAssetRepository = 'IAssetRepository';
|
export const IAssetRepository = 'IAssetRepository';
|
||||||
|
|
||||||
export interface IAssetRepository {
|
export interface IAssetRepository {
|
||||||
create(asset: AssetCreate): Promise<AssetEntity>;
|
create(asset: AssetCreate): Promise<AssetEntity>;
|
||||||
getByIds(
|
getByIds(ids: string[], relations?: Prisma.AssetsInclude): Promise<AssetEntity[]>;
|
||||||
ids: string[],
|
|
||||||
relations?: FindOptionsRelations<AssetEntity>,
|
|
||||||
select?: FindOptionsSelect<AssetEntity>,
|
|
||||||
): Promise<AssetEntity[]>;
|
|
||||||
getByIdsWithAllRelations(ids: string[]): Promise<AssetEntity[]>;
|
getByIdsWithAllRelations(ids: string[]): Promise<AssetEntity[]>;
|
||||||
getByDayOfYear(ownerIds: string[], monthDay: MonthDay): Promise<AssetEntity[]>;
|
getByDayOfYear(ownerIds: string[], monthDay: MonthDay): Promise<AssetEntity[]>;
|
||||||
getByChecksum(options: { ownerId: string; checksum: Buffer; libraryId?: string }): Promise<AssetEntity | null>;
|
getByChecksum(options: AssetGetByChecksumOptions): Promise<AssetEntity | null>;
|
||||||
getByChecksums(userId: string, checksums: Buffer[]): Promise<AssetEntity[]>;
|
getByChecksums(userId: string, checksums: Buffer[]): Promise<AssetEntity[]>;
|
||||||
getUploadAssetIdByChecksum(ownerId: string, checksum: Buffer): Promise<string | undefined>;
|
getUploadAssetIdByChecksum(ownerId: string, checksum: Buffer): Promise<string | undefined>;
|
||||||
getByAlbumId(pagination: PaginationOptions, albumId: string): Paginated<AssetEntity>;
|
getByAlbumId(pagination: PaginationOptions, albumId: string): Paginated<AssetEntity>;
|
||||||
getByDeviceIds(ownerId: string, deviceId: string, deviceAssetIds: string[]): Promise<string[]>;
|
getByDeviceIds(ownerId: string, deviceId: string, deviceAssetIds: string[]): Promise<string[]>;
|
||||||
getByUserId(pagination: PaginationOptions, userId: string, options?: AssetSearchOptions): Paginated<AssetEntity>;
|
getByUserId(pagination: PaginationOptions, userId: string, options?: AssetSearchOptions): Paginated<AssetEntity>;
|
||||||
getById(
|
getById(id: string, relations?: Prisma.AssetsInclude): Promise<AssetEntity | null>;
|
||||||
id: string,
|
|
||||||
relations?: FindOptionsRelations<AssetEntity>,
|
|
||||||
order?: FindOptionsOrder<AssetEntity>,
|
|
||||||
): Promise<AssetEntity | null>;
|
|
||||||
getWithout(pagination: PaginationOptions, property: WithoutProperty): Paginated<AssetEntity>;
|
getWithout(pagination: PaginationOptions, property: WithoutProperty): Paginated<AssetEntity>;
|
||||||
getWith(pagination: PaginationOptions, property: WithProperty, libraryId?: string): Paginated<AssetEntity>;
|
getWith(pagination: PaginationOptions, property: WithProperty, libraryId?: string): Paginated<AssetEntity>;
|
||||||
getRandom(userId: string, count: number): Promise<AssetEntity[]>;
|
getRandom(userId: string, count: number): Promise<AssetEntity[]>;
|
||||||
|
@ -176,7 +173,7 @@ export interface IAssetRepository {
|
||||||
getLivePhotoCount(motionId: string): Promise<number>;
|
getLivePhotoCount(motionId: string): Promise<number>;
|
||||||
updateAll(ids: string[], options: Partial<AssetUpdateAllOptions>): Promise<void>;
|
updateAll(ids: string[], options: Partial<AssetUpdateAllOptions>): Promise<void>;
|
||||||
updateDuplicates(options: AssetUpdateDuplicateOptions): Promise<void>;
|
updateDuplicates(options: AssetUpdateDuplicateOptions): Promise<void>;
|
||||||
update(asset: AssetUpdateOptions): Promise<void>;
|
update(asset: AssetUpdateOptions): Promise<AssetEntity>;
|
||||||
remove(asset: AssetEntity): Promise<void>;
|
remove(asset: AssetEntity): Promise<void>;
|
||||||
softDeleteAll(ids: string[]): Promise<void>;
|
softDeleteAll(ids: string[]): Promise<void>;
|
||||||
restoreAll(ids: string[]): Promise<void>;
|
restoreAll(ids: string[]): Promise<void>;
|
||||||
|
@ -188,7 +185,7 @@ export interface IAssetRepository {
|
||||||
upsertJobStatus(...jobStatus: Partial<AssetJobStatusEntity>[]): Promise<void>;
|
upsertJobStatus(...jobStatus: Partial<AssetJobStatusEntity>[]): Promise<void>;
|
||||||
getAssetIdByCity(userId: string, options: AssetExploreFieldOptions): Promise<SearchExploreItem<string>>;
|
getAssetIdByCity(userId: string, options: AssetExploreFieldOptions): Promise<SearchExploreItem<string>>;
|
||||||
getAssetIdByTag(userId: string, options: AssetExploreFieldOptions): Promise<SearchExploreItem<string>>;
|
getAssetIdByTag(userId: string, options: AssetExploreFieldOptions): Promise<SearchExploreItem<string>>;
|
||||||
getDuplicates(options: AssetBuilderOptions): Promise<AssetEntity[]>;
|
getDuplicates(userId: string): Promise<AssetEntity[]>;
|
||||||
getAllForUserFullSync(options: AssetFullSyncOptions): Promise<AssetEntity[]>;
|
getAllForUserFullSync(options: AssetFullSyncOptions): Promise<AssetEntity[]>;
|
||||||
getChangedDeltaSync(options: AssetDeltaSyncOptions): Promise<AssetEntity[]>;
|
getChangedDeltaSync(options: AssetDeltaSyncOptions): Promise<AssetEntity[]>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -147,13 +147,13 @@ export type SmartSearchOptions = SearchDateOptions &
|
||||||
export interface FaceEmbeddingSearch extends SearchEmbeddingOptions {
|
export interface FaceEmbeddingSearch extends SearchEmbeddingOptions {
|
||||||
hasPerson?: boolean;
|
hasPerson?: boolean;
|
||||||
numResults: number;
|
numResults: number;
|
||||||
maxDistance?: number;
|
maxDistance: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AssetDuplicateSearch {
|
export interface AssetDuplicateSearch {
|
||||||
assetId: string;
|
assetId: string;
|
||||||
embedding: number[];
|
embedding: number[];
|
||||||
maxDistance?: number;
|
maxDistance: number;
|
||||||
type: AssetType;
|
type: AssetType;
|
||||||
userIds: string[];
|
userIds: string[];
|
||||||
}
|
}
|
||||||
|
|
14
server/src/migrations/1713654632360-AddTruncatedDate.ts
Normal file
14
server/src/migrations/1713654632360-AddTruncatedDate.ts
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||||
|
|
||||||
|
export class AddTruncatedDate1713654632360 implements MigrationInterface {
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`
|
||||||
|
ALTER TABLE assets
|
||||||
|
ADD COLUMN "truncatedDate" timestamptz
|
||||||
|
GENERATED ALWAYS AS (date_trunc('day', "localDateTime" at time zone 'UTC') at time zone 'UTC') STORED`);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`ALTER TABLE assets DROP COLUMN "truncatedDate"`);
|
||||||
|
}
|
||||||
|
}
|
32
server/src/prisma/find-non-deleted.ts
Normal file
32
server/src/prisma/find-non-deleted.ts
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
import { Prisma } from '@prisma/client';
|
||||||
|
|
||||||
|
const excludeDeleted = ({ args, query }: { args: any; query: any }) => {
|
||||||
|
if (args.where === undefined) {
|
||||||
|
args.where = { deletedAt: null };
|
||||||
|
} else if (
|
||||||
|
args.where.deletedAt === undefined &&
|
||||||
|
!args.where.OR?.some(({ deletedAt }: any) => deletedAt !== undefined) &&
|
||||||
|
!args.where.AND?.some(({ deletedAt }: any) => deletedAt !== undefined)
|
||||||
|
) {
|
||||||
|
args.where.deletedAt = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return query(args);
|
||||||
|
};
|
||||||
|
|
||||||
|
const findNonDeleted = {
|
||||||
|
findFirst: excludeDeleted,
|
||||||
|
findFirstOrThrow: excludeDeleted,
|
||||||
|
findMany: excludeDeleted,
|
||||||
|
findUnique: excludeDeleted,
|
||||||
|
findUniqueOrThrow: excludeDeleted,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const findNonDeletedExtension = Prisma.defineExtension({
|
||||||
|
query: {
|
||||||
|
albums: findNonDeleted,
|
||||||
|
assets: findNonDeleted,
|
||||||
|
libraries: findNonDeleted,
|
||||||
|
users: findNonDeleted,
|
||||||
|
},
|
||||||
|
});
|
294
server/src/prisma/generated/types.ts
Normal file
294
server/src/prisma/generated/types.ts
Normal file
|
@ -0,0 +1,294 @@
|
||||||
|
import type { ColumnType } from "kysely";
|
||||||
|
export type Generated<T> = T extends ColumnType<infer S, infer I, infer U>
|
||||||
|
? ColumnType<S, I | undefined, U>
|
||||||
|
: ColumnType<T, T | undefined, T>;
|
||||||
|
export type Timestamp = ColumnType<Date, Date | string, Date | string>;
|
||||||
|
|
||||||
|
export type Activity = {
|
||||||
|
id: Generated<string>;
|
||||||
|
createdAt: Generated<Timestamp>;
|
||||||
|
updatedAt: Generated<Timestamp>;
|
||||||
|
albumId: string;
|
||||||
|
userId: string;
|
||||||
|
assetId: string | null;
|
||||||
|
comment: string | null;
|
||||||
|
isLiked: Generated<boolean>;
|
||||||
|
};
|
||||||
|
export type Albums = {
|
||||||
|
id: Generated<string>;
|
||||||
|
ownerId: string;
|
||||||
|
albumName: Generated<string>;
|
||||||
|
createdAt: Generated<Timestamp>;
|
||||||
|
albumThumbnailAssetId: string | null;
|
||||||
|
updatedAt: Generated<Timestamp>;
|
||||||
|
description: Generated<string>;
|
||||||
|
deletedAt: Timestamp | null;
|
||||||
|
isActivityEnabled: Generated<boolean>;
|
||||||
|
order: Generated<string>;
|
||||||
|
};
|
||||||
|
export type AlbumsAssetsAssets = {
|
||||||
|
albumsId: string;
|
||||||
|
assetsId: string;
|
||||||
|
};
|
||||||
|
export type AlbumsSharedUsersUsers = {
|
||||||
|
albumsId: string;
|
||||||
|
usersId: string;
|
||||||
|
};
|
||||||
|
export type ApiKeys = {
|
||||||
|
name: string;
|
||||||
|
key: string;
|
||||||
|
userId: string;
|
||||||
|
createdAt: Generated<Timestamp>;
|
||||||
|
updatedAt: Generated<Timestamp>;
|
||||||
|
id: Generated<string>;
|
||||||
|
};
|
||||||
|
export type AssetFaces = {
|
||||||
|
assetId: string;
|
||||||
|
personId: string | null;
|
||||||
|
imageWidth: Generated<number>;
|
||||||
|
imageHeight: Generated<number>;
|
||||||
|
boundingBoxX1: Generated<number>;
|
||||||
|
boundingBoxY1: Generated<number>;
|
||||||
|
boundingBoxX2: Generated<number>;
|
||||||
|
boundingBoxY2: Generated<number>;
|
||||||
|
id: Generated<string>;
|
||||||
|
};
|
||||||
|
export type AssetJobStatus = {
|
||||||
|
assetId: string;
|
||||||
|
facesRecognizedAt: Timestamp | null;
|
||||||
|
metadataExtractedAt: Timestamp | null;
|
||||||
|
duplicatesDetectedAt: Timestamp | null;
|
||||||
|
};
|
||||||
|
export type Assets = {
|
||||||
|
id: Generated<string>;
|
||||||
|
deviceAssetId: string;
|
||||||
|
ownerId: string;
|
||||||
|
deviceId: string;
|
||||||
|
type: string;
|
||||||
|
originalPath: string;
|
||||||
|
previewPath: string | null;
|
||||||
|
fileCreatedAt: Timestamp;
|
||||||
|
fileModifiedAt: Timestamp;
|
||||||
|
isFavorite: Generated<boolean>;
|
||||||
|
duration: string | null;
|
||||||
|
thumbnailPath: Generated<string | null>;
|
||||||
|
encodedVideoPath: Generated<string | null>;
|
||||||
|
checksum: Buffer;
|
||||||
|
isVisible: Generated<boolean>;
|
||||||
|
livePhotoVideoId: string | null;
|
||||||
|
updatedAt: Generated<Timestamp>;
|
||||||
|
createdAt: Generated<Timestamp>;
|
||||||
|
isArchived: Generated<boolean>;
|
||||||
|
originalFileName: string;
|
||||||
|
sidecarPath: string | null;
|
||||||
|
isReadOnly: Generated<boolean>;
|
||||||
|
thumbhash: Buffer | null;
|
||||||
|
isOffline: Generated<boolean>;
|
||||||
|
libraryId: string | null;
|
||||||
|
isExternal: Generated<boolean>;
|
||||||
|
deletedAt: Timestamp | null;
|
||||||
|
localDateTime: Timestamp;
|
||||||
|
stackId: string | null;
|
||||||
|
duplicateId: string | null;
|
||||||
|
truncatedDate: Generated<Timestamp>;
|
||||||
|
};
|
||||||
|
export type AssetStack = {
|
||||||
|
id: Generated<string>;
|
||||||
|
primaryAssetId: string;
|
||||||
|
};
|
||||||
|
export type Audit = {
|
||||||
|
id: Generated<number>;
|
||||||
|
entityType: string;
|
||||||
|
entityId: string;
|
||||||
|
action: string;
|
||||||
|
ownerId: string;
|
||||||
|
createdAt: Generated<Timestamp>;
|
||||||
|
};
|
||||||
|
export type Exif = {
|
||||||
|
assetId: string;
|
||||||
|
make: string | null;
|
||||||
|
model: string | null;
|
||||||
|
exifImageWidth: number | null;
|
||||||
|
exifImageHeight: number | null;
|
||||||
|
fileSizeInByte: string | null;
|
||||||
|
orientation: string | null;
|
||||||
|
dateTimeOriginal: Timestamp | null;
|
||||||
|
modifyDate: Timestamp | null;
|
||||||
|
lensModel: string | null;
|
||||||
|
fNumber: number | null;
|
||||||
|
focalLength: number | null;
|
||||||
|
iso: number | null;
|
||||||
|
latitude: number | null;
|
||||||
|
longitude: number | null;
|
||||||
|
city: string | null;
|
||||||
|
state: string | null;
|
||||||
|
country: string | null;
|
||||||
|
description: Generated<string>;
|
||||||
|
fps: number | null;
|
||||||
|
exposureTime: string | null;
|
||||||
|
livePhotoCID: string | null;
|
||||||
|
timeZone: string | null;
|
||||||
|
projectionType: string | null;
|
||||||
|
profileDescription: string | null;
|
||||||
|
colorspace: string | null;
|
||||||
|
bitsPerSample: number | null;
|
||||||
|
autoStackId: string | null;
|
||||||
|
};
|
||||||
|
export type GeodataPlaces = {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
longitude: number;
|
||||||
|
latitude: number;
|
||||||
|
countryCode: string;
|
||||||
|
admin1Code: string | null;
|
||||||
|
admin2Code: string | null;
|
||||||
|
modificationDate: Timestamp;
|
||||||
|
admin1Name: string | null;
|
||||||
|
admin2Name: string | null;
|
||||||
|
alternateNames: string | null;
|
||||||
|
};
|
||||||
|
export type Libraries = {
|
||||||
|
id: Generated<string>;
|
||||||
|
name: string;
|
||||||
|
ownerId: string;
|
||||||
|
type: string;
|
||||||
|
importPaths: string[];
|
||||||
|
exclusionPatterns: string[];
|
||||||
|
createdAt: Generated<Timestamp>;
|
||||||
|
updatedAt: Generated<Timestamp>;
|
||||||
|
deletedAt: Timestamp | null;
|
||||||
|
refreshedAt: Timestamp | null;
|
||||||
|
isVisible: Generated<boolean>;
|
||||||
|
};
|
||||||
|
export type MoveHistory = {
|
||||||
|
id: Generated<string>;
|
||||||
|
entityId: string;
|
||||||
|
pathType: string;
|
||||||
|
oldPath: string;
|
||||||
|
newPath: string;
|
||||||
|
};
|
||||||
|
export type Partners = {
|
||||||
|
sharedById: string;
|
||||||
|
sharedWithId: string;
|
||||||
|
createdAt: Generated<Timestamp>;
|
||||||
|
updatedAt: Generated<Timestamp>;
|
||||||
|
inTimeline: Generated<boolean>;
|
||||||
|
};
|
||||||
|
export type Person = {
|
||||||
|
id: Generated<string>;
|
||||||
|
createdAt: Generated<Timestamp>;
|
||||||
|
updatedAt: Generated<Timestamp>;
|
||||||
|
ownerId: string;
|
||||||
|
name: Generated<string>;
|
||||||
|
thumbnailPath: Generated<string>;
|
||||||
|
isHidden: Generated<boolean>;
|
||||||
|
birthDate: Timestamp | null;
|
||||||
|
faceAssetId: string | null;
|
||||||
|
};
|
||||||
|
export type SharedLinkAsset = {
|
||||||
|
assetsId: string;
|
||||||
|
sharedLinksId: string;
|
||||||
|
};
|
||||||
|
export type SharedLinks = {
|
||||||
|
id: Generated<string>;
|
||||||
|
description: string | null;
|
||||||
|
userId: string;
|
||||||
|
key: Buffer;
|
||||||
|
type: string;
|
||||||
|
createdAt: Generated<Timestamp>;
|
||||||
|
expiresAt: Timestamp | null;
|
||||||
|
allowUpload: Generated<boolean>;
|
||||||
|
albumId: string | null;
|
||||||
|
allowDownload: Generated<boolean>;
|
||||||
|
showExif: Generated<boolean>;
|
||||||
|
password: string | null;
|
||||||
|
};
|
||||||
|
export type SmartInfo = {
|
||||||
|
assetId: string;
|
||||||
|
tags: string[];
|
||||||
|
objects: string[];
|
||||||
|
};
|
||||||
|
export type SmartSearch = {
|
||||||
|
assetId: string;
|
||||||
|
};
|
||||||
|
export type SocketIoAttachments = {
|
||||||
|
id: Generated<string>;
|
||||||
|
created_at: Generated<Timestamp | null>;
|
||||||
|
payload: Buffer | null;
|
||||||
|
};
|
||||||
|
export type SystemConfig = {
|
||||||
|
key: string;
|
||||||
|
value: string | null;
|
||||||
|
};
|
||||||
|
export type SystemMetadata = {
|
||||||
|
key: string;
|
||||||
|
value: Generated<unknown>;
|
||||||
|
};
|
||||||
|
export type TagAsset = {
|
||||||
|
assetsId: string;
|
||||||
|
tagsId: string;
|
||||||
|
};
|
||||||
|
export type Tags = {
|
||||||
|
id: Generated<string>;
|
||||||
|
type: string;
|
||||||
|
name: string;
|
||||||
|
userId: string;
|
||||||
|
renameTagId: string | null;
|
||||||
|
};
|
||||||
|
export type Users = {
|
||||||
|
id: Generated<string>;
|
||||||
|
email: string;
|
||||||
|
password: Generated<string>;
|
||||||
|
createdAt: Generated<Timestamp>;
|
||||||
|
profileImagePath: Generated<string>;
|
||||||
|
isAdmin: Generated<boolean>;
|
||||||
|
shouldChangePassword: Generated<boolean>;
|
||||||
|
deletedAt: Timestamp | null;
|
||||||
|
oauthId: Generated<string>;
|
||||||
|
updatedAt: Generated<Timestamp>;
|
||||||
|
storageLabel: string | null;
|
||||||
|
memoriesEnabled: Generated<boolean>;
|
||||||
|
name: Generated<string>;
|
||||||
|
avatarColor: string | null;
|
||||||
|
quotaSizeInBytes: string | null;
|
||||||
|
quotaUsageInBytes: Generated<string>;
|
||||||
|
status: Generated<string>;
|
||||||
|
};
|
||||||
|
export type UserToken = {
|
||||||
|
id: Generated<string>;
|
||||||
|
token: string;
|
||||||
|
createdAt: Generated<Timestamp>;
|
||||||
|
updatedAt: Generated<Timestamp>;
|
||||||
|
userId: string;
|
||||||
|
deviceType: Generated<string>;
|
||||||
|
deviceOS: Generated<string>;
|
||||||
|
};
|
||||||
|
export type DB = {
|
||||||
|
activity: Activity;
|
||||||
|
albums: Albums;
|
||||||
|
albums_assets_assets: AlbumsAssetsAssets;
|
||||||
|
albums_shared_users_users: AlbumsSharedUsersUsers;
|
||||||
|
api_keys: ApiKeys;
|
||||||
|
asset_faces: AssetFaces;
|
||||||
|
asset_job_status: AssetJobStatus;
|
||||||
|
asset_stack: AssetStack;
|
||||||
|
assets: Assets;
|
||||||
|
audit: Audit;
|
||||||
|
exif: Exif;
|
||||||
|
geodata_places: GeodataPlaces;
|
||||||
|
libraries: Libraries;
|
||||||
|
move_history: MoveHistory;
|
||||||
|
partners: Partners;
|
||||||
|
person: Person;
|
||||||
|
shared_link__asset: SharedLinkAsset;
|
||||||
|
shared_links: SharedLinks;
|
||||||
|
smart_info: SmartInfo;
|
||||||
|
smart_search: SmartSearch;
|
||||||
|
socket_io_attachments: SocketIoAttachments;
|
||||||
|
system_config: SystemConfig;
|
||||||
|
system_metadata: SystemMetadata;
|
||||||
|
tag_asset: TagAsset;
|
||||||
|
tags: Tags;
|
||||||
|
user_token: UserToken;
|
||||||
|
users: Users;
|
||||||
|
};
|
16
server/src/prisma/kysely.ts
Normal file
16
server/src/prisma/kysely.ts
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import { DeduplicateJoinsPlugin, Kysely, PostgresAdapter, PostgresIntrospector, PostgresQueryCompiler } from 'kysely';
|
||||||
|
import kyselyExt from 'prisma-extension-kysely';
|
||||||
|
import type { DB } from 'src/prisma/generated/types';
|
||||||
|
|
||||||
|
export const kyselyExtension = kyselyExt({
|
||||||
|
kysely: (driver) =>
|
||||||
|
new Kysely<DB>({
|
||||||
|
dialect: {
|
||||||
|
createDriver: () => driver,
|
||||||
|
createAdapter: () => new PostgresAdapter(),
|
||||||
|
createIntrospector: (db) => new PostgresIntrospector(db),
|
||||||
|
createQueryCompiler: () => new PostgresQueryCompiler(),
|
||||||
|
},
|
||||||
|
plugins: [new DeduplicateJoinsPlugin()],
|
||||||
|
}),
|
||||||
|
});
|
17
server/src/prisma/metrics.ts
Normal file
17
server/src/prisma/metrics.ts
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
import { Prisma } from '@prisma/client';
|
||||||
|
import util from 'node:util';
|
||||||
|
|
||||||
|
export const metricsExtension = Prisma.defineExtension({
|
||||||
|
query: {
|
||||||
|
$allModels: {
|
||||||
|
async $allOperations({ operation, model, args, query }) {
|
||||||
|
const start = performance.now();
|
||||||
|
const result = await query(args);
|
||||||
|
const end = performance.now();
|
||||||
|
const time = end - start;
|
||||||
|
console.log(util.inspect({ model, operation, args, time }, { showHidden: false, depth: null, colors: true }));
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
469
server/src/prisma/schema.prisma
Normal file
469
server/src/prisma/schema.prisma
Normal file
|
@ -0,0 +1,469 @@
|
||||||
|
generator client {
|
||||||
|
provider = "prisma-client-js"
|
||||||
|
previewFeatures = ["postgresqlExtensions", "relationJoins"]
|
||||||
|
}
|
||||||
|
|
||||||
|
generator kysely {
|
||||||
|
provider = "prisma-kysely"
|
||||||
|
}
|
||||||
|
|
||||||
|
datasource db {
|
||||||
|
provider = "postgresql"
|
||||||
|
url = env("DB_URL")
|
||||||
|
extensions = [cube, earthdistance, pg_trgm, unaccent, uuid_ossp(map: "uuid-ossp", schema: "public"), vectors(map: "vectors", schema: "vectors")]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This table contains check constraints and requires additional setup for migrations. Visit https://pris.ly/d/check-constraints for more info.
|
||||||
|
model Activity {
|
||||||
|
id String @id(map: "PK_24625a1d6b1b089c8ae206fe467") @default(dbgenerated("uuid_generate_v4()")) @db.Uuid
|
||||||
|
createdAt DateTime @default(now()) @db.Timestamptz(6)
|
||||||
|
updatedAt DateTime @default(now()) @db.Timestamptz(6)
|
||||||
|
albumId String @db.Uuid
|
||||||
|
userId String @db.Uuid
|
||||||
|
assetId String? @db.Uuid
|
||||||
|
comment String?
|
||||||
|
isLiked Boolean @default(false)
|
||||||
|
albums Albums @relation(fields: [albumId], references: [id], onDelete: Cascade, map: "FK_1af8519996fbfb3684b58df280b")
|
||||||
|
users Users @relation(fields: [userId], references: [id], onDelete: Cascade, map: "FK_3571467bcbe021f66e2bdce96ea")
|
||||||
|
assets Assets? @relation(fields: [assetId], references: [id], onDelete: Cascade, map: "FK_8091ea76b12338cb4428d33d782")
|
||||||
|
|
||||||
|
@@map(name: "activity")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments
|
||||||
|
model Albums {
|
||||||
|
id String @id(map: "PK_7f71c7b5bc7c87b8f94c9a93a00") @default(dbgenerated("uuid_generate_v4()")) @db.Uuid
|
||||||
|
ownerId String @db.Uuid
|
||||||
|
albumName String @default("Untitled Album") @db.VarChar
|
||||||
|
createdAt DateTime @default(now()) @db.Timestamptz(6)
|
||||||
|
albumThumbnailAssetId String? @db.Uuid
|
||||||
|
updatedAt DateTime @default(now()) @db.Timestamptz(6)
|
||||||
|
description String @default("")
|
||||||
|
deletedAt DateTime? @db.Timestamptz(6)
|
||||||
|
isActivityEnabled Boolean @default(true)
|
||||||
|
order String @default("desc") @db.VarChar
|
||||||
|
activity Activity[]
|
||||||
|
assets Assets? @relation(fields: [albumThumbnailAssetId], references: [id], map: "FK_05895aa505a670300d4816debce")
|
||||||
|
users Users @relation(fields: [ownerId], references: [id], onDelete: Cascade, map: "FK_b22c53f35ef20c28c21637c85f4")
|
||||||
|
albums_assets_assets AlbumsAssetsAssets[]
|
||||||
|
albums_shared_users_users AlbumsSharedUsersUsers[]
|
||||||
|
shared_links SharedLinks[]
|
||||||
|
|
||||||
|
@@map(name: "albums")
|
||||||
|
}
|
||||||
|
|
||||||
|
model AlbumsAssetsAssets {
|
||||||
|
albumsId String @db.Uuid
|
||||||
|
assetsId String @db.Uuid
|
||||||
|
assets Assets @relation(fields: [assetsId], references: [id], onDelete: Cascade, map: "FK_4bd1303d199f4e72ccdf998c621")
|
||||||
|
albums Albums @relation(fields: [albumsId], references: [id], onDelete: Cascade, map: "FK_e590fa396c6898fcd4a50e40927")
|
||||||
|
|
||||||
|
@@id([albumsId, assetsId], map: "PK_c67bc36fa845fb7b18e0e398180")
|
||||||
|
@@index([assetsId], map: "IDX_4bd1303d199f4e72ccdf998c62")
|
||||||
|
@@index([albumsId], map: "IDX_e590fa396c6898fcd4a50e4092")
|
||||||
|
@@map(name: "albums_assets_assets")
|
||||||
|
}
|
||||||
|
|
||||||
|
model AlbumsSharedUsersUsers {
|
||||||
|
albumsId String @db.Uuid
|
||||||
|
usersId String @db.Uuid
|
||||||
|
albums Albums @relation(fields: [albumsId], references: [id], onDelete: Cascade, map: "FK_427c350ad49bd3935a50baab737")
|
||||||
|
users Users @relation(fields: [usersId], references: [id], onDelete: Cascade, map: "FK_f48513bf9bccefd6ff3ad30bd06")
|
||||||
|
|
||||||
|
@@id([albumsId, usersId], map: "PK_7df55657e0b2e8b626330a0ebc8")
|
||||||
|
@@index([albumsId], map: "IDX_427c350ad49bd3935a50baab73")
|
||||||
|
@@index([usersId], map: "IDX_f48513bf9bccefd6ff3ad30bd0")
|
||||||
|
@@map(name: "albums_shared_users_users")
|
||||||
|
}
|
||||||
|
|
||||||
|
model ApiKeys {
|
||||||
|
name String @db.VarChar
|
||||||
|
key String @db.VarChar
|
||||||
|
userId String @db.Uuid
|
||||||
|
createdAt DateTime @default(now()) @db.Timestamptz(6)
|
||||||
|
updatedAt DateTime @default(now()) @db.Timestamptz(6)
|
||||||
|
id String @id(map: "PK_5c8a79801b44bd27b79228e1dad") @default(dbgenerated("uuid_generate_v4()")) @db.Uuid
|
||||||
|
users Users @relation(fields: [userId], references: [id], onDelete: Cascade, map: "FK_6c2e267ae764a9413b863a29342")
|
||||||
|
|
||||||
|
@@map(name: "api_keys")
|
||||||
|
}
|
||||||
|
|
||||||
|
model AssetFaces {
|
||||||
|
assetId String @db.Uuid
|
||||||
|
personId String? @db.Uuid
|
||||||
|
embedding Unsupported("vector")
|
||||||
|
imageWidth Int @default(0)
|
||||||
|
imageHeight Int @default(0)
|
||||||
|
boundingBoxX1 Int @default(0)
|
||||||
|
boundingBoxY1 Int @default(0)
|
||||||
|
boundingBoxX2 Int @default(0)
|
||||||
|
boundingBoxY2 Int @default(0)
|
||||||
|
id String @id(map: "PK_6df76ab2eb6f5b57b7c2f1fc684") @default(dbgenerated("uuid_generate_v4()")) @db.Uuid
|
||||||
|
assets Assets @relation(fields: [assetId], references: [id], onDelete: Cascade, map: "FK_02a43fd0b3c50fb6d7f0cb7282c")
|
||||||
|
person Person? @relation("asset_faces_personIdToperson", fields: [personId], references: [id], map: "FK_95ad7106dd7b484275443f580f9")
|
||||||
|
person_person_faceAssetIdToasset_faces Person[] @relation("person_faceAssetIdToasset_faces")
|
||||||
|
|
||||||
|
@@index([assetId, personId], map: "IDX_asset_faces_assetId_personId")
|
||||||
|
@@index([assetId], map: "IDX_asset_faces_on_assetId")
|
||||||
|
@@index([personId], map: "IDX_asset_faces_personId")
|
||||||
|
@@index([personId, assetId], map: "IDX_bf339a24070dac7e71304ec530")
|
||||||
|
@@index([embedding], map: "face_index")
|
||||||
|
@@map(name: "asset_faces")
|
||||||
|
}
|
||||||
|
|
||||||
|
model AssetJobStatus {
|
||||||
|
assetId String @id(map: "PK_420bec36fc02813bddf5c8b73d4") @db.Uuid
|
||||||
|
facesRecognizedAt DateTime? @db.Timestamptz(6)
|
||||||
|
metadataExtractedAt DateTime? @db.Timestamptz(6)
|
||||||
|
duplicatesDetectedAt DateTime? @db.Timestamptz(6)
|
||||||
|
assets Assets @relation(fields: [assetId], references: [id], onDelete: Cascade, map: "FK_420bec36fc02813bddf5c8b73d4")
|
||||||
|
|
||||||
|
@@map(name: "asset_job_status")
|
||||||
|
}
|
||||||
|
|
||||||
|
model AssetStack {
|
||||||
|
id String @id(map: "PK_74a27e7fcbd5852463d0af3034b") @default(dbgenerated("uuid_generate_v4()")) @db.Uuid
|
||||||
|
primaryAssetId String @unique(map: "REL_91704e101438fd0653f582426d") @db.Uuid
|
||||||
|
primaryAsset Assets @relation("asset_stack_primaryAssetIdToassets", fields: [primaryAssetId], references: [id], onDelete: NoAction, onUpdate: NoAction, map: "FK_91704e101438fd0653f582426dc")
|
||||||
|
assets Assets[] @relation("assets_stackIdToasset_stack")
|
||||||
|
|
||||||
|
@@map(name: "asset_stack")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This model contains an expression index which requires additional setup for migrations. Visit https://pris.ly/d/expression-indexes for more info.
|
||||||
|
model Assets {
|
||||||
|
id String @id(map: "PK_da96729a8b113377cfb6a62439c") @default(dbgenerated("uuid_generate_v4()")) @db.Uuid
|
||||||
|
deviceAssetId String @db.VarChar
|
||||||
|
ownerId String @db.Uuid
|
||||||
|
deviceId String @db.VarChar
|
||||||
|
type String @db.VarChar
|
||||||
|
originalPath String @db.VarChar
|
||||||
|
previewPath String? @db.VarChar
|
||||||
|
fileCreatedAt DateTime @db.Timestamptz(6)
|
||||||
|
fileModifiedAt DateTime @db.Timestamptz(6)
|
||||||
|
isFavorite Boolean @default(false)
|
||||||
|
duration String? @db.VarChar
|
||||||
|
thumbnailPath String? @default("") @db.VarChar
|
||||||
|
encodedVideoPath String? @default("") @db.VarChar
|
||||||
|
checksum Bytes
|
||||||
|
isVisible Boolean @default(true)
|
||||||
|
livePhotoVideoId String? @unique(map: "UQ_16294b83fa8c0149719a1f631ef") @db.Uuid
|
||||||
|
updatedAt DateTime @default(now()) @db.Timestamptz(6)
|
||||||
|
createdAt DateTime @default(now()) @db.Timestamptz(6)
|
||||||
|
isArchived Boolean @default(false)
|
||||||
|
originalFileName String @db.VarChar
|
||||||
|
sidecarPath String? @db.VarChar
|
||||||
|
isReadOnly Boolean @default(false)
|
||||||
|
thumbhash Bytes?
|
||||||
|
isOffline Boolean @default(false)
|
||||||
|
libraryId String? @db.Uuid
|
||||||
|
isExternal Boolean @default(false)
|
||||||
|
deletedAt DateTime? @db.Timestamptz(6)
|
||||||
|
localDateTime DateTime @db.Timestamptz(6)
|
||||||
|
stackId String? @db.Uuid
|
||||||
|
duplicateId String? @db.Uuid
|
||||||
|
truncatedDate DateTime @default(dbgenerated("date_trunc('day', \"localDateTime\" at time zone 'UTC') at time zone 'UTC'")) @db.Timestamptz(6)
|
||||||
|
activity Activity[]
|
||||||
|
albums Albums[]
|
||||||
|
albumsAssetsAssets AlbumsAssetsAssets[]
|
||||||
|
faces AssetFaces[]
|
||||||
|
assetJobStatus AssetJobStatus?
|
||||||
|
assetStackAssetStackPrimaryAssetIdToAssets AssetStack? @relation("asset_stack_primaryAssetIdToassets")
|
||||||
|
livePhotoVideo Assets? @relation("assetsToassets", fields: [livePhotoVideoId], references: [id], map: "FK_16294b83fa8c0149719a1f631ef")
|
||||||
|
otherAssets Assets? @relation("assetsToassets")
|
||||||
|
owner Users @relation(fields: [ownerId], references: [id], onDelete: Cascade, map: "FK_2c5ac0d6fb58b238fd2068de67d")
|
||||||
|
library Libraries? @relation(fields: [libraryId], references: [id], onDelete: Cascade, map: "FK_9977c3c1de01c3d848039a6b90c")
|
||||||
|
stack AssetStack? @relation("assets_stackIdToasset_stack", fields: [stackId], references: [id], map: "FK_f15d48fa3ea5e4bda05ca8ab207")
|
||||||
|
exifInfo Exif?
|
||||||
|
sharedLinks SharedLinkAsset[]
|
||||||
|
smartInfo SmartInfo?
|
||||||
|
smartSearch SmartSearch?
|
||||||
|
tags TagAsset[]
|
||||||
|
|
||||||
|
@@unique([ownerId, libraryId, checksum], map: "UQ_assets_owner_library_checksum")
|
||||||
|
@@index([originalFileName], map: "IDX_4d66e76dada1ca180f67a205dc")
|
||||||
|
@@index([checksum], map: "IDX_8d3efe36c0755849395e6ea866")
|
||||||
|
@@index([id, stackId], map: "IDX_asset_id_stackId")
|
||||||
|
@@index([originalPath, libraryId], map: "IDX_originalPath_libraryId")
|
||||||
|
@@index([fileCreatedAt], map: "idx_asset_file_created_at")
|
||||||
|
@@map(name: "assets")
|
||||||
|
}
|
||||||
|
|
||||||
|
model Audit {
|
||||||
|
id Int @id(map: "PK_1d3d120ddaf7bc9b1ed68ed463a") @default(autoincrement())
|
||||||
|
entityType String @db.VarChar
|
||||||
|
entityId String @db.Uuid
|
||||||
|
action String @db.VarChar
|
||||||
|
ownerId String @db.Uuid
|
||||||
|
createdAt DateTime @default(now()) @db.Timestamptz(6)
|
||||||
|
|
||||||
|
@@index([ownerId, createdAt], map: "IDX_ownerId_createdAt")
|
||||||
|
@@map(name: "audit")
|
||||||
|
}
|
||||||
|
|
||||||
|
model Exif {
|
||||||
|
assetId String @id(map: "PK_c0117fdbc50b917ef9067740c44") @db.Uuid
|
||||||
|
make String? @db.VarChar
|
||||||
|
model String? @db.VarChar
|
||||||
|
exifImageWidth Int?
|
||||||
|
exifImageHeight Int?
|
||||||
|
fileSizeInByte BigInt?
|
||||||
|
orientation String? @db.VarChar
|
||||||
|
dateTimeOriginal DateTime? @db.Timestamptz(6)
|
||||||
|
modifyDate DateTime? @db.Timestamptz(6)
|
||||||
|
lensModel String? @db.VarChar
|
||||||
|
fNumber Float?
|
||||||
|
focalLength Float?
|
||||||
|
iso Int?
|
||||||
|
latitude Float?
|
||||||
|
longitude Float?
|
||||||
|
city String? @db.VarChar
|
||||||
|
state String? @db.VarChar
|
||||||
|
country String? @db.VarChar
|
||||||
|
description String @default("")
|
||||||
|
fps Float?
|
||||||
|
exposureTime String? @db.VarChar
|
||||||
|
livePhotoCID String? @db.VarChar
|
||||||
|
timeZone String? @db.VarChar
|
||||||
|
exifTextSearchableColumn Unsupported("tsvector") @default(dbgenerated("to_tsvector('english'::regconfig, (((((((((((((COALESCE(make, ''::character varying))::text || ' '::text) || (COALESCE(model, ''::character varying))::text) || ' '::text) || (COALESCE(orientation, ''::character varying))::text) || ' '::text) || (COALESCE(\"lensModel\", ''::character varying))::text) || ' '::text) || (COALESCE(city, ''::character varying))::text) || ' '::text) || (COALESCE(state, ''::character varying))::text) || ' '::text) || (COALESCE(country, ''::character varying))::text))"))
|
||||||
|
projectionType String? @db.VarChar
|
||||||
|
profileDescription String? @db.VarChar
|
||||||
|
colorspace String? @db.VarChar
|
||||||
|
bitsPerSample Int?
|
||||||
|
autoStackId String? @db.VarChar
|
||||||
|
assets Assets @relation(fields: [assetId], references: [id], onDelete: Cascade, onUpdate: NoAction, map: "FK_c0117fdbc50b917ef9067740c44")
|
||||||
|
|
||||||
|
@@index([autoStackId], map: "IDX_auto_stack_id")
|
||||||
|
@@index([livePhotoCID], map: "IDX_live_photo_cid")
|
||||||
|
@@index([city], map: "exif_city")
|
||||||
|
@@map(name: "exif")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This model contains an expression index which requires additional setup for migrations. Visit https://pris.ly/d/expression-indexes for more info.
|
||||||
|
model GeodataPlaces {
|
||||||
|
id Int @id(map: "PK_c29918988912ef4036f3d7fbff4")
|
||||||
|
name String @db.VarChar(200)
|
||||||
|
longitude Float
|
||||||
|
latitude Float
|
||||||
|
countryCode String @db.Char(2)
|
||||||
|
admin1Code String? @db.VarChar(20)
|
||||||
|
admin2Code String? @db.VarChar(80)
|
||||||
|
modificationDate DateTime @db.Date
|
||||||
|
earthCoord Unsupported("cube")? @default(dbgenerated("ll_to_earth(latitude, longitude)"))
|
||||||
|
admin1Name String? @db.VarChar
|
||||||
|
admin2Name String? @db.VarChar
|
||||||
|
alternateNames String? @db.VarChar
|
||||||
|
|
||||||
|
@@index([earthCoord], map: "IDX_geodata_gist_earthcoord", type: Gist)
|
||||||
|
@@map(name: "geodata_places")
|
||||||
|
}
|
||||||
|
|
||||||
|
model Libraries {
|
||||||
|
id String @id(map: "PK_505fedfcad00a09b3734b4223de") @default(dbgenerated("uuid_generate_v4()")) @db.Uuid
|
||||||
|
name String @db.VarChar
|
||||||
|
ownerId String @db.Uuid
|
||||||
|
type String @db.VarChar
|
||||||
|
importPaths String[]
|
||||||
|
exclusionPatterns String[]
|
||||||
|
createdAt DateTime @default(now()) @db.Timestamptz(6)
|
||||||
|
updatedAt DateTime @default(now()) @db.Timestamptz(6)
|
||||||
|
deletedAt DateTime? @db.Timestamptz(6)
|
||||||
|
refreshedAt DateTime? @db.Timestamptz(6)
|
||||||
|
isVisible Boolean @default(true)
|
||||||
|
assets Assets[]
|
||||||
|
owner Users @relation(fields: [ownerId], references: [id], onDelete: Cascade, map: "FK_0f6fc2fb195f24d19b0fb0d57c1")
|
||||||
|
|
||||||
|
@@map(name: "libraries")
|
||||||
|
}
|
||||||
|
|
||||||
|
model MoveHistory {
|
||||||
|
id String @id(map: "PK_af608f132233acf123f2949678d") @default(dbgenerated("uuid_generate_v4()")) @db.Uuid
|
||||||
|
entityId String @db.VarChar
|
||||||
|
pathType String @db.VarChar
|
||||||
|
oldPath String @db.VarChar
|
||||||
|
newPath String @unique(map: "UQ_newPath") @db.VarChar
|
||||||
|
|
||||||
|
@@unique([entityId, pathType], map: "UQ_entityId_pathType")
|
||||||
|
@@map(name: "move_history")
|
||||||
|
}
|
||||||
|
|
||||||
|
model Partners {
|
||||||
|
sharedById String @db.Uuid
|
||||||
|
sharedWithId String @db.Uuid
|
||||||
|
createdAt DateTime @default(now()) @db.Timestamptz(6)
|
||||||
|
updatedAt DateTime @default(now()) @db.Timestamptz(6)
|
||||||
|
inTimeline Boolean @default(false)
|
||||||
|
sharedBy Users @relation("partners_sharedByIdTousers", fields: [sharedById], references: [id], onDelete: Cascade, onUpdate: NoAction, map: "FK_7e077a8b70b3530138610ff5e04")
|
||||||
|
sharedWith Users @relation("partners_sharedWithIdTousers", fields: [sharedWithId], references: [id], onDelete: Cascade, onUpdate: NoAction, map: "FK_d7e875c6c60e661723dbf372fd3")
|
||||||
|
|
||||||
|
@@id([sharedById, sharedWithId], map: "PK_f1cc8f73d16b367f426261a8736")
|
||||||
|
@@map(name: "partners")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This table contains check constraints and requires additional setup for migrations. Visit https://pris.ly/d/check-constraints for more info.
|
||||||
|
model Person {
|
||||||
|
id String @id(map: "PK_5fdaf670315c4b7e70cce85daa3") @default(dbgenerated("uuid_generate_v4()")) @db.Uuid
|
||||||
|
createdAt DateTime @default(now()) @db.Timestamptz(6)
|
||||||
|
updatedAt DateTime @default(now()) @db.Timestamptz(6)
|
||||||
|
ownerId String @db.Uuid
|
||||||
|
name String @default("") @db.VarChar
|
||||||
|
thumbnailPath String @default("") @db.VarChar
|
||||||
|
isHidden Boolean @default(false)
|
||||||
|
birthDate DateTime? @db.Date
|
||||||
|
faceAssetId String? @db.Uuid
|
||||||
|
asset_faces_asset_faces_personIdToperson AssetFaces[] @relation("asset_faces_personIdToperson")
|
||||||
|
asset_faces_person_faceAssetIdToasset_faces AssetFaces? @relation("person_faceAssetIdToasset_faces", fields: [faceAssetId], references: [id], onUpdate: NoAction, map: "FK_2bbabe31656b6778c6b87b61023")
|
||||||
|
users Users @relation(fields: [ownerId], references: [id], onDelete: Cascade, map: "FK_5527cc99f530a547093f9e577b6")
|
||||||
|
|
||||||
|
@@map(name: "person")
|
||||||
|
}
|
||||||
|
|
||||||
|
model SharedLinkAsset {
|
||||||
|
assetsId String @db.Uuid
|
||||||
|
sharedLinksId String @db.Uuid
|
||||||
|
assets Assets @relation(fields: [assetsId], references: [id], onDelete: Cascade, map: "FK_5b7decce6c8d3db9593d6111a66")
|
||||||
|
sharedLinks SharedLinks @relation(fields: [sharedLinksId], references: [id], onDelete: Cascade, map: "FK_c9fab4aa97ffd1b034f3d6581ab")
|
||||||
|
|
||||||
|
@@id([assetsId, sharedLinksId], map: "PK_9b4f3687f9b31d1e311336b05e3")
|
||||||
|
@@index([assetsId], map: "IDX_5b7decce6c8d3db9593d6111a6")
|
||||||
|
@@index([sharedLinksId], map: "IDX_c9fab4aa97ffd1b034f3d6581a")
|
||||||
|
@@map(name: "shared_link__asset")
|
||||||
|
}
|
||||||
|
|
||||||
|
model SharedLinks {
|
||||||
|
id String @id(map: "PK_642e2b0f619e4876e5f90a43465") @default(dbgenerated("uuid_generate_v4()")) @db.Uuid
|
||||||
|
description String? @db.VarChar
|
||||||
|
userId String @db.Uuid
|
||||||
|
key Bytes @unique(map: "UQ_sharedlink_key")
|
||||||
|
type String @db.VarChar
|
||||||
|
createdAt DateTime @default(now()) @db.Timestamptz(6)
|
||||||
|
expiresAt DateTime? @db.Timestamptz(6)
|
||||||
|
allowUpload Boolean @default(false)
|
||||||
|
albumId String? @db.Uuid
|
||||||
|
allowDownload Boolean @default(true)
|
||||||
|
showExif Boolean @default(true)
|
||||||
|
password String? @db.VarChar
|
||||||
|
assets SharedLinkAsset[]
|
||||||
|
albums Albums? @relation(fields: [albumId], references: [id], onDelete: Cascade, map: "FK_0c6ce9058c29f07cdf7014eac66")
|
||||||
|
users Users @relation(fields: [userId], references: [id], onDelete: Cascade, map: "FK_66fe3837414c5a9f1c33ca49340")
|
||||||
|
|
||||||
|
@@index([albumId], map: "IDX_sharedlink_albumId")
|
||||||
|
@@index([key], map: "IDX_sharedlink_key")
|
||||||
|
@@map(name: "shared_links")
|
||||||
|
}
|
||||||
|
|
||||||
|
model SmartInfo {
|
||||||
|
assetId String @id(map: "PK_5e3753aadd956110bf3ec0244ac") @db.Uuid
|
||||||
|
tags String[]
|
||||||
|
objects String[]
|
||||||
|
smartInfoTextSearchableColumn Unsupported("tsvector") @default(dbgenerated("to_tsvector('english'::regconfig, f_concat_ws(' '::text, (COALESCE(tags, ARRAY[]::text[]) || COALESCE(objects, ARRAY[]::text[]))))"))
|
||||||
|
assets Assets @relation(fields: [assetId], references: [id], onDelete: Cascade, onUpdate: NoAction, map: "FK_5e3753aadd956110bf3ec0244ac")
|
||||||
|
|
||||||
|
@@index([tags], map: "si_tags", type: Gin)
|
||||||
|
@@index([smartInfoTextSearchableColumn], map: "smart_info_text_searchable_idx", type: Gin)
|
||||||
|
@@map(name: "smart_info")
|
||||||
|
}
|
||||||
|
|
||||||
|
model SmartSearch {
|
||||||
|
assetId String @id @db.Uuid
|
||||||
|
embedding Unsupported("vector")
|
||||||
|
assets Assets @relation(fields: [assetId], references: [id], onDelete: Cascade, onUpdate: NoAction)
|
||||||
|
|
||||||
|
@@index([embedding], map: "clip_index")
|
||||||
|
@@map(name: "smart_search")
|
||||||
|
}
|
||||||
|
|
||||||
|
model SocketIoAttachments {
|
||||||
|
id BigInt @unique @default(autoincrement())
|
||||||
|
created_at DateTime? @default(now()) @db.Timestamptz(6)
|
||||||
|
payload Bytes?
|
||||||
|
|
||||||
|
@@map(name: "socket_io_attachments")
|
||||||
|
}
|
||||||
|
|
||||||
|
model SystemConfig {
|
||||||
|
key String @id(map: "PK_aab69295b445016f56731f4d535") @db.VarChar
|
||||||
|
value String? @db.VarChar
|
||||||
|
|
||||||
|
@@map(name: "system_config")
|
||||||
|
}
|
||||||
|
|
||||||
|
model SystemMetadata {
|
||||||
|
key String @id(map: "PK_fa94f6857470fb5b81ec6084465") @db.VarChar
|
||||||
|
value Json @default("{}")
|
||||||
|
|
||||||
|
@@map(name: "system_metadata")
|
||||||
|
}
|
||||||
|
|
||||||
|
model TagAsset {
|
||||||
|
assetsId String @db.Uuid
|
||||||
|
tagsId String @db.Uuid
|
||||||
|
tags Tags @relation(fields: [tagsId], references: [id], onDelete: NoAction, onUpdate: NoAction, map: "FK_e99f31ea4cdf3a2c35c7287eb42")
|
||||||
|
assets Assets @relation(fields: [assetsId], references: [id], onDelete: Cascade, map: "FK_f8e8a9e893cb5c54907f1b798e9")
|
||||||
|
|
||||||
|
@@id([assetsId, tagsId], map: "PK_ef5346fe522b5fb3bc96454747e")
|
||||||
|
@@index([tagsId], map: "IDX_e99f31ea4cdf3a2c35c7287eb4")
|
||||||
|
@@index([assetsId], map: "IDX_f8e8a9e893cb5c54907f1b798e")
|
||||||
|
@@index([assetsId, tagsId], map: "IDX_tag_asset_assetsId_tagsId")
|
||||||
|
@@map(name: "tag_asset")
|
||||||
|
}
|
||||||
|
|
||||||
|
model Tags {
|
||||||
|
id String @id(map: "PK_e7dc17249a1148a1970748eda99") @default(dbgenerated("uuid_generate_v4()")) @db.Uuid
|
||||||
|
type String @db.VarChar
|
||||||
|
name String @db.VarChar
|
||||||
|
userId String @db.Uuid
|
||||||
|
renameTagId String? @db.Uuid
|
||||||
|
tags TagAsset[]
|
||||||
|
users Users @relation(fields: [userId], references: [id], onDelete: NoAction, onUpdate: NoAction, map: "FK_92e67dc508c705dd66c94615576")
|
||||||
|
|
||||||
|
@@unique([name, userId], map: "UQ_tag_name_userId")
|
||||||
|
@@map(name: "tags")
|
||||||
|
}
|
||||||
|
|
||||||
|
model UserToken {
|
||||||
|
id String @id(map: "PK_48cb6b5c20faa63157b3c1baf7f") @default(dbgenerated("uuid_generate_v4()")) @db.Uuid
|
||||||
|
token String @db.VarChar
|
||||||
|
createdAt DateTime @default(now()) @db.Timestamptz(6)
|
||||||
|
updatedAt DateTime @default(now()) @db.Timestamptz(6)
|
||||||
|
userId String @db.Uuid
|
||||||
|
deviceType String @default("") @db.VarChar
|
||||||
|
deviceOS String @default("") @db.VarChar
|
||||||
|
users Users @relation(fields: [userId], references: [id], onDelete: Cascade, map: "FK_d37db50eecdf9b8ce4eedd2f918")
|
||||||
|
|
||||||
|
@@map(name: "user_token")
|
||||||
|
}
|
||||||
|
|
||||||
|
model Users {
|
||||||
|
id String @id(map: "PK_a3ffb1c0c8416b9fc6f907b7433") @default(dbgenerated("uuid_generate_v4()")) @db.Uuid
|
||||||
|
email String @unique(map: "UQ_97672ac88f789774dd47f7c8be3") @db.VarChar
|
||||||
|
password String @default("") @db.VarChar
|
||||||
|
createdAt DateTime @default(now()) @db.Timestamptz(6)
|
||||||
|
profileImagePath String @default("") @db.VarChar
|
||||||
|
isAdmin Boolean @default(false)
|
||||||
|
shouldChangePassword Boolean @default(true)
|
||||||
|
deletedAt DateTime? @db.Timestamptz(6)
|
||||||
|
oauthId String @default("") @db.VarChar
|
||||||
|
updatedAt DateTime @default(now()) @db.Timestamptz(6)
|
||||||
|
storageLabel String? @unique(map: "UQ_b309cf34fa58137c416b32cea3a") @db.VarChar
|
||||||
|
memoriesEnabled Boolean @default(true)
|
||||||
|
name String @default("") @db.VarChar
|
||||||
|
avatarColor String? @db.VarChar
|
||||||
|
quotaSizeInBytes BigInt?
|
||||||
|
quotaUsageInBytes BigInt @default(0)
|
||||||
|
status String @default("active") @db.VarChar
|
||||||
|
activity Activity[]
|
||||||
|
albums Albums[]
|
||||||
|
albumsSharedUsersUsers AlbumsSharedUsersUsers[]
|
||||||
|
apiKeys ApiKeys[]
|
||||||
|
assets Assets[]
|
||||||
|
libraries Libraries[]
|
||||||
|
sharedBy Partners[] @relation("partners_sharedByIdTousers")
|
||||||
|
sharedWith Partners[] @relation("partners_sharedWithIdTousers")
|
||||||
|
person Person[]
|
||||||
|
sharedLinks SharedLinks[]
|
||||||
|
tags Tags[]
|
||||||
|
userToken UserToken[]
|
||||||
|
|
||||||
|
@@map(name: "users")
|
||||||
|
}
|
File diff suppressed because it is too large
Load diff
|
@ -54,6 +54,7 @@ import { MoveRepository } from 'src/repositories/move.repository';
|
||||||
import { NotificationRepository } from 'src/repositories/notification.repository';
|
import { NotificationRepository } from 'src/repositories/notification.repository';
|
||||||
import { PartnerRepository } from 'src/repositories/partner.repository';
|
import { PartnerRepository } from 'src/repositories/partner.repository';
|
||||||
import { PersonRepository } from 'src/repositories/person.repository';
|
import { PersonRepository } from 'src/repositories/person.repository';
|
||||||
|
import { PrismaRepository } from 'src/repositories/prisma.repository';
|
||||||
import { SearchRepository } from 'src/repositories/search.repository';
|
import { SearchRepository } from 'src/repositories/search.repository';
|
||||||
import { ServerInfoRepository } from 'src/repositories/server-info.repository';
|
import { ServerInfoRepository } from 'src/repositories/server-info.repository';
|
||||||
import { SessionRepository } from 'src/repositories/session.repository';
|
import { SessionRepository } from 'src/repositories/session.repository';
|
||||||
|
@ -96,4 +97,5 @@ export const repositories = [
|
||||||
{ provide: ISystemMetadataRepository, useClass: SystemMetadataRepository },
|
{ provide: ISystemMetadataRepository, useClass: SystemMetadataRepository },
|
||||||
{ provide: ITagRepository, useClass: TagRepository },
|
{ provide: ITagRepository, useClass: TagRepository },
|
||||||
{ provide: IUserRepository, useClass: UserRepository },
|
{ provide: IUserRepository, useClass: UserRepository },
|
||||||
|
{ provide: PrismaRepository, useClass: PrismaRepository },
|
||||||
];
|
];
|
||||||
|
|
32
server/src/repositories/prisma.repository.ts
Normal file
32
server/src/repositories/prisma.repository.ts
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
import { Injectable, OnModuleDestroy, OnModuleInit } from '@nestjs/common';
|
||||||
|
import { PrismaClient } from '@prisma/client';
|
||||||
|
import { findNonDeletedExtension } from 'src/prisma/find-non-deleted';
|
||||||
|
import { kyselyExtension } from 'src/prisma/kysely';
|
||||||
|
import { metricsExtension } from 'src/prisma/metrics';
|
||||||
|
|
||||||
|
function extendClient(base: PrismaClient) {
|
||||||
|
return base.$extends(metricsExtension).$extends(findNonDeletedExtension).$extends(kyselyExtension);
|
||||||
|
}
|
||||||
|
|
||||||
|
class UntypedExtendedClient extends PrismaClient {
|
||||||
|
constructor(options?: ConstructorParameters<typeof PrismaClient>[0]) {
|
||||||
|
super(options);
|
||||||
|
|
||||||
|
return extendClient(this) as this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const ExtendedPrismaClient = UntypedExtendedClient as unknown as new (
|
||||||
|
options?: ConstructorParameters<typeof PrismaClient>[0],
|
||||||
|
) => ReturnType<typeof extendClient>;
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class PrismaRepository extends ExtendedPrismaClient implements OnModuleInit, OnModuleDestroy {
|
||||||
|
async onModuleInit() {
|
||||||
|
await this.$connect();
|
||||||
|
}
|
||||||
|
|
||||||
|
async onModuleDestroy() {
|
||||||
|
await this.$disconnect();
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,17 +1,14 @@
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { Kysely, OrderByDirectionExpression, sql } from 'kysely';
|
||||||
import { getVectorExtension } from 'src/database.config';
|
|
||||||
import { DummyValue, GenerateSql } from 'src/decorators';
|
import { DummyValue, GenerateSql } from 'src/decorators';
|
||||||
import { AssetFaceEntity } from 'src/entities/asset-face.entity';
|
import { AssetFaceEntity } from 'src/entities/asset-face.entity';
|
||||||
import { AssetEntity, AssetType } from 'src/entities/asset.entity';
|
import { AssetEntity, AssetType } from 'src/entities/asset.entity';
|
||||||
import { GeodataPlacesEntity } from 'src/entities/geodata-places.entity';
|
import { GeodataPlacesEntity } from 'src/entities/geodata-places.entity';
|
||||||
import { SmartInfoEntity } from 'src/entities/smart-info.entity';
|
|
||||||
import { SmartSearchEntity } from 'src/entities/smart-search.entity';
|
|
||||||
import { DatabaseExtension } from 'src/interfaces/database.interface';
|
|
||||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||||
import {
|
import {
|
||||||
AssetDuplicateResult,
|
AssetDuplicateResult,
|
||||||
AssetDuplicateSearch,
|
AssetDuplicateSearch,
|
||||||
|
AssetSearchBuilderOptions,
|
||||||
AssetSearchOptions,
|
AssetSearchOptions,
|
||||||
FaceEmbeddingSearch,
|
FaceEmbeddingSearch,
|
||||||
FaceSearchResult,
|
FaceSearchResult,
|
||||||
|
@ -19,40 +16,22 @@ import {
|
||||||
SearchPaginationOptions,
|
SearchPaginationOptions,
|
||||||
SmartSearchOptions,
|
SmartSearchOptions,
|
||||||
} from 'src/interfaces/search.interface';
|
} from 'src/interfaces/search.interface';
|
||||||
import { asVector, searchAssetBuilder } from 'src/utils/database';
|
import { DB } from 'src/prisma/generated/types';
|
||||||
|
import { PrismaRepository } from 'src/repositories/prisma.repository';
|
||||||
|
import { asVector, withExif, withFaces, withPeople, withSmartInfo } from 'src/utils/database';
|
||||||
import { Instrumentation } from 'src/utils/instrumentation';
|
import { Instrumentation } from 'src/utils/instrumentation';
|
||||||
import { getCLIPModelInfo } from 'src/utils/misc';
|
import { getCLIPModelInfo } from 'src/utils/misc';
|
||||||
import { Paginated, PaginationMode, PaginationResult, paginatedBuilder } from 'src/utils/pagination';
|
import { Paginated } from 'src/utils/pagination';
|
||||||
import { isValidInteger } from 'src/validation';
|
import { isValidInteger } from 'src/validation';
|
||||||
import { Repository, SelectQueryBuilder } from 'typeorm';
|
|
||||||
|
|
||||||
@Instrumentation()
|
@Instrumentation()
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class SearchRepository implements ISearchRepository {
|
export class SearchRepository implements ISearchRepository {
|
||||||
private faceColumns: string[];
|
|
||||||
private assetsByCityQuery: string;
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@InjectRepository(SmartInfoEntity) private repository: Repository<SmartInfoEntity>,
|
|
||||||
@InjectRepository(AssetEntity) private assetRepository: Repository<AssetEntity>,
|
|
||||||
@InjectRepository(AssetFaceEntity) private assetFaceRepository: Repository<AssetFaceEntity>,
|
|
||||||
@InjectRepository(SmartSearchEntity) private smartSearchRepository: Repository<SmartSearchEntity>,
|
|
||||||
@InjectRepository(GeodataPlacesEntity) private geodataPlacesRepository: Repository<GeodataPlacesEntity>,
|
|
||||||
@Inject(ILoggerRepository) private logger: ILoggerRepository,
|
@Inject(ILoggerRepository) private logger: ILoggerRepository,
|
||||||
|
private prismaRepository: PrismaRepository,
|
||||||
) {
|
) {
|
||||||
this.logger.setContext(SearchRepository.name);
|
this.logger.setContext(SearchRepository.name);
|
||||||
this.faceColumns = this.assetFaceRepository.manager.connection
|
|
||||||
.getMetadata(AssetFaceEntity)
|
|
||||||
.ownColumns.map((column) => column.propertyName)
|
|
||||||
.filter((propertyName) => propertyName !== 'embedding');
|
|
||||||
this.assetsByCityQuery =
|
|
||||||
assetsByCityCte +
|
|
||||||
this.assetRepository
|
|
||||||
.createQueryBuilder('asset')
|
|
||||||
.innerJoinAndSelect('asset.exifInfo', 'exif')
|
|
||||||
.withDeleted()
|
|
||||||
.getQuery() +
|
|
||||||
' INNER JOIN cte ON asset.id = cte."assetId" ORDER BY exif.city';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async init(modelName: string): Promise<void> {
|
async init(modelName: string): Promise<void> {
|
||||||
|
@ -80,23 +59,16 @@ export class SearchRepository implements ISearchRepository {
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
async searchMetadata(pagination: SearchPaginationOptions, options: AssetSearchOptions): Paginated<AssetEntity> {
|
async searchMetadata(pagination: SearchPaginationOptions, options: AssetSearchOptions): Paginated<AssetEntity> {
|
||||||
let builder = this.assetRepository.createQueryBuilder('asset');
|
const orderDirection = (options.orderDirection?.toLowerCase() || 'desc') as OrderByDirectionExpression;
|
||||||
builder = searchAssetBuilder(builder, options);
|
const builder = this.searchAssetBuilder(options)
|
||||||
|
.orderBy('assets.fileCreatedAt', orderDirection)
|
||||||
|
.limit(pagination.size + 1)
|
||||||
|
.offset((pagination.page - 1) * pagination.size);
|
||||||
|
|
||||||
builder.orderBy('asset.fileCreatedAt', options.orderDirection ?? 'DESC');
|
const items = (await builder.execute()) as any as AssetEntity[];
|
||||||
return paginatedBuilder<AssetEntity>(builder, {
|
const hasNextPage = items.length > pagination.size;
|
||||||
mode: PaginationMode.SKIP_TAKE,
|
items.splice(pagination.size);
|
||||||
skip: (pagination.page - 1) * pagination.size,
|
return { items, hasNextPage };
|
||||||
take: pagination.size,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private createPersonFilter(builder: SelectQueryBuilder<AssetFaceEntity>, personIds: string[]) {
|
|
||||||
return builder
|
|
||||||
.select(`${builder.alias}."assetId"`)
|
|
||||||
.where(`${builder.alias}."personId" IN (:...personIds)`, { personIds })
|
|
||||||
.groupBy(`${builder.alias}."assetId"`)
|
|
||||||
.having(`COUNT(DISTINCT ${builder.alias}."personId") = :personCount`, { personCount: personIds.length });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@GenerateSql({
|
@GenerateSql({
|
||||||
|
@ -112,39 +84,26 @@ export class SearchRepository implements ISearchRepository {
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
async searchSmart(
|
async searchSmart(pagination: SearchPaginationOptions, options: SmartSearchOptions): Paginated<AssetEntity> {
|
||||||
pagination: SearchPaginationOptions,
|
if (!isValidInteger(pagination.size, { min: 1, max: 1000 })) {
|
||||||
{ embedding, userIds, personIds, ...options }: SmartSearchOptions,
|
throw new Error(`Invalid value for 'size': ${pagination.size}`);
|
||||||
): Paginated<AssetEntity> {
|
}
|
||||||
let results: PaginationResult<AssetEntity> = { items: [], hasNextPage: false };
|
|
||||||
|
|
||||||
await this.assetRepository.manager.transaction(async (manager) => {
|
let items: AssetEntity[] = [];
|
||||||
let builder = manager.createQueryBuilder(AssetEntity, 'asset');
|
await this.prismaRepository.$transaction(async (tx) => {
|
||||||
|
await tx.$queryRawUnsafe(`SET LOCAL vectors.hnsw_ef_search = ${pagination.size + 1}`);
|
||||||
|
const builder = this.searchAssetBuilder(options, tx.$kysely)
|
||||||
|
.innerJoin('smart_search', 'assets.id', 'smart_search.assetId')
|
||||||
|
.orderBy(sql`smart_search.embedding <=> ${asVector(options.embedding)}::vector`)
|
||||||
|
.limit(pagination.size + 1)
|
||||||
|
.offset((pagination.page - 1) * pagination.size);
|
||||||
|
|
||||||
if (personIds?.length) {
|
items = (await builder.execute()) as any as AssetEntity[];
|
||||||
const assetFaceBuilder = manager.createQueryBuilder(AssetFaceEntity, 'asset_face');
|
|
||||||
const cte = this.createPersonFilter(assetFaceBuilder, personIds);
|
|
||||||
builder
|
|
||||||
.addCommonTableExpression(cte, 'asset_face_ids')
|
|
||||||
.innerJoin('asset_face_ids', 'a', 'a."assetId" = asset.id');
|
|
||||||
}
|
|
||||||
|
|
||||||
builder = searchAssetBuilder(builder, options);
|
|
||||||
builder
|
|
||||||
.innerJoin('asset.smartSearch', 'search')
|
|
||||||
.andWhere('asset.ownerId IN (:...userIds )')
|
|
||||||
.orderBy('search.embedding <=> :embedding')
|
|
||||||
.setParameters({ userIds, embedding: asVector(embedding) });
|
|
||||||
|
|
||||||
await manager.query(this.getRuntimeConfig(pagination.size));
|
|
||||||
results = await paginatedBuilder<AssetEntity>(builder, {
|
|
||||||
mode: PaginationMode.LIMIT_OFFSET,
|
|
||||||
skip: (pagination.page - 1) * pagination.size,
|
|
||||||
take: pagination.size,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return results;
|
const hasNextPage = items.length > pagination.size;
|
||||||
|
items.splice(pagination.size);
|
||||||
|
return { items, hasNextPage };
|
||||||
}
|
}
|
||||||
|
|
||||||
@GenerateSql({
|
@GenerateSql({
|
||||||
|
@ -163,31 +122,28 @@ export class SearchRepository implements ISearchRepository {
|
||||||
type,
|
type,
|
||||||
userIds,
|
userIds,
|
||||||
}: AssetDuplicateSearch): Promise<AssetDuplicateResult[]> {
|
}: AssetDuplicateSearch): Promise<AssetDuplicateResult[]> {
|
||||||
const cte = this.assetRepository.createQueryBuilder('asset');
|
const vector = asVector(embedding);
|
||||||
cte
|
return this.prismaRepository.$kysely
|
||||||
.select('search.assetId', 'assetId')
|
.with('cte', (qb) =>
|
||||||
.addSelect('asset.duplicateId', 'duplicateId')
|
qb
|
||||||
.addSelect(`search.embedding <=> :embedding`, 'distance')
|
.selectFrom('assets')
|
||||||
.innerJoin('asset.smartSearch', 'search')
|
.select([
|
||||||
.where('asset.ownerId IN (:...userIds )')
|
'assets.id as assetId',
|
||||||
.andWhere('asset.id != :assetId')
|
'assets.duplicateId',
|
||||||
.andWhere('asset.isVisible = :isVisible')
|
sql<number>`smart_search.embedding <=> ${vector}::vector`.as('distance'),
|
||||||
.andWhere('asset.type = :type')
|
])
|
||||||
.orderBy('search.embedding <=> :embedding')
|
.innerJoin('smart_search', 'assets.id', 'smart_search.assetId')
|
||||||
.limit(64)
|
.where('assets.ownerId', '=', sql<string>`ANY(ARRAY[${userIds}]::uuid[])`)
|
||||||
.setParameters({ assetId, embedding: asVector(embedding), isVisible: true, type, userIds });
|
.where('assets.isVisible', '=', true)
|
||||||
|
.where('assets.type', '=', type)
|
||||||
const builder = this.assetRepository.manager
|
.where('assets.id', '!=', assetId)
|
||||||
.createQueryBuilder()
|
.orderBy(sql`smart_search.embedding <=> ${vector}::vector`)
|
||||||
.addCommonTableExpression(cte, 'cte')
|
.limit(64),
|
||||||
.from('cte', 'res')
|
)
|
||||||
.select('res.*');
|
.selectFrom('cte')
|
||||||
|
.selectAll()
|
||||||
if (maxDistance) {
|
.where('cte.distance', '<=', maxDistance as number)
|
||||||
builder.where('res.distance <= :maxDistance', { maxDistance });
|
.execute();
|
||||||
}
|
|
||||||
|
|
||||||
return builder.getRawMany() as Promise<AssetDuplicateResult[]>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@GenerateSql({
|
@GenerateSql({
|
||||||
|
@ -200,104 +156,107 @@ export class SearchRepository implements ISearchRepository {
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
async searchFaces({
|
searchFaces({
|
||||||
userIds,
|
userIds,
|
||||||
embedding,
|
embedding,
|
||||||
numResults,
|
numResults,
|
||||||
maxDistance,
|
maxDistance,
|
||||||
hasPerson,
|
hasPerson,
|
||||||
}: FaceEmbeddingSearch): Promise<FaceSearchResult[]> {
|
}: FaceEmbeddingSearch): Promise<FaceSearchResult[]> {
|
||||||
if (!isValidInteger(numResults, { min: 1 })) {
|
if (!isValidInteger(numResults, { min: 1, max: 1000 })) {
|
||||||
throw new Error(`Invalid value for 'numResults': ${numResults}`);
|
throw new Error(`Invalid value for 'numResults': ${numResults}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// setting this too low messes with prefilter recall
|
// setting this too low messes with prefilter recall
|
||||||
numResults = Math.max(numResults, 64);
|
numResults = Math.max(numResults, 64);
|
||||||
|
const vector = asVector(embedding);
|
||||||
let results: Array<AssetFaceEntity & { distance: number }> = [];
|
return this.prismaRepository.$transaction(async (tx) => {
|
||||||
await this.assetRepository.manager.transaction(async (manager) => {
|
await tx.$queryRawUnsafe(`SET LOCAL vectors.hnsw_ef_search = ${numResults}`);
|
||||||
const cte = manager
|
return tx.$kysely
|
||||||
.createQueryBuilder(AssetFaceEntity, 'faces')
|
.with('cte', (qb) =>
|
||||||
.select('search.embedding <=> :embedding', 'distance')
|
qb
|
||||||
.innerJoin('faces.asset', 'asset')
|
.selectFrom('asset_faces')
|
||||||
.innerJoin('faces.faceSearch', 'search')
|
.select([
|
||||||
.where('asset.ownerId IN (:...userIds )')
|
(eb) => eb.fn.toJson(sql`asset_faces.*`).as('face'),
|
||||||
.orderBy('search.embedding <=> :embedding')
|
sql<number>`asset_faces.embedding <=> ${vector}::vector`.as('distance'),
|
||||||
.setParameters({ userIds, embedding: asVector(embedding) });
|
])
|
||||||
|
.innerJoin('assets', 'assets.id', 'asset_faces.assetId')
|
||||||
cte.limit(numResults);
|
.where('assets.ownerId', '=', sql<string>`ANY(ARRAY[${userIds}]::uuid[])`)
|
||||||
|
.$if(!!hasPerson, (qb) => qb.where('asset_faces.personId', 'is not', null))
|
||||||
if (hasPerson) {
|
.orderBy(sql`asset_faces.embedding <=> ${vector}::vector`)
|
||||||
cte.andWhere('faces."personId" IS NOT NULL');
|
.limit(numResults),
|
||||||
}
|
)
|
||||||
|
.selectFrom('cte')
|
||||||
for (const col of this.faceColumns) {
|
.selectAll()
|
||||||
cte.addSelect(`faces.${col}`, col);
|
.where('cte.distance', '<=', maxDistance)
|
||||||
}
|
.execute() as any as Array<{ face: AssetFaceEntity; distance: number }>;
|
||||||
|
|
||||||
await manager.query(this.getRuntimeConfig(numResults));
|
|
||||||
results = await manager
|
|
||||||
.createQueryBuilder()
|
|
||||||
.select('res.*')
|
|
||||||
.addCommonTableExpression(cte, 'cte')
|
|
||||||
.from('cte', 'res')
|
|
||||||
.where('res.distance <= :maxDistance', { maxDistance })
|
|
||||||
.orderBy('res.distance')
|
|
||||||
.getRawMany();
|
|
||||||
});
|
});
|
||||||
return results.map((row) => ({
|
|
||||||
face: this.assetFaceRepository.create(row),
|
|
||||||
distance: row.distance,
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@GenerateSql({ params: [DummyValue.STRING] })
|
@GenerateSql({ params: [DummyValue.STRING] })
|
||||||
async searchPlaces(placeName: string): Promise<GeodataPlacesEntity[]> {
|
searchPlaces(placeName: string): Promise<GeodataPlacesEntity[]> {
|
||||||
return await this.geodataPlacesRepository
|
const contains = '%>>' as any as 'ilike';
|
||||||
.createQueryBuilder('geoplaces')
|
return this.prismaRepository.$kysely
|
||||||
.where(`f_unaccent(name) %>> f_unaccent(:placeName)`)
|
.selectFrom('geodata_places')
|
||||||
.orWhere(`f_unaccent("admin2Name") %>> f_unaccent(:placeName)`)
|
.selectAll()
|
||||||
.orWhere(`f_unaccent("admin1Name") %>> f_unaccent(:placeName)`)
|
.where((eb) =>
|
||||||
.orWhere(`f_unaccent("alternateNames") %>> f_unaccent(:placeName)`)
|
eb.or([
|
||||||
|
eb(eb.fn('f_unaccent', ['name']), contains, eb.fn('f_unaccent', [eb.val(placeName)])),
|
||||||
|
eb(eb.fn('f_unaccent', ['admin2Name']), contains, eb.fn('f_unaccent', [eb.val(placeName)])),
|
||||||
|
eb(eb.fn('f_unaccent', ['admin1Name']), contains, eb.fn('f_unaccent', [eb.val(placeName)])),
|
||||||
|
eb(eb.fn('f_unaccent', ['alternateNames']), contains, eb.fn('f_unaccent', [eb.val(placeName)])),
|
||||||
|
]),
|
||||||
|
)
|
||||||
.orderBy(
|
.orderBy(
|
||||||
`
|
sql`
|
||||||
COALESCE(f_unaccent(name) <->>> f_unaccent(:placeName), 0.1) +
|
COALESCE(f_unaccent(name) <->>> f_unaccent(${placeName}), 0.1) +
|
||||||
COALESCE(f_unaccent("admin2Name") <->>> f_unaccent(:placeName), 0.1) +
|
COALESCE(f_unaccent("admin2Name") <->>> f_unaccent(${placeName}), 0.1) +
|
||||||
COALESCE(f_unaccent("admin1Name") <->>> f_unaccent(:placeName), 0.1) +
|
COALESCE(f_unaccent("admin1Name") <->>> f_unaccent(${placeName}), 0.1) +
|
||||||
COALESCE(f_unaccent("alternateNames") <->>> f_unaccent(:placeName), 0.1)
|
COALESCE(f_unaccent("alternateNames") <->>> f_unaccent(${placeName}), 0.1)
|
||||||
`,
|
`,
|
||||||
)
|
)
|
||||||
.setParameters({ placeName })
|
|
||||||
.limit(20)
|
.limit(20)
|
||||||
.getMany();
|
.execute() as Promise<GeodataPlacesEntity[]>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@GenerateSql({ params: [[DummyValue.UUID]] })
|
@GenerateSql({ params: [[DummyValue.UUID]] })
|
||||||
async getAssetsByCity(userIds: string[]): Promise<AssetEntity[]> {
|
getAssetsByCity(userIds: string[]): Promise<AssetEntity[]> {
|
||||||
const parameters = [userIds, true, false, AssetType.IMAGE];
|
// the performance difference between this and the normal way is too huge to ignore, e.g. 3s vs 4ms
|
||||||
const rawRes = await this.repository.query(this.assetsByCityQuery, parameters);
|
return this.prismaRepository.$queryRaw`WITH RECURSIVE cte AS (
|
||||||
|
(
|
||||||
|
SELECT city, "assetId"
|
||||||
|
FROM exif
|
||||||
|
INNER JOIN assets ON exif."assetId" = assets.id
|
||||||
|
WHERE "ownerId" = ANY(ARRAY[${userIds}]::uuid[]) AND "isVisible" = true AND "isArchived" = false AND type = 'IMAGE'
|
||||||
|
ORDER BY city
|
||||||
|
LIMIT 1
|
||||||
|
)
|
||||||
|
|
||||||
const items: AssetEntity[] = [];
|
UNION ALL
|
||||||
for (const res of rawRes) {
|
|
||||||
const item = { exifInfo: {} as Record<string, any> } as Record<string, any>;
|
|
||||||
for (const [key, value] of Object.entries(res)) {
|
|
||||||
if (key.startsWith('exif_')) {
|
|
||||||
item.exifInfo[key.replace('exif_', '')] = value;
|
|
||||||
} else {
|
|
||||||
item[key.replace('asset_', '')] = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
items.push(item as AssetEntity);
|
|
||||||
}
|
|
||||||
|
|
||||||
return items;
|
SELECT l.city, l."assetId"
|
||||||
|
FROM cte c
|
||||||
|
, LATERAL (
|
||||||
|
SELECT city, "assetId"
|
||||||
|
FROM exif
|
||||||
|
INNER JOIN assets ON exif."assetId" = assets.id
|
||||||
|
WHERE city > c.city AND "ownerId" = ANY(ARRAY[${userIds}]::uuid[]) AND "isVisible" = true AND "isArchived" = false AND type = 'IMAGE'
|
||||||
|
ORDER BY city
|
||||||
|
LIMIT 1
|
||||||
|
) l
|
||||||
|
)
|
||||||
|
select "assets".*, json_strip_nulls(to_json(exif.*)) as "exifInfo"
|
||||||
|
from "assets"
|
||||||
|
inner join "exif" on "assets"."id" = "exif"."assetId"
|
||||||
|
inner join "cte" on "assets"."id" = "cte"."assetId"`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async upsert(assetId: string, embedding: number[]): Promise<void> {
|
async upsert(assetId: string, embedding: number[]): Promise<void> {
|
||||||
await this.smartSearchRepository.upsert(
|
await this.prismaRepository.$kysely
|
||||||
{ assetId, embedding: () => asVector(embedding, true) },
|
.insertInto('smart_search')
|
||||||
{ conflictPaths: ['assetId'] },
|
.values({ assetId, embedding: asVector(embedding, true) } as any)
|
||||||
);
|
.onConflict((oc) => oc.column('assetId').doUpdateSet({ embedding: asVector(embedding, true) } as any))
|
||||||
|
.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async updateDimSize(dimSize: number): Promise<void> {
|
private async updateDimSize(dimSize: number): Promise<void> {
|
||||||
|
@ -312,28 +271,28 @@ export class SearchRepository implements ISearchRepository {
|
||||||
|
|
||||||
this.logger.log(`Updating database CLIP dimension size to ${dimSize}.`);
|
this.logger.log(`Updating database CLIP dimension size to ${dimSize}.`);
|
||||||
|
|
||||||
await this.smartSearchRepository.manager.transaction(async (manager) => {
|
await this.prismaRepository.$transaction(async (tx) => {
|
||||||
await manager.clear(SmartSearchEntity);
|
await tx.$queryRawUnsafe(`TRUNCATE smart_search`);
|
||||||
await manager.query(`ALTER TABLE smart_search ALTER COLUMN embedding SET DATA TYPE vector(${dimSize})`);
|
await tx.$queryRawUnsafe(`ALTER TABLE smart_search ALTER COLUMN embedding SET DATA TYPE vector(${dimSize})`);
|
||||||
await manager.query(`REINDEX INDEX clip_index`);
|
await tx.$queryRawUnsafe(`REINDEX INDEX clip_index`);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.logger.log(`Successfully updated database CLIP dimension size from ${curDimSize} to ${dimSize}.`);
|
this.logger.log(`Successfully updated database CLIP dimension size from ${curDimSize} to ${dimSize}.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteAllSearchEmbeddings(): Promise<void> {
|
deleteAllSearchEmbeddings(): Promise<void> {
|
||||||
return this.smartSearchRepository.clear();
|
return this.prismaRepository.$queryRawUnsafe(`TRUNCATE smart_search`);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getDimSize(): Promise<number> {
|
private async getDimSize(): Promise<number> {
|
||||||
const res = await this.smartSearchRepository.manager.query(`
|
const res = await this.prismaRepository.$queryRaw<[{ dimsize: number }]>`
|
||||||
SELECT atttypmod as dimsize
|
SELECT atttypmod as dimsize
|
||||||
FROM pg_attribute f
|
FROM pg_attribute f
|
||||||
JOIN pg_class c ON c.oid = f.attrelid
|
JOIN pg_class c ON c.oid = f.attrelid
|
||||||
WHERE c.relkind = 'r'::char
|
WHERE c.relkind = 'r'::char
|
||||||
AND f.attnum > 0
|
AND f.attnum > 0
|
||||||
AND c.relname = 'smart_search'
|
AND c.relname = 'smart_search'
|
||||||
AND f.attname = 'embedding'`);
|
AND f.attname = 'embedding'`;
|
||||||
|
|
||||||
const dimSize = res[0]['dimsize'];
|
const dimSize = res[0]['dimsize'];
|
||||||
if (!isValidInteger(dimSize, { min: 1, max: 2 ** 16 })) {
|
if (!isValidInteger(dimSize, { min: 1, max: 2 ** 16 })) {
|
||||||
|
@ -342,43 +301,90 @@ export class SearchRepository implements ISearchRepository {
|
||||||
return dimSize;
|
return dimSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
private getRuntimeConfig(numResults?: number): string {
|
private searchAssetBuilder(options: AssetSearchBuilderOptions, kysely: Kysely<DB> = this.prismaRepository.$kysely) {
|
||||||
if (getVectorExtension() === DatabaseExtension.VECTOR) {
|
options.isArchived ??= options.withArchived ? undefined : false;
|
||||||
return 'SET LOCAL hnsw.ef_search = 1000;'; // mitigate post-filter recall
|
options.withDeleted ??= !!(options.trashedAfter || options.trashedBefore);
|
||||||
}
|
const query = kysely
|
||||||
|
.selectFrom('assets')
|
||||||
let runtimeConfig = 'SET LOCAL vectors.enable_prefilter=on; SET LOCAL vectors.search_mode=vbase;';
|
.selectAll('assets')
|
||||||
if (numResults) {
|
.$if(!!options.createdBefore, (qb) => qb.where('assets.createdAt', '<=', options.createdBefore as Date))
|
||||||
runtimeConfig += ` SET LOCAL vectors.hnsw_ef_search = ${numResults};`;
|
.$if(!!options.createdAfter, (qb) => qb.where('assets.createdAt', '>=', options.createdAfter as Date))
|
||||||
}
|
.$if(!!options.updatedBefore, (qb) => qb.where('assets.updatedAt', '<=', options.updatedBefore as Date))
|
||||||
|
.$if(!!options.updatedAfter, (qb) => qb.where('assets.updatedAt', '>=', options.updatedAfter as Date))
|
||||||
return runtimeConfig;
|
.$if(!!options.trashedBefore, (qb) => qb.where('assets.deletedAt', '<=', options.trashedBefore as Date))
|
||||||
|
.$if(!!options.trashedAfter, (qb) => qb.where('assets.deletedAt', '>=', options.trashedAfter as Date))
|
||||||
|
.$if(!!options.takenBefore, (qb) => qb.where('assets.fileCreatedAt', '<=', options.takenBefore as Date))
|
||||||
|
.$if(!!options.takenAfter, (qb) => qb.where('assets.fileCreatedAt', '>=', options.takenAfter as Date))
|
||||||
|
.$if(!!options.city, (qb) =>
|
||||||
|
qb.leftJoin('exif', 'exif.assetId', 'assets.id').where('exif.city', '=', options.city as string),
|
||||||
|
)
|
||||||
|
.$if(!!options.country, (qb) =>
|
||||||
|
qb.leftJoin('exif', 'exif.assetId', 'assets.id').where('exif.country', '=', options.country as string),
|
||||||
|
)
|
||||||
|
.$if(!!options.lensModel, (qb) =>
|
||||||
|
qb.leftJoin('exif', 'exif.assetId', 'assets.id').where('exif.lensModel', '=', options.lensModel as string),
|
||||||
|
)
|
||||||
|
.$if(!!options.make, (qb) =>
|
||||||
|
qb.leftJoin('exif', 'exif.assetId', 'assets.id').where('exif.make', '=', options.make as string),
|
||||||
|
)
|
||||||
|
.$if(!!options.model, (qb) =>
|
||||||
|
qb.leftJoin('exif', 'exif.assetId', 'assets.id').where('exif.model', '=', options.model as string),
|
||||||
|
)
|
||||||
|
.$if(!!options.state, (qb) =>
|
||||||
|
qb.leftJoin('exif', 'exif.assetId', 'assets.id').where('exif.state', '=', options.state as string),
|
||||||
|
)
|
||||||
|
.$if(!!options.checksum, (qb) => qb.where('assets.checksum', '=', options.checksum as Buffer))
|
||||||
|
.$if(!!options.deviceAssetId, (qb) => qb.where('assets.deviceAssetId', '=', options.deviceAssetId as string))
|
||||||
|
.$if(!!options.deviceId, (qb) => qb.where('assets.deviceId', '=', options.deviceId as string))
|
||||||
|
.$if(!!options.id, (qb) => qb.where('assets.id', '=', options.id as string))
|
||||||
|
.$if(!!options.libraryId, (qb) => qb.where('assets.libraryId', '=', options.libraryId as string))
|
||||||
|
.$if(!!options.userIds, (qb) =>
|
||||||
|
qb.where('assets.ownerId', '=', sql<string>`ANY(ARRAY[${options.userIds}]::uuid[])`),
|
||||||
|
)
|
||||||
|
.$if(!!options.encodedVideoPath, (qb) =>
|
||||||
|
qb.where('assets.encodedVideoPath', '=', options.encodedVideoPath as string),
|
||||||
|
)
|
||||||
|
.$if(!!options.originalPath, (qb) => qb.where('assets.originalPath', '=', options.originalPath as string))
|
||||||
|
.$if(!!options.previewPath, (qb) => qb.where('assets.previewPath', '=', options.previewPath as string))
|
||||||
|
.$if(!!options.thumbnailPath, (qb) => qb.where('assets.thumbnailPath', '=', options.thumbnailPath as string))
|
||||||
|
.$if(!!options.originalFileName, (qb) =>
|
||||||
|
qb.where(sql`f_unaccent(assets.originalFileName)`, 'ilike', sql`f_unaccent(${options.originalFileName})`),
|
||||||
|
)
|
||||||
|
.$if(!!options.isFavorite, (qb) => qb.where('assets.isFavorite', '=', options.isFavorite as boolean))
|
||||||
|
.$if(!!options.isOffline, (qb) => qb.where('assets.isOffline', '=', options.isOffline as boolean))
|
||||||
|
.$if(!!options.isVisible, (qb) => qb.where('assets.isVisible', '=', options.isVisible as boolean))
|
||||||
|
.$if(!!options.type, (qb) => qb.where('assets.type', '=', options.type as AssetType))
|
||||||
|
.$if(!!options.isArchived, (qb) => qb.where('assets.isArchived', '=', options.isArchived as boolean))
|
||||||
|
.$if(!!options.isEncoded, (qb) => qb.where('assets.encodedVideoPath', 'is not', null))
|
||||||
|
.$if(!!options.isMotion, (qb) => qb.where('assets.livePhotoVideoId', 'is not', null))
|
||||||
|
.$if(!!options.isNotInAlbum, (qb) =>
|
||||||
|
qb
|
||||||
|
.leftJoin('albums_assets_assets', 'albums_assets_assets.assetsId', 'assets.id')
|
||||||
|
.where('albums_assets_assets.assetsId', 'is', null),
|
||||||
|
)
|
||||||
|
.$if(!!options.withExif, (qb) => qb.select((eb) => withExif(eb)))
|
||||||
|
.$if(!!options.withSmartInfo, (qb) => qb.select((eb) => withSmartInfo(eb)))
|
||||||
|
.$if(!(!options.withFaces || options.withPeople), (qb) =>
|
||||||
|
qb.select((eb) => withFaces(eb)).$if(!!options.withPeople, (qb) => qb.select((eb) => withPeople(eb) as any)),
|
||||||
|
)
|
||||||
|
.$if(!!options.personIds && options.personIds.length > 0, (qb) =>
|
||||||
|
qb.innerJoin(
|
||||||
|
(eb: any) =>
|
||||||
|
eb
|
||||||
|
.selectFrom('asset_faces')
|
||||||
|
.select('asset_faces.assetId')
|
||||||
|
.where('asset_faces.personId', '=', sql`ANY(ARRAY[${options.personIds}]::uuid[])`)
|
||||||
|
.groupBy('asset_faces.assetId')
|
||||||
|
.having(
|
||||||
|
(eb: any) => eb.fn.count('asset_faces.personId').distinct(),
|
||||||
|
'=',
|
||||||
|
(options.personIds as string[]).length,
|
||||||
|
)
|
||||||
|
.as('personAssetIds'),
|
||||||
|
(join) => join.onRef('personAssetIds.assetId' as any, '=', 'assets.id' as any),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.$if(!options.withDeleted, (qb) => qb.where('assets.deletedAt', 'is', null));
|
||||||
|
return query;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// the performance difference between this and the normal way is too huge to ignore, e.g. 3s vs 4ms
|
|
||||||
const assetsByCityCte = `
|
|
||||||
WITH RECURSIVE cte AS (
|
|
||||||
(
|
|
||||||
SELECT city, "assetId"
|
|
||||||
FROM exif
|
|
||||||
INNER JOIN assets ON exif."assetId" = assets.id
|
|
||||||
WHERE "ownerId" = ANY($1::uuid[]) AND "isVisible" = $2 AND "isArchived" = $3 AND type = $4
|
|
||||||
ORDER BY city
|
|
||||||
LIMIT 1
|
|
||||||
)
|
|
||||||
|
|
||||||
UNION ALL
|
|
||||||
|
|
||||||
SELECT l.city, l."assetId"
|
|
||||||
FROM cte c
|
|
||||||
, LATERAL (
|
|
||||||
SELECT city, "assetId"
|
|
||||||
FROM exif
|
|
||||||
INNER JOIN assets ON exif."assetId" = assets.id
|
|
||||||
WHERE city > c.city AND "ownerId" = ANY($1::uuid[]) AND "isVisible" = $2 AND "isArchived" = $3 AND type = $4
|
|
||||||
ORDER BY city
|
|
||||||
LIMIT 1
|
|
||||||
) l
|
|
||||||
)
|
|
||||||
`;
|
|
||||||
|
|
|
@ -371,7 +371,7 @@ export class AssetMediaService {
|
||||||
localDateTime: dto.fileCreatedAt,
|
localDateTime: dto.fileCreatedAt,
|
||||||
duration: dto.duration || null,
|
duration: dto.duration || null,
|
||||||
|
|
||||||
livePhotoVideo: null,
|
livePhotoVideoId: null,
|
||||||
sidecarPath: sidecarPath || null,
|
sidecarPath: sidecarPath || null,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -218,14 +218,20 @@ describe(AssetService.name, () => {
|
||||||
it('should update the asset', async () => {
|
it('should update the asset', async () => {
|
||||||
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1']));
|
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1']));
|
||||||
assetMock.getById.mockResolvedValue(assetStub.image);
|
assetMock.getById.mockResolvedValue(assetStub.image);
|
||||||
|
assetMock.update.mockResolvedValue(assetStub.image);
|
||||||
|
|
||||||
await sut.update(authStub.admin, 'asset-1', { isFavorite: true });
|
await sut.update(authStub.admin, 'asset-1', { isFavorite: true });
|
||||||
|
|
||||||
expect(assetMock.update).toHaveBeenCalledWith({ id: 'asset-1', isFavorite: true });
|
expect(assetMock.update).toHaveBeenCalledWith({ id: 'asset-1', isFavorite: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should update the exif description', async () => {
|
it('should update the exif description', async () => {
|
||||||
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1']));
|
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1']));
|
||||||
assetMock.getById.mockResolvedValue(assetStub.image);
|
assetMock.getById.mockResolvedValue(assetStub.image);
|
||||||
|
assetMock.update.mockResolvedValue(assetStub.image);
|
||||||
|
|
||||||
await sut.update(authStub.admin, 'asset-1', { description: 'Test description' });
|
await sut.update(authStub.admin, 'asset-1', { description: 'Test description' });
|
||||||
|
|
||||||
expect(assetMock.upsertExif).toHaveBeenCalledWith({ assetId: 'asset-1', description: 'Test description' });
|
expect(assetMock.upsertExif).toHaveBeenCalledWith({ assetId: 'asset-1', description: 'Test description' });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -110,29 +110,26 @@ export class AssetService {
|
||||||
async get(auth: AuthDto, id: string): Promise<AssetResponseDto | SanitizedAssetResponseDto> {
|
async get(auth: AuthDto, id: string): Promise<AssetResponseDto | SanitizedAssetResponseDto> {
|
||||||
await this.access.requirePermission(auth, Permission.ASSET_READ, id);
|
await this.access.requirePermission(auth, Permission.ASSET_READ, id);
|
||||||
|
|
||||||
const asset = await this.assetRepository.getById(
|
const asset = await this.assetRepository.getById(id, {
|
||||||
id,
|
exifInfo: true,
|
||||||
{
|
tags: true,
|
||||||
exifInfo: true,
|
sharedLinks: true,
|
||||||
tags: true,
|
smartInfo: true,
|
||||||
sharedLinks: true,
|
owner: true,
|
||||||
smartInfo: true,
|
faces: {
|
||||||
owner: true,
|
include: { person: true },
|
||||||
faces: {
|
orderBy: { boundingBoxX1: 'asc' },
|
||||||
person: true,
|
},
|
||||||
},
|
stack: {
|
||||||
stack: {
|
include: {
|
||||||
assets: {
|
assets: {
|
||||||
exifInfo: true,
|
include: {
|
||||||
|
exifInfo: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
});
|
||||||
faces: {
|
|
||||||
boundingBoxX1: 'ASC',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!asset) {
|
if (!asset) {
|
||||||
throw new BadRequestException('Asset not found');
|
throw new BadRequestException('Asset not found');
|
||||||
|
@ -161,16 +158,7 @@ export class AssetService {
|
||||||
const { description, dateTimeOriginal, latitude, longitude, ...rest } = dto;
|
const { description, dateTimeOriginal, latitude, longitude, ...rest } = dto;
|
||||||
await this.updateMetadata({ id, description, dateTimeOriginal, latitude, longitude });
|
await this.updateMetadata({ id, description, dateTimeOriginal, latitude, longitude });
|
||||||
|
|
||||||
await this.assetRepository.update({ id, ...rest });
|
const asset = await this.assetRepository.update({ id, ...rest });
|
||||||
const asset = await this.assetRepository.getById(id, {
|
|
||||||
exifInfo: true,
|
|
||||||
owner: true,
|
|
||||||
smartInfo: true,
|
|
||||||
tags: true,
|
|
||||||
faces: {
|
|
||||||
person: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
if (!asset) {
|
if (!asset) {
|
||||||
throw new BadRequestException('Asset not found');
|
throw new BadRequestException('Asset not found');
|
||||||
}
|
}
|
||||||
|
@ -196,14 +184,16 @@ export class AssetService {
|
||||||
} else if (options.stackParentId) {
|
} else if (options.stackParentId) {
|
||||||
//Creating new stack if parent doesn't have one already. If it does, then we add to the existing stack
|
//Creating new stack if parent doesn't have one already. If it does, then we add to the existing stack
|
||||||
await this.access.requirePermission(auth, Permission.ASSET_UPDATE, options.stackParentId);
|
await this.access.requirePermission(auth, Permission.ASSET_UPDATE, options.stackParentId);
|
||||||
const primaryAsset = await this.assetRepository.getById(options.stackParentId, { stack: { assets: true } });
|
const primaryAsset = await this.assetRepository.getById(options.stackParentId, {
|
||||||
|
stack: { include: { assets: true } },
|
||||||
|
});
|
||||||
if (!primaryAsset) {
|
if (!primaryAsset) {
|
||||||
throw new BadRequestException('Asset not found for given stackParentId');
|
throw new BadRequestException('Asset not found for given stackParentId');
|
||||||
}
|
}
|
||||||
let stack = primaryAsset.stack;
|
let stack = primaryAsset.stack;
|
||||||
|
|
||||||
ids.push(options.stackParentId);
|
ids.push(options.stackParentId);
|
||||||
const assets = await this.assetRepository.getByIds(ids, { stack: { assets: true } });
|
const assets = await this.assetRepository.getByIds(ids, { stack: { include: { assets: true } } });
|
||||||
stackIdsToCheckForDelete.push(
|
stackIdsToCheckForDelete.push(
|
||||||
...new Set(assets.filter((a) => !!a.stackId && stack?.id !== a.stackId).map((a) => a.stackId!)),
|
...new Set(assets.filter((a) => !!a.stackId && stack?.id !== a.stackId).map((a) => a.stackId!)),
|
||||||
);
|
);
|
||||||
|
@ -273,10 +263,10 @@ export class AssetService {
|
||||||
|
|
||||||
const asset = await this.assetRepository.getById(id, {
|
const asset = await this.assetRepository.getById(id, {
|
||||||
faces: {
|
faces: {
|
||||||
person: true,
|
include: { person: true },
|
||||||
},
|
},
|
||||||
library: true,
|
library: true,
|
||||||
stack: { assets: true },
|
stack: { include: { assets: true } },
|
||||||
exifInfo: true,
|
exifInfo: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -351,11 +341,11 @@ export class AssetService {
|
||||||
const childIds: string[] = [];
|
const childIds: string[] = [];
|
||||||
const oldParent = await this.assetRepository.getById(oldParentId, {
|
const oldParent = await this.assetRepository.getById(oldParentId, {
|
||||||
faces: {
|
faces: {
|
||||||
person: true,
|
include: { person: true },
|
||||||
},
|
},
|
||||||
library: true,
|
library: true,
|
||||||
stack: {
|
stack: {
|
||||||
assets: true,
|
include: { assets: true },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (!oldParent?.stackId) {
|
if (!oldParent?.stackId) {
|
||||||
|
|
|
@ -37,7 +37,7 @@ export class DuplicateService {
|
||||||
}
|
}
|
||||||
|
|
||||||
async getDuplicates(auth: AuthDto): Promise<DuplicateResponseDto[]> {
|
async getDuplicates(auth: AuthDto): Promise<DuplicateResponseDto[]> {
|
||||||
const res = await this.assetRepository.getDuplicates({ userIds: [auth.user.id] });
|
const res = await this.assetRepository.getDuplicates(auth.user.id);
|
||||||
|
|
||||||
return mapDuplicateResponse(res.map((a) => mapAsset(a, { auth })));
|
return mapDuplicateResponse(res.map((a) => mapAsset(a, { auth })));
|
||||||
}
|
}
|
||||||
|
|
|
@ -319,13 +319,7 @@ export class PersonService {
|
||||||
return JobStatus.SKIPPED;
|
return JobStatus.SKIPPED;
|
||||||
}
|
}
|
||||||
|
|
||||||
const relations = {
|
const [asset] = await this.assetRepository.getByIds([id], { exifInfo: true, faces: true });
|
||||||
exifInfo: true,
|
|
||||||
faces: {
|
|
||||||
person: false,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const [asset] = await this.assetRepository.getByIds([id], relations);
|
|
||||||
if (!asset || !asset.previewPath || asset.faces?.length > 0) {
|
if (!asset || !asset.previewPath || asset.faces?.length > 0) {
|
||||||
return JobStatus.FAILED;
|
return JobStatus.FAILED;
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { IPartnerRepository } from 'src/interfaces/partner.interface';
|
||||||
import { TimelineService } from 'src/services/timeline.service';
|
import { TimelineService } from 'src/services/timeline.service';
|
||||||
import { assetStub } from 'test/fixtures/asset.stub';
|
import { assetStub } from 'test/fixtures/asset.stub';
|
||||||
import { authStub } from 'test/fixtures/auth.stub';
|
import { authStub } from 'test/fixtures/auth.stub';
|
||||||
|
import { partnerStub } from 'test/fixtures/partner.stub';
|
||||||
import { IAccessRepositoryMock, newAccessRepositoryMock } from 'test/repositories/access.repository.mock';
|
import { IAccessRepositoryMock, newAccessRepositoryMock } from 'test/repositories/access.repository.mock';
|
||||||
import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock';
|
import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock';
|
||||||
import { newPartnerRepositoryMock } from 'test/repositories/partner.repository.mock';
|
import { newPartnerRepositoryMock } from 'test/repositories/partner.repository.mock';
|
||||||
|
@ -52,6 +53,7 @@ describe(TimelineService.name, () => {
|
||||||
size: TimeBucketSize.DAY,
|
size: TimeBucketSize.DAY,
|
||||||
timeBucket: 'bucket',
|
timeBucket: 'bucket',
|
||||||
albumId: 'album-id',
|
albumId: 'album-id',
|
||||||
|
userIds: [authStub.admin.user.id],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -66,12 +68,15 @@ describe(TimelineService.name, () => {
|
||||||
userId: authStub.admin.user.id,
|
userId: authStub.admin.user.id,
|
||||||
}),
|
}),
|
||||||
).resolves.toEqual(expect.arrayContaining([expect.objectContaining({ id: 'asset-id' })]));
|
).resolves.toEqual(expect.arrayContaining([expect.objectContaining({ id: 'asset-id' })]));
|
||||||
expect(assetMock.getTimeBucket).toHaveBeenCalledWith('bucket', {
|
expect(assetMock.getTimeBucket).toHaveBeenCalledWith(
|
||||||
size: TimeBucketSize.DAY,
|
'bucket',
|
||||||
timeBucket: 'bucket',
|
expect.objectContaining({
|
||||||
isArchived: true,
|
size: TimeBucketSize.DAY,
|
||||||
userIds: [authStub.admin.user.id],
|
timeBucket: 'bucket',
|
||||||
});
|
isArchived: true,
|
||||||
|
userIds: [authStub.admin.user.id],
|
||||||
|
}),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return the assets for a library time bucket if user has library.read', async () => {
|
it('should return the assets for a library time bucket if user has library.read', async () => {
|
||||||
|
@ -84,11 +89,14 @@ describe(TimelineService.name, () => {
|
||||||
userId: authStub.admin.user.id,
|
userId: authStub.admin.user.id,
|
||||||
}),
|
}),
|
||||||
).resolves.toEqual(expect.arrayContaining([expect.objectContaining({ id: 'asset-id' })]));
|
).resolves.toEqual(expect.arrayContaining([expect.objectContaining({ id: 'asset-id' })]));
|
||||||
expect(assetMock.getTimeBucket).toHaveBeenCalledWith('bucket', {
|
expect(assetMock.getTimeBucket).toHaveBeenCalledWith(
|
||||||
size: TimeBucketSize.DAY,
|
'bucket',
|
||||||
timeBucket: 'bucket',
|
expect.objectContaining({
|
||||||
userIds: [authStub.admin.user.id],
|
size: TimeBucketSize.DAY,
|
||||||
});
|
timeBucket: 'bucket',
|
||||||
|
userIds: [authStub.admin.user.id],
|
||||||
|
}),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should throw an error if withParners is true and isArchived true or undefined', async () => {
|
it('should throw an error if withParners is true and isArchived true or undefined', async () => {
|
||||||
|
|
|
@ -60,15 +60,6 @@ export class TimelineService {
|
||||||
private async timeBucketChecks(auth: AuthDto, dto: TimeBucketDto) {
|
private async timeBucketChecks(auth: AuthDto, dto: TimeBucketDto) {
|
||||||
if (dto.albumId) {
|
if (dto.albumId) {
|
||||||
await this.accessCore.requirePermission(auth, Permission.ALBUM_READ, [dto.albumId]);
|
await this.accessCore.requirePermission(auth, Permission.ALBUM_READ, [dto.albumId]);
|
||||||
} else {
|
|
||||||
dto.userId = dto.userId || auth.user.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dto.userId) {
|
|
||||||
await this.accessCore.requirePermission(auth, Permission.TIMELINE_READ, [dto.userId]);
|
|
||||||
if (dto.isArchived !== false) {
|
|
||||||
await this.accessCore.requirePermission(auth, Permission.ARCHIVE_READ, [dto.userId]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dto.withPartners) {
|
if (dto.withPartners) {
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
|
import { ExpressionBuilder } from 'kysely';
|
||||||
|
import { jsonArrayFrom, jsonObjectFrom } from 'kysely/helpers/postgres';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { AssetEntity } from 'src/entities/asset.entity';
|
import { AssetEntity } from 'src/entities/asset.entity';
|
||||||
import { AssetSearchBuilderOptions } from 'src/interfaces/search.interface';
|
import { AssetSearchBuilderOptions } from 'src/interfaces/search.interface';
|
||||||
|
import { DB } from 'src/prisma/generated/types';
|
||||||
import { Between, IsNull, LessThanOrEqual, MoreThanOrEqual, Not, SelectQueryBuilder } from 'typeorm';
|
import { Between, IsNull, LessThanOrEqual, MoreThanOrEqual, Not, SelectQueryBuilder } from 'typeorm';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -142,3 +145,20 @@ export function searchAssetBuilder(
|
||||||
|
|
||||||
return builder;
|
return builder;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const withExif = (eb: ExpressionBuilder<DB, 'assets'>) =>
|
||||||
|
jsonObjectFrom(eb.selectFrom('exif').selectAll().whereRef('exif.assetId', '=', 'assets.id')).as('exifInfo');
|
||||||
|
|
||||||
|
export const withSmartInfo = (eb: ExpressionBuilder<DB, 'assets'>) =>
|
||||||
|
jsonObjectFrom(eb.selectFrom('smart_info').selectAll().whereRef('smart_info.assetId', '=', 'assets.id')).as(
|
||||||
|
'smartInfo',
|
||||||
|
);
|
||||||
|
|
||||||
|
export const withFaces = (eb: ExpressionBuilder<DB, 'assets'>) =>
|
||||||
|
jsonArrayFrom(eb.selectFrom('asset_faces').selectAll().whereRef('asset_faces.assetId', '=', 'assets.id')).as('faces');
|
||||||
|
|
||||||
|
export const withPeople = (eb: ExpressionBuilder<DB, 'assets' | 'asset_faces'>) =>
|
||||||
|
jsonObjectFrom(eb.selectFrom('person').selectAll().whereRef('asset_faces.personId', '=', 'person.id')).as('people');
|
||||||
|
|
||||||
|
export const withOwner = (eb: ExpressionBuilder<DB, 'assets'>) =>
|
||||||
|
jsonObjectFrom(eb.selectFrom('users').selectAll().whereRef('users.id', '=', 'assets.ownerId')).as('owner');
|
||||||
|
|
|
@ -37,7 +37,10 @@ export async function* usePagination<T>(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function paginationHelper<Entity extends ObjectLiteral>(items: Entity[], take: number): PaginationResult<Entity> {
|
export function paginationHelper<Entity extends ObjectLiteral>(
|
||||||
|
items: Entity[],
|
||||||
|
take: number,
|
||||||
|
): PaginationResult<Entity> {
|
||||||
const hasNextPage = items.length > take;
|
const hasNextPage = items.length > take;
|
||||||
items.splice(take);
|
items.splice(take);
|
||||||
|
|
||||||
|
|
|
@ -18,7 +18,8 @@
|
||||||
"preserveWatchOutput": true,
|
"preserveWatchOutput": true,
|
||||||
"baseUrl": "./",
|
"baseUrl": "./",
|
||||||
"jsx": "react",
|
"jsx": "react",
|
||||||
"types": ["vitest/globals"]
|
"types": ["vitest/globals"],
|
||||||
|
"noErrorTruncation": true
|
||||||
},
|
},
|
||||||
"exclude": ["dist", "node_modules", "upload"]
|
"exclude": ["dist", "node_modules", "upload"]
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue