diff --git a/server/Dockerfile b/server/Dockerfile index 36d3297080..15cc71256e 100644 --- a/server/Dockerfile +++ b/server/Dockerfile @@ -10,13 +10,17 @@ RUN npm ci && \ rm -rf node_modules/@img/sharp-libvips* && \ rm -rf node_modules/@img/sharp-linuxmusl-x64 COPY server . + +WORKDIR /usr/src/app/server +RUN npm run prisma:generate + +WORKDIR /usr/src/app ENV PATH="${PATH}:/usr/src/app/bin" \ IMMICH_ENV=development \ NVIDIA_DRIVER_CAPABILITIES=all \ NVIDIA_VISIBLE_DEVICES=all ENTRYPOINT ["tini", "--", "/bin/sh"] - FROM dev AS prod RUN npm run build diff --git a/server/package-lock.json b/server/package-lock.json index b3f413c014..7fb9be5544 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -24,6 +24,7 @@ "@opentelemetry/context-async-hooks": "^1.24.0", "@opentelemetry/exporter-prometheus": "^0.52.0", "@opentelemetry/sdk-node": "^0.52.0", + "@prisma/client": "^5.11.0", "@react-email/components": "^0.0.19", "@socket.io/postgres-adapter": "^0.3.1", "archiver": "^7.0.0", @@ -43,6 +44,7 @@ "ioredis": "^5.3.2", "joi": "^17.10.0", "js-yaml": "^4.1.0", + "kysely": "^0.27.3", "lodash": "^4.17.21", "luxon": "^3.4.2", "mnemonist": "^0.39.8", @@ -53,6 +55,7 @@ "openid-client": "^5.4.3", "pg": "^8.11.3", "picomatch": "^4.0.0", + "prisma-extension-kysely": "^2.1.0", "react-email": "^2.1.2", "reflect-metadata": "^0.2.0", "rxjs": "^7.8.1", @@ -96,6 +99,8 @@ "mock-fs": "^5.2.0", "prettier": "^3.0.2", "prettier-plugin-organize-imports": "^3.2.3", + "prisma": "^5.11.0", + "prisma-kysely": "^1.8.0", "rimraf": "^5.0.1", "source-map-support": "^0.5.21", "sql-formatter": "^15.0.0", @@ -321,6 +326,21 @@ "node": ">=0.12.0" } }, + "node_modules/@antfu/ni": { + "version": "0.21.8", + "resolved": "https://registry.npmjs.org/@antfu/ni/-/ni-0.21.8.tgz", + "integrity": "sha512-90X8pU2szlvw0AJo9EZMbYc2eQKkmO7mAdC4tD4r5co2Mm56MT37MIG8EyB7p4WRheuzGxuLDxJ63mF6+Zajiw==", + "dev": true, + "bin": { + "na": "bin/na.mjs", + "nci": "bin/nci.mjs", + "ni": "bin/ni.mjs", + "nlx": "bin/nlx.mjs", + "nr": "bin/nr.mjs", + "nu": "bin/nu.mjs", + "nun": "bin/nun.mjs" + } + }, "node_modules/@babel/code-frame": { "version": "7.24.6", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.6.tgz", @@ -732,6 +752,39 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, + "node_modules/@chevrotain/cst-dts-gen": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/@chevrotain/cst-dts-gen/-/cst-dts-gen-10.5.0.tgz", + "integrity": "sha512-lhmC/FyqQ2o7pGK4Om+hzuDrm9rhFYIJ/AXoQBeongmn870Xeb0L6oGEiuR8nohFNL5sMaQEJWCxr1oIVIVXrw==", + "dev": true, + "dependencies": { + "@chevrotain/gast": "10.5.0", + "@chevrotain/types": "10.5.0", + "lodash": "4.17.21" + } + }, + "node_modules/@chevrotain/gast": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/@chevrotain/gast/-/gast-10.5.0.tgz", + "integrity": "sha512-pXdMJ9XeDAbgOWKuD1Fldz4ieCs6+nLNmyVhe2gZVqoO7v8HXuHYs5OV2EzUtbuai37TlOAQHrTDvxMnvMJz3A==", + "dev": true, + "dependencies": { + "@chevrotain/types": "10.5.0", + "lodash": "4.17.21" + } + }, + "node_modules/@chevrotain/types": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-10.5.0.tgz", + "integrity": "sha512-f1MAia0x/pAVPWH/T73BJVyO2XU5tI4/iE7cnxb7tqdNTNhQI3Uq3XkqcoteTmD4t1aM0LbHCJOhgIDn07kl2A==", + "dev": true + }, + "node_modules/@chevrotain/utils": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/@chevrotain/utils/-/utils-10.5.0.tgz", + "integrity": "sha512-hBzuU5+JjB2cqNZyszkDHZgOSrUUT8V3dhgRl8Q9Gp6dAj/H5+KILGjbhDpc3Iy9qmqlm/akuOI2ut9VUtzJxQ==", + "dev": true + }, "node_modules/@colors/colors": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", @@ -2078,6 +2131,19 @@ "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.14.2.tgz", "integrity": "sha512-9b8mPpKrfeGRuhFH5iO1iwCLeIIsV6+H1sRfxbkoGXIyQE2BTsPd9zqSqQJ+pv5sJ/hT5M1zvOFL02MnEezFug==" }, + "node_modules/@mrleebo/prisma-ast": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@mrleebo/prisma-ast/-/prisma-ast-0.7.0.tgz", + "integrity": "sha512-GTPkYf1meO2UXXIrz/SIDFWz+P4kXo2PTt36LYh/oNxV1PieYi7ZgenQk4IV0ut71Je3Z8ZoNZ8Tr7v2c1X1pg==", + "dev": true, + "dependencies": { + "chevrotain": "^10.5.0", + "lilconfig": "^2.1.0" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/@msgpack/msgpack": { "version": "2.8.0", "resolved": "https://registry.npmjs.org/@msgpack/msgpack/-/msgpack-2.8.0.tgz", @@ -4371,6 +4437,554 @@ "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.25.tgz", "integrity": "sha512-j7P6Rgr3mmtdkeDGTe0E/aYyWEWVtc5yFXtHCRHs28/jptDEWfaVOc5T7cblqy1XKPPfCxJc/8DwQ5YgLOZOVQ==" }, + "node_modules/@prisma/client": { + "version": "5.13.0", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.13.0.tgz", + "integrity": "sha512-uYdfpPncbZ/syJyiYBwGZS8Gt1PTNoErNYMuqHDa2r30rNSFtgTA/LXsSk55R7pdRTMi5pHkeP9B14K6nHmwkg==", + "hasInstallScript": true, + "engines": { + "node": ">=16.13" + }, + "peerDependencies": { + "prisma": "*" + }, + "peerDependenciesMeta": { + "prisma": { + "optional": true + } + } + }, + "node_modules/@prisma/debug": { + "version": "5.13.0", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.13.0.tgz", + "integrity": "sha512-699iqlEvzyCj9ETrXhs8o8wQc/eVW+FigSsHpiskSFydhjVuwTJEfj/nIYqTaWFYuxiWQRfm3r01meuW97SZaQ==", + "devOptional": true + }, + "node_modules/@prisma/engines": { + "version": "5.13.0", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.13.0.tgz", + "integrity": "sha512-hIFLm4H1boj6CBZx55P4xKby9jgDTeDG0Jj3iXtwaaHmlD5JmiDkZhh8+DYWkTGchu+rRF36AVROLnk0oaqhHw==", + "devOptional": true, + "hasInstallScript": true, + "dependencies": { + "@prisma/debug": "5.13.0", + "@prisma/engines-version": "5.13.0-23.b9a39a7ee606c28e3455d0fd60e78c3ba82b1a2b", + "@prisma/fetch-engine": "5.13.0", + "@prisma/get-platform": "5.13.0" + } + }, + "node_modules/@prisma/engines-version": { + "version": "5.13.0-23.b9a39a7ee606c28e3455d0fd60e78c3ba82b1a2b", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.13.0-23.b9a39a7ee606c28e3455d0fd60e78c3ba82b1a2b.tgz", + "integrity": "sha512-AyUuhahTINGn8auyqYdmxsN+qn0mw3eg+uhkp8zwknXYIqoT3bChG4RqNY/nfDkPvzWAPBa9mrDyBeOnWSgO6A==", + "devOptional": true + }, + "node_modules/@prisma/fetch-engine": { + "version": "5.13.0", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.13.0.tgz", + "integrity": "sha512-Yh4W+t6YKyqgcSEB3odBXt7QyVSm0OQlBSldQF2SNXtmOgMX8D7PF/fvH6E6qBCpjB/yeJLy/FfwfFijoHI6sA==", + "devOptional": true, + "dependencies": { + "@prisma/debug": "5.13.0", + "@prisma/engines-version": "5.13.0-23.b9a39a7ee606c28e3455d0fd60e78c3ba82b1a2b", + "@prisma/get-platform": "5.13.0" + } + }, + "node_modules/@prisma/generator-helper": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/@prisma/generator-helper/-/generator-helper-5.3.1.tgz", + "integrity": "sha512-zrYS0iHLgPlOJjYnd5KvVMMvSS+ktOL39EwooS5EnyvfzwfzxlKCeOUgxTfiKYs0WUWqzEvyNAYtramYgSknsQ==", + "dev": true, + "dependencies": { + "@prisma/debug": "5.3.1", + "@types/cross-spawn": "6.0.2", + "cross-spawn": "7.0.3", + "kleur": "4.1.5" + } + }, + "node_modules/@prisma/generator-helper/node_modules/@prisma/debug": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.3.1.tgz", + "integrity": "sha512-eYrxqslEKf+wpMFIIHgbcNYuZBXUdiJLA85Or3TwOhgPIN1ZoXT9CwJph3ynW8H1Xg0LkdYLwVmuULCwiMoU5A==", + "dev": true, + "dependencies": { + "@types/debug": "4.1.8", + "debug": "4.3.4", + "strip-ansi": "6.0.1" + } + }, + "node_modules/@prisma/get-platform": { + "version": "5.13.0", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.13.0.tgz", + "integrity": "sha512-B/WrQwYTzwr7qCLifQzYOmQhZcFmIFhR81xC45gweInSUn2hTEbfKUPd2keAog+y5WI5xLAFNJ3wkXplvSVkSw==", + "devOptional": true, + "dependencies": { + "@prisma/debug": "5.13.0" + } + }, + "node_modules/@prisma/internals": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/@prisma/internals/-/internals-5.3.1.tgz", + "integrity": "sha512-zkW73hPHHNrMD21PeYgCTBfMu71vzJf+WtfydtJbS0JVJKyLfOel0iWSQg7wjNeQfccKp+NdHJ/5rTJ4NEUzgA==", + "dev": true, + "dependencies": { + "@antfu/ni": "0.21.8", + "@opentelemetry/api": "1.4.1", + "@prisma/debug": "5.3.1", + "@prisma/engines": "5.3.1", + "@prisma/fetch-engine": "5.3.1", + "@prisma/generator-helper": "5.3.1", + "@prisma/get-platform": "5.3.1", + "@prisma/prisma-schema-wasm": "5.3.1-2.61e140623197a131c2a6189271ffee05a7aa9a59", + "archiver": "5.3.2", + "arg": "5.0.2", + "checkpoint-client": "1.1.27", + "cli-truncate": "2.1.0", + "dotenv": "16.0.3", + "escape-string-regexp": "4.0.0", + "execa": "5.1.1", + "find-up": "5.0.0", + "fp-ts": "2.16.1", + "fs-extra": "11.1.1", + "fs-jetpack": "5.1.0", + "global-dirs": "3.0.1", + "globby": "11.1.0", + "indent-string": "4.0.0", + "is-windows": "1.0.2", + "is-wsl": "2.2.0", + "kleur": "4.1.5", + "new-github-issue-url": "0.2.1", + "node-fetch": "2.7.0", + "npm-packlist": "5.1.3", + "open": "7.4.2", + "p-map": "4.0.0", + "prompts": "2.4.2", + "read-pkg-up": "7.0.1", + "replace-string": "3.1.0", + "resolve": "1.22.4", + "string-width": "4.2.3", + "strip-ansi": "6.0.1", + "strip-indent": "3.0.0", + "temp-dir": "2.0.0", + "tempy": "1.0.1", + "terminal-link": "2.1.1", + "tmp": "0.2.1", + "ts-pattern": "4.3.0" + } + }, + "node_modules/@prisma/internals/node_modules/@opentelemetry/api": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.4.1.tgz", + "integrity": "sha512-O2yRJce1GOc6PAy3QxFM4NzFiWzvScDC1/5ihYBL6BUEVdq0XMWN01sppE+H6bBXbaFYipjwFLEWLg5PaSOThA==", + "dev": true, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@prisma/internals/node_modules/@prisma/debug": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.3.1.tgz", + "integrity": "sha512-eYrxqslEKf+wpMFIIHgbcNYuZBXUdiJLA85Or3TwOhgPIN1ZoXT9CwJph3ynW8H1Xg0LkdYLwVmuULCwiMoU5A==", + "dev": true, + "dependencies": { + "@types/debug": "4.1.8", + "debug": "4.3.4", + "strip-ansi": "6.0.1" + } + }, + "node_modules/@prisma/internals/node_modules/@prisma/engines": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.3.1.tgz", + "integrity": "sha512-6QkILNyfeeN67BNEPEtkgh3Xo2tm6D7V+UhrkBbRHqKw9CTaz/vvTP/ROwYSP/3JT2MtIutZm/EnhxUiuOPVDA==", + "dev": true, + "hasInstallScript": true + }, + "node_modules/@prisma/internals/node_modules/@prisma/fetch-engine": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.3.1.tgz", + "integrity": "sha512-w1yk1YiK8N82Pobdq58b85l6e8akyrkxuzwV9DoiUTRf3gpsuhJJesHc4Yi0WzUC9/3znizl1UfCsI6dhkj3Vw==", + "dev": true, + "dependencies": { + "@prisma/debug": "5.3.1", + "@prisma/get-platform": "5.3.1", + "execa": "5.1.1", + "find-cache-dir": "3.3.2", + "fs-extra": "11.1.1", + "hasha": "5.2.2", + "http-proxy-agent": "7.0.0", + "https-proxy-agent": "7.0.2", + "kleur": "4.1.5", + "node-fetch": "2.7.0", + "p-filter": "2.1.0", + "p-map": "4.0.0", + "p-retry": "4.6.2", + "progress": "2.0.3", + "rimraf": "3.0.2", + "temp-dir": "2.0.0", + "tempy": "1.0.1" + } + }, + "node_modules/@prisma/internals/node_modules/@prisma/get-platform": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.3.1.tgz", + "integrity": "sha512-3IiZY2BUjKnAuZ0569zppZE6/rZbVAM09//c2nvPbbkGG9MqrirA8fbhhF7tfVmhyVfdmVCHnf/ujWPHJ8B46Q==", + "dev": true, + "dependencies": { + "@prisma/debug": "5.3.1", + "escape-string-regexp": "4.0.0", + "execa": "5.1.1", + "fs-jetpack": "5.1.0", + "kleur": "4.1.5", + "replace-string": "3.1.0", + "strip-ansi": "6.0.1", + "tempy": "1.0.1", + "terminal-link": "2.1.1", + "ts-pattern": "4.3.0" + } + }, + "node_modules/@prisma/internals/node_modules/agent-base": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", + "dev": true, + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@prisma/internals/node_modules/archiver": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-5.3.2.tgz", + "integrity": "sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==", + "dev": true, + "dependencies": { + "archiver-utils": "^2.1.0", + "async": "^3.2.4", + "buffer-crc32": "^0.2.1", + "readable-stream": "^3.6.0", + "readdir-glob": "^1.1.2", + "tar-stream": "^2.2.0", + "zip-stream": "^4.1.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@prisma/internals/node_modules/archiver-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-2.1.0.tgz", + "integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==", + "dev": true, + "dependencies": { + "glob": "^7.1.4", + "graceful-fs": "^4.2.0", + "lazystream": "^1.0.0", + "lodash.defaults": "^4.2.0", + "lodash.difference": "^4.5.0", + "lodash.flatten": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.union": "^4.6.0", + "normalize-path": "^3.0.0", + "readable-stream": "^2.0.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@prisma/internals/node_modules/archiver-utils/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/@prisma/internals/node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true + }, + "node_modules/@prisma/internals/node_modules/compress-commons": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-4.1.2.tgz", + "integrity": "sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==", + "dev": true, + "dependencies": { + "buffer-crc32": "^0.2.13", + "crc32-stream": "^4.0.2", + "normalize-path": "^3.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@prisma/internals/node_modules/crc32-stream": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-4.0.3.tgz", + "integrity": "sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==", + "dev": true, + "dependencies": { + "crc-32": "^1.2.0", + "readable-stream": "^3.4.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@prisma/internals/node_modules/dotenv": { + "version": "16.0.3", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.3.tgz", + "integrity": "sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/@prisma/internals/node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/@prisma/internals/node_modules/fs-extra": { + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.1.1.tgz", + "integrity": "sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/@prisma/internals/node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@prisma/internals/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@prisma/internals/node_modules/https-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.2.tgz", + "integrity": "sha512-NmLNjm6ucYwtcUmL7JQC1ZQ57LmHP4lT15FQ8D61nak1rO6DH+fz5qNK2Ap5UN4ZapYICE3/0KodcLYSPsPbaA==", + "dev": true, + "dependencies": { + "agent-base": "^7.0.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@prisma/internals/node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/@prisma/internals/node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@prisma/internals/node_modules/resolve": { + "version": "1.22.4", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.4.tgz", + "integrity": "sha512-PXNdCiPqDqeUou+w1C2eTQbNfxKSuMxqTCuvlmmMsk1NWHL5fRrhY6Pl0qEYYc6+QqGClco1Qj8XnjPego4wfg==", + "dev": true, + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@prisma/internals/node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@prisma/internals/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "node_modules/@prisma/internals/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, + "node_modules/@prisma/internals/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/@prisma/internals/node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/@prisma/internals/node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dev": true, + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@prisma/internals/node_modules/tmp": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", + "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", + "dev": true, + "dependencies": { + "rimraf": "^3.0.0" + }, + "engines": { + "node": ">=8.17.0" + } + }, + "node_modules/@prisma/internals/node_modules/zip-stream": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-4.1.1.tgz", + "integrity": "sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==", + "dev": true, + "dependencies": { + "archiver-utils": "^3.0.4", + "compress-commons": "^4.1.2", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@prisma/internals/node_modules/zip-stream/node_modules/archiver-utils": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-3.0.4.tgz", + "integrity": "sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==", + "dev": true, + "dependencies": { + "glob": "^7.2.3", + "graceful-fs": "^4.2.0", + "lazystream": "^1.0.0", + "lodash.defaults": "^4.2.0", + "lodash.difference": "^4.5.0", + "lodash.flatten": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.union": "^4.6.0", + "normalize-path": "^3.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@prisma/prisma-schema-wasm": { + "version": "5.3.1-2.61e140623197a131c2a6189271ffee05a7aa9a59", + "resolved": "https://registry.npmjs.org/@prisma/prisma-schema-wasm/-/prisma-schema-wasm-5.3.1-2.61e140623197a131c2a6189271ffee05a7aa9a59.tgz", + "integrity": "sha512-+zUI7NQDXfcNnU8HgrAj4jRMv8yRfITLzcfv0Urf0adKimM+hkkVG4rX38i9zWMlxekkEBw7NLFx3Gxxy8d3iQ==", + "dev": true + }, "node_modules/@protobufjs/aspromise": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", @@ -6032,6 +6646,24 @@ "@types/node": "*" } }, + "node_modules/@types/cross-spawn": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@types/cross-spawn/-/cross-spawn-6.0.2.tgz", + "integrity": "sha512-KuwNhp3eza+Rhu8IFI5HUXRP0LIhqH5cAjubUvGXXthh4YYBuP2ntwEX+Cz8GJoZUHlKo247wPWOfA9LYEq4cw==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/debug": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.8.tgz", + "integrity": "sha512-/vPO1EPOs306Cvhwv7KfVfYvOJqA/S/AXjaHQiJboCZzcNDb+TIJFN9/2C9DZ//ijSKWioNyUxD792QmDJ+HKQ==", + "dev": true, + "dependencies": { + "@types/ms": "*" + } + }, "node_modules/@types/docker-modem": { "version": "3.0.6", "resolved": "https://registry.npmjs.org/@types/docker-modem/-/docker-modem-3.0.6.tgz", @@ -6212,6 +6844,12 @@ "@types/node": "*" } }, + "node_modules/@types/ms": { + "version": "0.7.34", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz", + "integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==", + "dev": true + }, "node_modules/@types/multer": { "version": "1.4.11", "resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.4.11.tgz", @@ -6374,6 +7012,12 @@ "@types/node": "*" } }, + "node_modules/@types/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", + "dev": true + }, "node_modules/@types/semver": { "version": "7.5.8", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz", @@ -7043,6 +7687,19 @@ "node": ">= 6.0.0" } }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "dev": true, + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/ajv": { "version": "8.12.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", @@ -7374,6 +8031,15 @@ "node": "*" } }, + "node_modules/astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/async": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz", @@ -7889,6 +8555,99 @@ "node": "*" } }, + "node_modules/checkpoint-client": { + "version": "1.1.27", + "resolved": "https://registry.npmjs.org/checkpoint-client/-/checkpoint-client-1.1.27.tgz", + "integrity": "sha512-xstymfUalJOv6ZvTtmkwP4ORJN36ikT4PvrIoLe3wstbYf87XIXCcZrSmbFQOjyB0v1qbBnCsAscDpfdZlCkFA==", + "dev": true, + "dependencies": { + "ci-info": "3.8.0", + "env-paths": "2.2.1", + "make-dir": "4.0.0", + "ms": "2.1.3", + "node-fetch": "2.6.12", + "uuid": "9.0.0" + } + }, + "node_modules/checkpoint-client/node_modules/ci-info": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.8.0.tgz", + "integrity": "sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "engines": { + "node": ">=8" + } + }, + "node_modules/checkpoint-client/node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/checkpoint-client/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/checkpoint-client/node_modules/node-fetch": { + "version": "2.6.12", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.12.tgz", + "integrity": "sha512-C/fGU2E8ToujUivIO0H+tpQ6HWo4eEmchoPIoXtxCrVghxdKq+QOHqEZW7tuP3KlV3bC8FRMO5nMCC7Zm1VP6g==", + "dev": true, + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/checkpoint-client/node_modules/uuid": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", + "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==", + "dev": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/chevrotain": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-10.5.0.tgz", + "integrity": "sha512-Pkv5rBY3+CsHOYfV5g/Vs5JY9WTHHDEKOlohI2XeygaZhUeqhAlldZ8Hz9cRmxu709bvS08YzxHdTPHhffc13A==", + "dev": true, + "dependencies": { + "@chevrotain/cst-dts-gen": "10.5.0", + "@chevrotain/gast": "10.5.0", + "@chevrotain/types": "10.5.0", + "@chevrotain/utils": "10.5.0", + "lodash": "4.17.21", + "regexp-to-ast": "0.5.0" + } + }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -7984,6 +8743,15 @@ "node": ">=0.8.0" } }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/cli-cursor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", @@ -8041,6 +8809,22 @@ "@colors/colors": "1.5.0" } }, + "node_modules/cli-truncate": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz", + "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==", + "dev": true, + "dependencies": { + "slice-ansi": "^3.0.0", + "string-width": "^4.2.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/cli-width": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", @@ -8173,6 +8957,12 @@ "node": ">= 6" } }, + "node_modules/commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", + "dev": true + }, "node_modules/compress-commons": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-6.0.2.tgz", @@ -8490,6 +9280,15 @@ "node": ">= 8" } }, + "node_modules/crypto-random-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", + "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -8591,6 +9390,63 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/del": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/del/-/del-6.1.1.tgz", + "integrity": "sha512-ua8BhapfP0JUJKC/zV9yHHDW/rDoDxP4Zhn3AkA6/xT6gY7jYXJiaeyBZznYVujhZZET+UgcbZiQ7sN3WqcImg==", + "dev": true, + "dependencies": { + "globby": "^11.0.1", + "graceful-fs": "^4.2.4", + "is-glob": "^4.0.1", + "is-path-cwd": "^2.2.0", + "is-path-inside": "^3.0.2", + "p-map": "^4.0.0", + "rimraf": "^3.0.2", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/del/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/del/node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/delegates": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", @@ -8989,6 +9845,15 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -9797,6 +10662,23 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, + "node_modules/find-cache-dir": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", + "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", + "dev": true, + "dependencies": { + "commondir": "^1.0.1", + "make-dir": "^3.0.2", + "pkg-dir": "^4.1.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/avajs/find-cache-dir?sponsor=1" + } + }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -9942,6 +10824,12 @@ "node": ">= 0.6" } }, + "node_modules/fp-ts": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/fp-ts/-/fp-ts-2.16.1.tgz", + "integrity": "sha512-by7U5W8dkIzcvDofUcO42yl9JbnHTEDBrzu3pt5fKT+Z4Oy85I21K80EYJYdjQGC2qum4Vo55Ag57iiIK4FYuA==", + "dev": true + }, "node_modules/fraction.js": { "version": "4.3.7", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", @@ -10005,6 +10893,36 @@ "node": ">=12" } }, + "node_modules/fs-jetpack": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/fs-jetpack/-/fs-jetpack-5.1.0.tgz", + "integrity": "sha512-Xn4fDhLydXkuzepZVsr02jakLlmoARPy+YWIclo4kh0GyNGUHnTqeH/w/qIsVn50dFxtp8otPL2t/HcPJBbxUA==", + "dev": true, + "dependencies": { + "minimatch": "^5.1.0" + } + }, + "node_modules/fs-jetpack/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/fs-jetpack/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/fs-minipass": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", @@ -10315,6 +11233,30 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/global-dirs": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-3.0.1.tgz", + "integrity": "sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA==", + "dev": true, + "dependencies": { + "ini": "2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/global-dirs/node_modules/ini": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz", + "integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, "node_modules/globals": { "version": "13.22.0", "resolved": "https://registry.npmjs.org/globals/-/globals-13.22.0.tgz", @@ -10464,6 +11406,31 @@ "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==" }, + "node_modules/hasha": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/hasha/-/hasha-5.2.2.tgz", + "integrity": "sha512-Hrp5vIK/xr5SkeN2onO32H0MgNZ0f17HRNH39WfL0SYUNOTZ5Lz1TJ8Pajo/87dYGEFlLMm7mIc/k/s6Bvz9HQ==", + "dev": true, + "dependencies": { + "is-stream": "^2.0.0", + "type-fest": "^0.8.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/hasha/node_modules/type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/hasown": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.1.tgz", @@ -10551,6 +11518,31 @@ "node": ">= 0.8" } }, + "node_modules/http-proxy-agent": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.0.tgz", + "integrity": "sha512-+ZT+iBxVUQ1asugqnD6oWoRiS25AkjNfG085dKJGtGxkdwLQrMKU5wJr2bOOFAXzKcTuqq+7fZlTMgG3SRfIYQ==", + "dev": true, + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/http-proxy-agent/node_modules/agent-base": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", + "dev": true, + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/https-proxy-agent": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", @@ -10621,6 +11613,39 @@ "node": ">= 4" } }, + "node_modules/ignore-walk": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-5.0.1.tgz", + "integrity": "sha512-yemi4pMf51WKT7khInJqAvsIGzoqYXblnsz0ql8tM+yi1EKYTY1evX4NAbJrLL/Aanr2HyZeluqU+Oi7MGHokw==", + "dev": true, + "dependencies": { + "minimatch": "^5.0.1" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/ignore-walk/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/ignore-walk/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", @@ -10797,6 +11822,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true, + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -10840,6 +11880,15 @@ "node": ">=0.12.0" } }, + "node_modules/is-path-cwd": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz", + "integrity": "sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/is-path-inside": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", @@ -10870,6 +11919,27 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", @@ -11142,6 +12212,23 @@ "json-buffer": "3.0.1" } }, + "node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/kysely": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/kysely/-/kysely-0.27.3.tgz", + "integrity": "sha512-lG03Ru+XyOJFsjH3OMY6R/9U38IjDPfnOfDgO3ynhbDr+Dz8fak+X6L62vqu3iybQnj+lG84OttBuU9KY3L9kA==", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/lazystream": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", @@ -11921,6 +13008,15 @@ "@nestjs/core": "^9.0.0 || ^10.0.0" } }, + "node_modules/new-github-issue-url": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/new-github-issue-url/-/new-github-issue-url-0.2.1.tgz", + "integrity": "sha512-md4cGoxuT4T4d/HDOXbrUHkTKrp/vp+m3aOA7XXVYwNsUNMK49g3SQicTSeV5GIz/5QVGAeYRAOlyp9OvlgsYA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, "node_modules/next": { "version": "14.1.4", "resolved": "https://registry.npmjs.org/next/-/next-14.1.4.tgz", @@ -12101,6 +13197,85 @@ "node": ">=0.10.0" } }, + "node_modules/npm-bundled": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-2.0.1.tgz", + "integrity": "sha512-gZLxXdjEzE/+mOstGDqR6b0EkhJ+kM6fxM6vUuckuctuVPh80Q6pw/rSZj9s4Gex9GxWtIicO1pc8DB9KZWudw==", + "dev": true, + "dependencies": { + "npm-normalize-package-bin": "^2.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/npm-normalize-package-bin": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-2.0.0.tgz", + "integrity": "sha512-awzfKUO7v0FscrSpRoogyNm0sajikhBWpU0QMrW09AMi9n1PoKU6WaIqUzuJSQnpciZZmJ/jMZ2Egfmb/9LiWQ==", + "dev": true, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/npm-packlist": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-5.1.3.tgz", + "integrity": "sha512-263/0NGrn32YFYi4J533qzrQ/krmmrWwhKkzwTuM4f/07ug51odoaNjUexxO4vxlzURHcmYMH1QjvHjsNDKLVg==", + "dev": true, + "dependencies": { + "glob": "^8.0.1", + "ignore-walk": "^5.0.1", + "npm-bundled": "^2.0.0", + "npm-normalize-package-bin": "^2.0.0" + }, + "bin": { + "npm-packlist": "bin/index.js" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/npm-packlist/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/npm-packlist/node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm-packlist/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/npm-run-path": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", @@ -12222,6 +13397,22 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/open": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-7.4.2.tgz", + "integrity": "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==", + "dev": true, + "dependencies": { + "is-docker": "^2.0.0", + "is-wsl": "^2.1.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/openid-client": { "version": "5.6.5", "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.6.5.tgz", @@ -12306,6 +13497,27 @@ "node": ">=0.10.0" } }, + "node_modules/p-filter": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/p-filter/-/p-filter-2.1.0.tgz", + "integrity": "sha512-ZBxxZ5sL2HghephhpGAQdoskxplTwr7ICaehZwLIlfL6acuVgZPm8yBNuRAFBGEqtD/hmUeq9eqLg2ys9Xr/yw==", + "dev": true, + "dependencies": { + "p-map": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-filter/node_modules/p-map": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz", + "integrity": "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -12334,6 +13546,43 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "dev": true, + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-retry": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", + "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", + "dev": true, + "dependencies": { + "@types/retry": "0.12.0", + "retry": "^0.13.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-retry/node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, "node_modules/p-try": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", @@ -12639,6 +13888,70 @@ "node": ">= 6" } }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/pkg-types": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.1.0.tgz", @@ -12940,6 +14253,46 @@ "node": ">=6" } }, + "node_modules/prisma": { + "version": "5.13.0", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.13.0.tgz", + "integrity": "sha512-kGtcJaElNRAdAGsCNykFSZ7dBKpL14Cbs+VaQ8cECxQlRPDjBlMHNFYeYt0SKovAVy2Y65JXQwB3A5+zIQwnTg==", + "devOptional": true, + "hasInstallScript": true, + "dependencies": { + "@prisma/engines": "5.13.0" + }, + "bin": { + "prisma": "build/index.js" + }, + "engines": { + "node": ">=16.13" + } + }, + "node_modules/prisma-extension-kysely": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/prisma-extension-kysely/-/prisma-extension-kysely-2.1.0.tgz", + "integrity": "sha512-s1hujYjrNzfQc9Z79s93464mkTkkt9ZPqPELDRFe9jEmiIlM/JRXZtqIyyN3FM0GDkN/BDPVtpPtdC52XnjAcQ==", + "peerDependencies": { + "@prisma/client": "latest" + } + }, + "node_modules/prisma-kysely": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/prisma-kysely/-/prisma-kysely-1.8.0.tgz", + "integrity": "sha512-VpNpolZ8RXRgfU+j4R+fPZmX8EE95w3vJ2tt7+FwuiQc0leNTfLK5QLf3KbbPDes2rfjh3g20AjDxefQIo5GIA==", + "dev": true, + "dependencies": { + "@mrleebo/prisma-ast": "^0.7.0", + "@prisma/generator-helper": "5.3.1", + "@prisma/internals": "5.3.1", + "typescript": "^5.2.2", + "zod": "^3.22.2" + }, + "bin": { + "prisma-kysely": "dist/bin.js" + } + }, "node_modules/prismjs": { "version": "1.29.0", "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.29.0.tgz", @@ -12961,6 +14314,37 @@ "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/prompts/node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/proper-lockfile": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", @@ -14225,6 +15609,12 @@ "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" }, + "node_modules/regexp-to-ast": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/regexp-to-ast/-/regexp-to-ast-0.5.0.tgz", + "integrity": "sha512-tlbJqcMHnPKI9zSrystikWKwHkBqu2a/Sgw01h3zFjvYrMxEDYHzzoMZnUrbIfpTFEsoRnnviOXNCzFiSc54Qw==", + "dev": true + }, "node_modules/regexp-tree": { "version": "0.1.27", "resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.27.tgz", @@ -14264,6 +15654,18 @@ "node": ">=0.10" } }, + "node_modules/replace-string": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/replace-string/-/replace-string-3.1.0.tgz", + "integrity": "sha512-yPpxc4ZR2makceA9hy/jHNqc7QVkd4Je/N0WRHm6bs3PtivPuPynxE5ejU/mp5EhnCv8+uZL7vhz8rkluSlx+Q==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -14865,6 +16267,12 @@ "node": ">= 10" } }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true + }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -14874,6 +16282,20 @@ "node": ">=8" } }, + "node_modules/slice-ansi": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz", + "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/slice-source": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/slice-source/-/slice-source-0.4.1.tgz", @@ -15292,6 +16714,19 @@ "node": ">=8" } }, + "node_modules/supports-hyperlinks": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.3.0.tgz", + "integrity": "sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0", + "supports-color": "^7.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/supports-preserve-symlinks-flag": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", @@ -15494,6 +16929,62 @@ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, + "node_modules/temp-dir": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-2.0.0.tgz", + "integrity": "sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/tempy": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tempy/-/tempy-1.0.1.tgz", + "integrity": "sha512-biM9brNqxSc04Ee71hzFbryD11nX7VPhQQY32AdDmjFvodsRFz/3ufeoTZ6uYkRFfGo188tENcASNs3vTdsM0w==", + "dev": true, + "dependencies": { + "del": "^6.0.0", + "is-stream": "^2.0.0", + "temp-dir": "^2.0.0", + "type-fest": "^0.16.0", + "unique-string": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/tempy/node_modules/type-fest": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.16.0.tgz", + "integrity": "sha512-eaBzG6MxNzEn9kiwvtre90cXaNLkmadMWa1zQMs3XORCXNbsH/OewwbxC5ia9dCxIxnTAsSxXJaa/p5y8DlvJg==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/terminal-link": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-2.1.1.tgz", + "integrity": "sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ==", + "dev": true, + "dependencies": { + "ansi-escapes": "^4.2.1", + "supports-hyperlinks": "^2.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/terser": { "version": "5.27.0", "resolved": "https://registry.npmjs.org/terser/-/terser-5.27.0.tgz", @@ -16003,6 +17494,12 @@ } } }, + "node_modules/ts-pattern": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ts-pattern/-/ts-pattern-4.3.0.tgz", + "integrity": "sha512-pefrkcd4lmIVR0LA49Imjf9DYLK8vtWhqBPA3Ya1ir8xCW0O2yjL9dsCVvI7pCodLC5q7smNpEtDR2yVulQxOg==", + "dev": true + }, "node_modules/tsconfig-paths": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", @@ -16356,6 +17853,18 @@ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" }, + "node_modules/unique-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", + "integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==", + "dev": true, + "dependencies": { + "crypto-random-string": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/universalify": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", @@ -17067,6 +18576,15 @@ "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } + }, + "node_modules/zod": { + "version": "3.23.7", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.7.tgz", + "integrity": "sha512-NBeIoqbtOiUMomACV/y+V3Qfs9+Okr18vR5c/5pHClPpufWOrsx8TENboDPe265lFdfewX2yBtNTLPvnmCxwog==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } }, "dependencies": { @@ -17209,6 +18727,12 @@ } } }, + "@antfu/ni": { + "version": "0.21.8", + "resolved": "https://registry.npmjs.org/@antfu/ni/-/ni-0.21.8.tgz", + "integrity": "sha512-90X8pU2szlvw0AJo9EZMbYc2eQKkmO7mAdC4tD4r5co2Mm56MT37MIG8EyB7p4WRheuzGxuLDxJ63mF6+Zajiw==", + "dev": true + }, "@babel/code-frame": { "version": "7.24.6", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.6.tgz", @@ -17514,6 +19038,39 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, + "@chevrotain/cst-dts-gen": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/@chevrotain/cst-dts-gen/-/cst-dts-gen-10.5.0.tgz", + "integrity": "sha512-lhmC/FyqQ2o7pGK4Om+hzuDrm9rhFYIJ/AXoQBeongmn870Xeb0L6oGEiuR8nohFNL5sMaQEJWCxr1oIVIVXrw==", + "dev": true, + "requires": { + "@chevrotain/gast": "10.5.0", + "@chevrotain/types": "10.5.0", + "lodash": "4.17.21" + } + }, + "@chevrotain/gast": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/@chevrotain/gast/-/gast-10.5.0.tgz", + "integrity": "sha512-pXdMJ9XeDAbgOWKuD1Fldz4ieCs6+nLNmyVhe2gZVqoO7v8HXuHYs5OV2EzUtbuai37TlOAQHrTDvxMnvMJz3A==", + "dev": true, + "requires": { + "@chevrotain/types": "10.5.0", + "lodash": "4.17.21" + } + }, + "@chevrotain/types": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-10.5.0.tgz", + "integrity": "sha512-f1MAia0x/pAVPWH/T73BJVyO2XU5tI4/iE7cnxb7tqdNTNhQI3Uq3XkqcoteTmD4t1aM0LbHCJOhgIDn07kl2A==", + "dev": true + }, + "@chevrotain/utils": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/@chevrotain/utils/-/utils-10.5.0.tgz", + "integrity": "sha512-hBzuU5+JjB2cqNZyszkDHZgOSrUUT8V3dhgRl8Q9Gp6dAj/H5+KILGjbhDpc3Iy9qmqlm/akuOI2ut9VUtzJxQ==", + "dev": true + }, "@colors/colors": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", @@ -18233,6 +19790,16 @@ "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.14.2.tgz", "integrity": "sha512-9b8mPpKrfeGRuhFH5iO1iwCLeIIsV6+H1sRfxbkoGXIyQE2BTsPd9zqSqQJ+pv5sJ/hT5M1zvOFL02MnEezFug==" }, + "@mrleebo/prisma-ast": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@mrleebo/prisma-ast/-/prisma-ast-0.7.0.tgz", + "integrity": "sha512-GTPkYf1meO2UXXIrz/SIDFWz+P4kXo2PTt36LYh/oNxV1PieYi7ZgenQk4IV0ut71Je3Z8ZoNZ8Tr7v2c1X1pg==", + "dev": true, + "requires": { + "chevrotain": "^10.5.0", + "lilconfig": "^2.1.0" + } + }, "@msgpack/msgpack": { "version": "2.8.0", "resolved": "https://registry.npmjs.org/@msgpack/msgpack/-/msgpack-2.8.0.tgz", @@ -19663,6 +21230,471 @@ "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.25.tgz", "integrity": "sha512-j7P6Rgr3mmtdkeDGTe0E/aYyWEWVtc5yFXtHCRHs28/jptDEWfaVOc5T7cblqy1XKPPfCxJc/8DwQ5YgLOZOVQ==" }, + "@prisma/client": { + "version": "5.13.0", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.13.0.tgz", + "integrity": "sha512-uYdfpPncbZ/syJyiYBwGZS8Gt1PTNoErNYMuqHDa2r30rNSFtgTA/LXsSk55R7pdRTMi5pHkeP9B14K6nHmwkg==", + "requires": {} + }, + "@prisma/debug": { + "version": "5.13.0", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.13.0.tgz", + "integrity": "sha512-699iqlEvzyCj9ETrXhs8o8wQc/eVW+FigSsHpiskSFydhjVuwTJEfj/nIYqTaWFYuxiWQRfm3r01meuW97SZaQ==", + "devOptional": true + }, + "@prisma/engines": { + "version": "5.13.0", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.13.0.tgz", + "integrity": "sha512-hIFLm4H1boj6CBZx55P4xKby9jgDTeDG0Jj3iXtwaaHmlD5JmiDkZhh8+DYWkTGchu+rRF36AVROLnk0oaqhHw==", + "devOptional": true, + "requires": { + "@prisma/debug": "5.13.0", + "@prisma/engines-version": "5.13.0-23.b9a39a7ee606c28e3455d0fd60e78c3ba82b1a2b", + "@prisma/fetch-engine": "5.13.0", + "@prisma/get-platform": "5.13.0" + } + }, + "@prisma/engines-version": { + "version": "5.13.0-23.b9a39a7ee606c28e3455d0fd60e78c3ba82b1a2b", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.13.0-23.b9a39a7ee606c28e3455d0fd60e78c3ba82b1a2b.tgz", + "integrity": "sha512-AyUuhahTINGn8auyqYdmxsN+qn0mw3eg+uhkp8zwknXYIqoT3bChG4RqNY/nfDkPvzWAPBa9mrDyBeOnWSgO6A==", + "devOptional": true + }, + "@prisma/fetch-engine": { + "version": "5.13.0", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.13.0.tgz", + "integrity": "sha512-Yh4W+t6YKyqgcSEB3odBXt7QyVSm0OQlBSldQF2SNXtmOgMX8D7PF/fvH6E6qBCpjB/yeJLy/FfwfFijoHI6sA==", + "devOptional": true, + "requires": { + "@prisma/debug": "5.13.0", + "@prisma/engines-version": "5.13.0-23.b9a39a7ee606c28e3455d0fd60e78c3ba82b1a2b", + "@prisma/get-platform": "5.13.0" + } + }, + "@prisma/generator-helper": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/@prisma/generator-helper/-/generator-helper-5.3.1.tgz", + "integrity": "sha512-zrYS0iHLgPlOJjYnd5KvVMMvSS+ktOL39EwooS5EnyvfzwfzxlKCeOUgxTfiKYs0WUWqzEvyNAYtramYgSknsQ==", + "dev": true, + "requires": { + "@prisma/debug": "5.3.1", + "@types/cross-spawn": "6.0.2", + "cross-spawn": "7.0.3", + "kleur": "4.1.5" + }, + "dependencies": { + "@prisma/debug": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.3.1.tgz", + "integrity": "sha512-eYrxqslEKf+wpMFIIHgbcNYuZBXUdiJLA85Or3TwOhgPIN1ZoXT9CwJph3ynW8H1Xg0LkdYLwVmuULCwiMoU5A==", + "dev": true, + "requires": { + "@types/debug": "4.1.8", + "debug": "4.3.4", + "strip-ansi": "6.0.1" + } + } + } + }, + "@prisma/get-platform": { + "version": "5.13.0", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.13.0.tgz", + "integrity": "sha512-B/WrQwYTzwr7qCLifQzYOmQhZcFmIFhR81xC45gweInSUn2hTEbfKUPd2keAog+y5WI5xLAFNJ3wkXplvSVkSw==", + "devOptional": true, + "requires": { + "@prisma/debug": "5.13.0" + } + }, + "@prisma/internals": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/@prisma/internals/-/internals-5.3.1.tgz", + "integrity": "sha512-zkW73hPHHNrMD21PeYgCTBfMu71vzJf+WtfydtJbS0JVJKyLfOel0iWSQg7wjNeQfccKp+NdHJ/5rTJ4NEUzgA==", + "dev": true, + "requires": { + "@antfu/ni": "0.21.8", + "@opentelemetry/api": "1.4.1", + "@prisma/debug": "5.3.1", + "@prisma/engines": "5.3.1", + "@prisma/fetch-engine": "5.3.1", + "@prisma/generator-helper": "5.3.1", + "@prisma/get-platform": "5.3.1", + "@prisma/prisma-schema-wasm": "5.3.1-2.61e140623197a131c2a6189271ffee05a7aa9a59", + "archiver": "5.3.2", + "arg": "5.0.2", + "checkpoint-client": "1.1.27", + "cli-truncate": "2.1.0", + "dotenv": "16.0.3", + "escape-string-regexp": "4.0.0", + "execa": "5.1.1", + "find-up": "5.0.0", + "fp-ts": "2.16.1", + "fs-extra": "11.1.1", + "fs-jetpack": "5.1.0", + "global-dirs": "3.0.1", + "globby": "11.1.0", + "indent-string": "4.0.0", + "is-windows": "1.0.2", + "is-wsl": "2.2.0", + "kleur": "4.1.5", + "new-github-issue-url": "0.2.1", + "node-fetch": "2.7.0", + "npm-packlist": "5.1.3", + "open": "7.4.2", + "p-map": "4.0.0", + "prompts": "2.4.2", + "read-pkg-up": "7.0.1", + "replace-string": "3.1.0", + "resolve": "1.22.4", + "string-width": "4.2.3", + "strip-ansi": "6.0.1", + "strip-indent": "3.0.0", + "temp-dir": "2.0.0", + "tempy": "1.0.1", + "terminal-link": "2.1.1", + "tmp": "0.2.1", + "ts-pattern": "4.3.0" + }, + "dependencies": { + "@opentelemetry/api": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.4.1.tgz", + "integrity": "sha512-O2yRJce1GOc6PAy3QxFM4NzFiWzvScDC1/5ihYBL6BUEVdq0XMWN01sppE+H6bBXbaFYipjwFLEWLg5PaSOThA==", + "dev": true + }, + "@prisma/debug": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.3.1.tgz", + "integrity": "sha512-eYrxqslEKf+wpMFIIHgbcNYuZBXUdiJLA85Or3TwOhgPIN1ZoXT9CwJph3ynW8H1Xg0LkdYLwVmuULCwiMoU5A==", + "dev": true, + "requires": { + "@types/debug": "4.1.8", + "debug": "4.3.4", + "strip-ansi": "6.0.1" + } + }, + "@prisma/engines": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.3.1.tgz", + "integrity": "sha512-6QkILNyfeeN67BNEPEtkgh3Xo2tm6D7V+UhrkBbRHqKw9CTaz/vvTP/ROwYSP/3JT2MtIutZm/EnhxUiuOPVDA==", + "dev": true + }, + "@prisma/fetch-engine": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.3.1.tgz", + "integrity": "sha512-w1yk1YiK8N82Pobdq58b85l6e8akyrkxuzwV9DoiUTRf3gpsuhJJesHc4Yi0WzUC9/3znizl1UfCsI6dhkj3Vw==", + "dev": true, + "requires": { + "@prisma/debug": "5.3.1", + "@prisma/get-platform": "5.3.1", + "execa": "5.1.1", + "find-cache-dir": "3.3.2", + "fs-extra": "11.1.1", + "hasha": "5.2.2", + "http-proxy-agent": "7.0.0", + "https-proxy-agent": "7.0.2", + "kleur": "4.1.5", + "node-fetch": "2.7.0", + "p-filter": "2.1.0", + "p-map": "4.0.0", + "p-retry": "4.6.2", + "progress": "2.0.3", + "rimraf": "3.0.2", + "temp-dir": "2.0.0", + "tempy": "1.0.1" + } + }, + "@prisma/get-platform": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.3.1.tgz", + "integrity": "sha512-3IiZY2BUjKnAuZ0569zppZE6/rZbVAM09//c2nvPbbkGG9MqrirA8fbhhF7tfVmhyVfdmVCHnf/ujWPHJ8B46Q==", + "dev": true, + "requires": { + "@prisma/debug": "5.3.1", + "escape-string-regexp": "4.0.0", + "execa": "5.1.1", + "fs-jetpack": "5.1.0", + "kleur": "4.1.5", + "replace-string": "3.1.0", + "strip-ansi": "6.0.1", + "tempy": "1.0.1", + "terminal-link": "2.1.1", + "ts-pattern": "4.3.0" + } + }, + "agent-base": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", + "dev": true, + "requires": { + "debug": "^4.3.4" + } + }, + "archiver": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-5.3.2.tgz", + "integrity": "sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==", + "dev": true, + "requires": { + "archiver-utils": "^2.1.0", + "async": "^3.2.4", + "buffer-crc32": "^0.2.1", + "readable-stream": "^3.6.0", + "readdir-glob": "^1.1.2", + "tar-stream": "^2.2.0", + "zip-stream": "^4.1.0" + } + }, + "archiver-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-2.1.0.tgz", + "integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==", + "dev": true, + "requires": { + "glob": "^7.1.4", + "graceful-fs": "^4.2.0", + "lazystream": "^1.0.0", + "lodash.defaults": "^4.2.0", + "lodash.difference": "^4.5.0", + "lodash.flatten": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.union": "^4.6.0", + "normalize-path": "^3.0.0", + "readable-stream": "^2.0.0" + }, + "dependencies": { + "readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + } + } + }, + "arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true + }, + "compress-commons": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-4.1.2.tgz", + "integrity": "sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==", + "dev": true, + "requires": { + "buffer-crc32": "^0.2.13", + "crc32-stream": "^4.0.2", + "normalize-path": "^3.0.0", + "readable-stream": "^3.6.0" + } + }, + "crc32-stream": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-4.0.3.tgz", + "integrity": "sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==", + "dev": true, + "requires": { + "crc-32": "^1.2.0", + "readable-stream": "^3.4.0" + } + }, + "dotenv": { + "version": "16.0.3", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.3.tgz", + "integrity": "sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==", + "dev": true + }, + "execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + } + }, + "fs-extra": { + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.1.1.tgz", + "integrity": "sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + } + }, + "get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true + }, + "glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "https-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.2.tgz", + "integrity": "sha512-NmLNjm6ucYwtcUmL7JQC1ZQ57LmHP4lT15FQ8D61nak1rO6DH+fz5qNK2Ap5UN4ZapYICE3/0KodcLYSPsPbaA==", + "dev": true, + "requires": { + "agent-base": "^7.0.2", + "debug": "4" + } + }, + "human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true + }, + "npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "requires": { + "path-key": "^3.0.0" + } + }, + "resolve": { + "version": "1.22.4", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.4.tgz", + "integrity": "sha512-PXNdCiPqDqeUou+w1C2eTQbNfxKSuMxqTCuvlmmMsk1NWHL5fRrhY6Pl0qEYYc6+QqGClco1Qj8XnjPego4wfg==", + "dev": true, + "requires": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + } + }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true + }, + "tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dev": true, + "requires": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + } + }, + "tmp": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", + "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", + "dev": true, + "requires": { + "rimraf": "^3.0.0" + } + }, + "zip-stream": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-4.1.1.tgz", + "integrity": "sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==", + "dev": true, + "requires": { + "archiver-utils": "^3.0.4", + "compress-commons": "^4.1.2", + "readable-stream": "^3.6.0" + }, + "dependencies": { + "archiver-utils": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-3.0.4.tgz", + "integrity": "sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==", + "dev": true, + "requires": { + "glob": "^7.2.3", + "graceful-fs": "^4.2.0", + "lazystream": "^1.0.0", + "lodash.defaults": "^4.2.0", + "lodash.difference": "^4.5.0", + "lodash.flatten": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.union": "^4.6.0", + "normalize-path": "^3.0.0", + "readable-stream": "^3.6.0" + } + } + } + } + } + }, + "@prisma/prisma-schema-wasm": { + "version": "5.3.1-2.61e140623197a131c2a6189271ffee05a7aa9a59", + "resolved": "https://registry.npmjs.org/@prisma/prisma-schema-wasm/-/prisma-schema-wasm-5.3.1-2.61e140623197a131c2a6189271ffee05a7aa9a59.tgz", + "integrity": "sha512-+zUI7NQDXfcNnU8HgrAj4jRMv8yRfITLzcfv0Urf0adKimM+hkkVG4rX38i9zWMlxekkEBw7NLFx3Gxxy8d3iQ==", + "dev": true + }, "@protobufjs/aspromise": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", @@ -20668,6 +22700,24 @@ "@types/node": "*" } }, + "@types/cross-spawn": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@types/cross-spawn/-/cross-spawn-6.0.2.tgz", + "integrity": "sha512-KuwNhp3eza+Rhu8IFI5HUXRP0LIhqH5cAjubUvGXXthh4YYBuP2ntwEX+Cz8GJoZUHlKo247wPWOfA9LYEq4cw==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/debug": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.8.tgz", + "integrity": "sha512-/vPO1EPOs306Cvhwv7KfVfYvOJqA/S/AXjaHQiJboCZzcNDb+TIJFN9/2C9DZ//ijSKWioNyUxD792QmDJ+HKQ==", + "dev": true, + "requires": { + "@types/ms": "*" + } + }, "@types/docker-modem": { "version": "3.0.6", "resolved": "https://registry.npmjs.org/@types/docker-modem/-/docker-modem-3.0.6.tgz", @@ -20848,6 +22898,12 @@ "@types/node": "*" } }, + "@types/ms": { + "version": "0.7.34", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz", + "integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==", + "dev": true + }, "@types/multer": { "version": "1.4.11", "resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.4.11.tgz", @@ -20997,6 +23053,12 @@ "@types/node": "*" } }, + "@types/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", + "dev": true + }, "@types/semver": { "version": "7.5.8", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz", @@ -21518,6 +23580,16 @@ "debug": "4" } }, + "aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "dev": true, + "requires": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + } + }, "ajv": { "version": "8.12.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", @@ -21754,6 +23826,12 @@ "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", "dev": true }, + "astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true + }, "async": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz", @@ -22114,6 +24192,72 @@ "get-func-name": "^2.0.2" } }, + "checkpoint-client": { + "version": "1.1.27", + "resolved": "https://registry.npmjs.org/checkpoint-client/-/checkpoint-client-1.1.27.tgz", + "integrity": "sha512-xstymfUalJOv6ZvTtmkwP4ORJN36ikT4PvrIoLe3wstbYf87XIXCcZrSmbFQOjyB0v1qbBnCsAscDpfdZlCkFA==", + "dev": true, + "requires": { + "ci-info": "3.8.0", + "env-paths": "2.2.1", + "make-dir": "4.0.0", + "ms": "2.1.3", + "node-fetch": "2.6.12", + "uuid": "9.0.0" + }, + "dependencies": { + "ci-info": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.8.0.tgz", + "integrity": "sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==", + "dev": true + }, + "make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "requires": { + "semver": "^7.5.3" + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node-fetch": { + "version": "2.6.12", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.12.tgz", + "integrity": "sha512-C/fGU2E8ToujUivIO0H+tpQ6HWo4eEmchoPIoXtxCrVghxdKq+QOHqEZW7tuP3KlV3bC8FRMO5nMCC7Zm1VP6g==", + "dev": true, + "requires": { + "whatwg-url": "^5.0.0" + } + }, + "uuid": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", + "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==", + "dev": true + } + } + }, + "chevrotain": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-10.5.0.tgz", + "integrity": "sha512-Pkv5rBY3+CsHOYfV5g/Vs5JY9WTHHDEKOlohI2XeygaZhUeqhAlldZ8Hz9cRmxu709bvS08YzxHdTPHhffc13A==", + "dev": true, + "requires": { + "@chevrotain/cst-dts-gen": "10.5.0", + "@chevrotain/gast": "10.5.0", + "@chevrotain/types": "10.5.0", + "@chevrotain/utils": "10.5.0", + "lodash": "4.17.21", + "regexp-to-ast": "0.5.0" + } + }, "chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -22182,6 +24326,12 @@ } } }, + "clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "dev": true + }, "cli-cursor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", @@ -22218,6 +24368,16 @@ "string-width": "^4.2.0" } }, + "cli-truncate": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz", + "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==", + "dev": true, + "requires": { + "slice-ansi": "^3.0.0", + "string-width": "^4.2.0" + } + }, "cli-width": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", @@ -22319,6 +24479,12 @@ "repeat-string": "^1.6.1" } }, + "commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", + "dev": true + }, "compress-commons": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-6.0.2.tgz", @@ -22548,6 +24714,12 @@ "which": "^2.0.1" } }, + "crypto-random-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", + "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==", + "dev": true + }, "cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -22614,6 +24786,47 @@ "has-property-descriptors": "^1.0.1" } }, + "del": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/del/-/del-6.1.1.tgz", + "integrity": "sha512-ua8BhapfP0JUJKC/zV9yHHDW/rDoDxP4Zhn3AkA6/xT6gY7jYXJiaeyBZznYVujhZZET+UgcbZiQ7sN3WqcImg==", + "dev": true, + "requires": { + "globby": "^11.0.1", + "graceful-fs": "^4.2.4", + "is-glob": "^4.0.1", + "is-path-cwd": "^2.2.0", + "is-path-inside": "^3.0.2", + "p-map": "^4.0.0", + "rimraf": "^3.0.2", + "slash": "^3.0.0" + }, + "dependencies": { + "glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + } + } + }, "delegates": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", @@ -22919,6 +25132,12 @@ "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==" }, + "env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true + }, "error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -23526,6 +25745,17 @@ } } }, + "find-cache-dir": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", + "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", + "dev": true, + "requires": { + "commondir": "^1.0.1", + "make-dir": "^3.0.2", + "pkg-dir": "^4.1.0" + } + }, "find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -23631,6 +25861,12 @@ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==" }, + "fp-ts": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/fp-ts/-/fp-ts-2.16.1.tgz", + "integrity": "sha512-by7U5W8dkIzcvDofUcO42yl9JbnHTEDBrzu3pt5fKT+Z4Oy85I21K80EYJYdjQGC2qum4Vo55Ag57iiIK4FYuA==", + "dev": true + }, "fraction.js": { "version": "4.3.7", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", @@ -23667,6 +25903,35 @@ "universalify": "^2.0.0" } }, + "fs-jetpack": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/fs-jetpack/-/fs-jetpack-5.1.0.tgz", + "integrity": "sha512-Xn4fDhLydXkuzepZVsr02jakLlmoARPy+YWIclo4kh0GyNGUHnTqeH/w/qIsVn50dFxtp8otPL2t/HcPJBbxUA==", + "dev": true, + "requires": { + "minimatch": "^5.1.0" + }, + "dependencies": { + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + } + } + } + }, "fs-minipass": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", @@ -23889,6 +26154,23 @@ "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==" }, + "global-dirs": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-3.0.1.tgz", + "integrity": "sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA==", + "dev": true, + "requires": { + "ini": "2.0.0" + }, + "dependencies": { + "ini": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz", + "integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==", + "dev": true + } + } + }, "globals": { "version": "13.22.0", "resolved": "https://registry.npmjs.org/globals/-/globals-13.22.0.tgz", @@ -23990,6 +26272,24 @@ "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==" }, + "hasha": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/hasha/-/hasha-5.2.2.tgz", + "integrity": "sha512-Hrp5vIK/xr5SkeN2onO32H0MgNZ0f17HRNH39WfL0SYUNOTZ5Lz1TJ8Pajo/87dYGEFlLMm7mIc/k/s6Bvz9HQ==", + "dev": true, + "requires": { + "is-stream": "^2.0.0", + "type-fest": "^0.8.0" + }, + "dependencies": { + "type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true + } + } + }, "hasown": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.1.tgz", @@ -24055,6 +26355,27 @@ "toidentifier": "1.0.1" } }, + "http-proxy-agent": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.0.tgz", + "integrity": "sha512-+ZT+iBxVUQ1asugqnD6oWoRiS25AkjNfG085dKJGtGxkdwLQrMKU5wJr2bOOFAXzKcTuqq+7fZlTMgG3SRfIYQ==", + "dev": true, + "requires": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "dependencies": { + "agent-base": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", + "dev": true, + "requires": { + "debug": "^4.3.4" + } + } + } + }, "https-proxy-agent": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", @@ -24096,6 +26417,35 @@ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==" }, + "ignore-walk": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-5.0.1.tgz", + "integrity": "sha512-yemi4pMf51WKT7khInJqAvsIGzoqYXblnsz0ql8tM+yi1EKYTY1evX4NAbJrLL/Aanr2HyZeluqU+Oi7MGHokw==", + "dev": true, + "requires": { + "minimatch": "^5.0.1" + }, + "dependencies": { + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + } + } + } + }, "import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", @@ -24232,6 +26582,12 @@ "has": "^1.0.3" } }, + "is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true + }, "is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -24260,6 +26616,12 @@ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==" }, + "is-path-cwd": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz", + "integrity": "sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==", + "dev": true + }, "is-path-inside": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", @@ -24275,6 +26637,21 @@ "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==" }, + "is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "dev": true + }, + "is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "requires": { + "is-docker": "^2.0.0" + } + }, "isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", @@ -24479,6 +26856,17 @@ "json-buffer": "3.0.1" } }, + "kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "dev": true + }, + "kysely": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/kysely/-/kysely-0.27.3.tgz", + "integrity": "sha512-lG03Ru+XyOJFsjH3OMY6R/9U38IjDPfnOfDgO3ynhbDr+Dz8fak+X6L62vqu3iybQnj+lG84OttBuU9KY3L9kA==" + }, "lazystream": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", @@ -25086,6 +27474,12 @@ "response-time": "^2.3.2" } }, + "new-github-issue-url": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/new-github-issue-url/-/new-github-issue-url-0.2.1.tgz", + "integrity": "sha512-md4cGoxuT4T4d/HDOXbrUHkTKrp/vp+m3aOA7XXVYwNsUNMK49g3SQicTSeV5GIz/5QVGAeYRAOlyp9OvlgsYA==", + "dev": true + }, "next": { "version": "14.1.4", "resolved": "https://registry.npmjs.org/next/-/next-14.1.4.tgz", @@ -25197,6 +27591,66 @@ "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==" }, + "npm-bundled": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-2.0.1.tgz", + "integrity": "sha512-gZLxXdjEzE/+mOstGDqR6b0EkhJ+kM6fxM6vUuckuctuVPh80Q6pw/rSZj9s4Gex9GxWtIicO1pc8DB9KZWudw==", + "dev": true, + "requires": { + "npm-normalize-package-bin": "^2.0.0" + } + }, + "npm-normalize-package-bin": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-2.0.0.tgz", + "integrity": "sha512-awzfKUO7v0FscrSpRoogyNm0sajikhBWpU0QMrW09AMi9n1PoKU6WaIqUzuJSQnpciZZmJ/jMZ2Egfmb/9LiWQ==", + "dev": true + }, + "npm-packlist": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-5.1.3.tgz", + "integrity": "sha512-263/0NGrn32YFYi4J533qzrQ/krmmrWwhKkzwTuM4f/07ug51odoaNjUexxO4vxlzURHcmYMH1QjvHjsNDKLVg==", + "dev": true, + "requires": { + "glob": "^8.0.1", + "ignore-walk": "^5.0.1", + "npm-bundled": "^2.0.0", + "npm-normalize-package-bin": "^2.0.0" + }, + "dependencies": { + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + } + }, + "minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + } + } + } + }, "npm-run-path": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", @@ -25284,6 +27738,16 @@ "mimic-fn": "^2.1.0" } }, + "open": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-7.4.2.tgz", + "integrity": "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==", + "dev": true, + "requires": { + "is-docker": "^2.0.0", + "is-wsl": "^2.1.1" + } + }, "openid-client": { "version": "5.6.5", "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.6.5.tgz", @@ -25349,6 +27813,23 @@ "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==" }, + "p-filter": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/p-filter/-/p-filter-2.1.0.tgz", + "integrity": "sha512-ZBxxZ5sL2HghephhpGAQdoskxplTwr7ICaehZwLIlfL6acuVgZPm8yBNuRAFBGEqtD/hmUeq9eqLg2ys9Xr/yw==", + "dev": true, + "requires": { + "p-map": "^2.0.0" + }, + "dependencies": { + "p-map": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz", + "integrity": "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==", + "dev": true + } + } + }, "p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -25365,6 +27846,33 @@ "p-limit": "^3.0.2" } }, + "p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "dev": true, + "requires": { + "aggregate-error": "^3.0.0" + } + }, + "p-retry": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", + "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", + "dev": true, + "requires": { + "@types/retry": "0.12.0", + "retry": "^0.13.1" + }, + "dependencies": { + "retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "dev": true + } + } + }, "p-try": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", @@ -25590,6 +28098,54 @@ "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==" }, + "pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "requires": { + "find-up": "^4.0.0" + }, + "dependencies": { + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } + } + } + }, "pkg-types": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.1.0.tgz", @@ -25762,6 +28318,34 @@ } } }, + "prisma": { + "version": "5.13.0", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.13.0.tgz", + "integrity": "sha512-kGtcJaElNRAdAGsCNykFSZ7dBKpL14Cbs+VaQ8cECxQlRPDjBlMHNFYeYt0SKovAVy2Y65JXQwB3A5+zIQwnTg==", + "devOptional": true, + "requires": { + "@prisma/engines": "5.13.0" + } + }, + "prisma-extension-kysely": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/prisma-extension-kysely/-/prisma-extension-kysely-2.1.0.tgz", + "integrity": "sha512-s1hujYjrNzfQc9Z79s93464mkTkkt9ZPqPELDRFe9jEmiIlM/JRXZtqIyyN3FM0GDkN/BDPVtpPtdC52XnjAcQ==", + "requires": {} + }, + "prisma-kysely": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/prisma-kysely/-/prisma-kysely-1.8.0.tgz", + "integrity": "sha512-VpNpolZ8RXRgfU+j4R+fPZmX8EE95w3vJ2tt7+FwuiQc0leNTfLK5QLf3KbbPDes2rfjh3g20AjDxefQIo5GIA==", + "dev": true, + "requires": { + "@mrleebo/prisma-ast": "^0.7.0", + "@prisma/generator-helper": "5.3.1", + "@prisma/internals": "5.3.1", + "typescript": "^5.2.2", + "zod": "^3.22.2" + } + }, "prismjs": { "version": "1.29.0", "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.29.0.tgz", @@ -25777,6 +28361,30 @@ "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" }, + "progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true + }, + "prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "requires": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "dependencies": { + "kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true + } + } + }, "proper-lockfile": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", @@ -26534,6 +29142,12 @@ "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" }, + "regexp-to-ast": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/regexp-to-ast/-/regexp-to-ast-0.5.0.tgz", + "integrity": "sha512-tlbJqcMHnPKI9zSrystikWKwHkBqu2a/Sgw01h3zFjvYrMxEDYHzzoMZnUrbIfpTFEsoRnnviOXNCzFiSc54Qw==", + "dev": true + }, "regexp-tree": { "version": "0.1.27", "resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.27.tgz", @@ -26563,6 +29177,12 @@ "integrity": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==", "dev": true }, + "replace-string": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/replace-string/-/replace-string-3.1.0.tgz", + "integrity": "sha512-yPpxc4ZR2makceA9hy/jHNqc7QVkd4Je/N0WRHm6bs3PtivPuPynxE5ejU/mp5EhnCv8+uZL7vhz8rkluSlx+Q==", + "dev": true + }, "require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -27016,12 +29636,29 @@ "totalist": "^3.0.0" } }, + "sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true + }, "slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", "dev": true }, + "slice-ansi": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz", + "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + } + }, "slice-source": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/slice-source/-/slice-source-0.4.1.tgz", @@ -27345,6 +29982,16 @@ "has-flag": "^4.0.0" } }, + "supports-hyperlinks": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.3.0.tgz", + "integrity": "sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA==", + "dev": true, + "requires": { + "has-flag": "^4.0.0", + "supports-color": "^7.0.0" + } + }, "supports-preserve-symlinks-flag": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", @@ -27485,6 +30132,43 @@ "streamx": "^2.15.0" } }, + "temp-dir": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-2.0.0.tgz", + "integrity": "sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg==", + "dev": true + }, + "tempy": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tempy/-/tempy-1.0.1.tgz", + "integrity": "sha512-biM9brNqxSc04Ee71hzFbryD11nX7VPhQQY32AdDmjFvodsRFz/3ufeoTZ6uYkRFfGo188tENcASNs3vTdsM0w==", + "dev": true, + "requires": { + "del": "^6.0.0", + "is-stream": "^2.0.0", + "temp-dir": "^2.0.0", + "type-fest": "^0.16.0", + "unique-string": "^2.0.0" + }, + "dependencies": { + "type-fest": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.16.0.tgz", + "integrity": "sha512-eaBzG6MxNzEn9kiwvtre90cXaNLkmadMWa1zQMs3XORCXNbsH/OewwbxC5ia9dCxIxnTAsSxXJaa/p5y8DlvJg==", + "dev": true + } + } + }, + "terminal-link": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-2.1.1.tgz", + "integrity": "sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ==", + "dev": true, + "requires": { + "ansi-escapes": "^4.2.1", + "supports-hyperlinks": "^2.0.0" + } + }, "terser": { "version": "5.27.0", "resolved": "https://registry.npmjs.org/terser/-/terser-5.27.0.tgz", @@ -27876,6 +30560,12 @@ "yn": "3.1.1" } }, + "ts-pattern": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ts-pattern/-/ts-pattern-4.3.0.tgz", + "integrity": "sha512-pefrkcd4lmIVR0LA49Imjf9DYLK8vtWhqBPA3Ya1ir8xCW0O2yjL9dsCVvI7pCodLC5q7smNpEtDR2yVulQxOg==", + "dev": true + }, "tsconfig-paths": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", @@ -28058,6 +30748,15 @@ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" }, + "unique-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", + "integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==", + "dev": true, + "requires": { + "crypto-random-string": "^2.0.0" + } + }, "universalify": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", @@ -28496,6 +31195,12 @@ } } } + }, + "zod": { + "version": "3.23.7", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.7.tgz", + "integrity": "sha512-NBeIoqbtOiUMomACV/y+V3Qfs9+Okr18vR5c/5pHClPpufWOrsx8TENboDPe265lFdfewX2yBtNTLPvnmCxwog==", + "dev": true } } } diff --git a/server/package.json b/server/package.json index b56ecc1280..4ae07b57e8 100644 --- a/server/package.json +++ b/server/package.json @@ -32,7 +32,8 @@ "typeorm:schema:reset": "npm run typeorm:schema:drop && npm run typeorm:migrations:run", "sync:open-api": "node ./dist/bin/sync-open-api.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": { "@nestjs/bullmq": "^10.0.1", @@ -51,6 +52,7 @@ "@opentelemetry/exporter-prometheus": "^0.52.0", "@opentelemetry/sdk-node": "^0.52.0", "@react-email/components": "^0.0.19", + "@prisma/client": "^5.11.0", "@socket.io/postgres-adapter": "^0.3.1", "archiver": "^7.0.0", "async-lock": "^1.4.0", @@ -69,6 +71,7 @@ "ioredis": "^5.3.2", "joi": "^17.10.0", "js-yaml": "^4.1.0", + "kysely": "^0.27.3", "lodash": "^4.17.21", "luxon": "^3.4.2", "mnemonist": "^0.39.8", @@ -79,6 +82,7 @@ "openid-client": "^5.4.3", "pg": "^8.11.3", "picomatch": "^4.0.0", + "prisma-extension-kysely": "^2.1.0", "react-email": "^2.1.2", "reflect-metadata": "^0.2.0", "rxjs": "^7.8.1", @@ -122,6 +126,8 @@ "mock-fs": "^5.2.0", "prettier": "^3.0.2", "prettier-plugin-organize-imports": "^3.2.3", + "prisma": "^5.11.0", + "prisma-kysely": "^1.8.0", "rimraf": "^5.0.1", "source-map-support": "^0.5.21", "sql-formatter": "^15.0.0", diff --git a/server/src/interfaces/asset.interface.ts b/server/src/interfaces/asset.interface.ts index 37115a6e3a..303e3ff164 100644 --- a/server/src/interfaces/asset.interface.ts +++ b/server/src/interfaces/asset.interface.ts @@ -1,10 +1,10 @@ +import { Prisma } from '@prisma/client'; import { AssetOrder } from 'src/entities/album.entity'; import { AssetJobStatusEntity } from 'src/entities/asset-job-status.entity'; import { AssetEntity, AssetType } from 'src/entities/asset.entity'; import { ExifEntity } from 'src/entities/exif.entity'; import { AssetSearchOptions, SearchExploreItem } from 'src/interfaces/search.interface'; import { Paginated, PaginationOptions } from 'src/utils/pagination'; -import { FindOptionsOrder, FindOptionsRelations, FindOptionsSelect } from 'typeorm'; export type AssetStats = Record; @@ -66,22 +66,6 @@ export interface TimeBucketItem { count: number; } -export type AssetCreate = Pick< - AssetEntity, - | 'deviceAssetId' - | 'ownerId' - | 'libraryId' - | 'deviceId' - | 'type' - | 'originalPath' - | 'fileCreatedAt' - | 'localDateTime' - | 'fileModifiedAt' - | 'checksum' - | 'originalFileName' -> & - Partial; - export type AssetWithoutRelations = Omit< AssetEntity, | 'livePhotoVideo' @@ -97,10 +81,25 @@ export type AssetWithoutRelations = Omit< | 'tags' >; -type AssetUpdateWithoutRelations = Pick & Partial; -type AssetUpdateWithLivePhotoRelation = Pick & Pick; +export type AssetCreate = Pick< + AssetEntity, + | 'deviceAssetId' + | 'ownerId' + | 'libraryId' + | 'deviceId' + | 'type' + | 'originalPath' + | 'fileCreatedAt' + | 'localDateTime' + | 'fileModifiedAt' + | 'checksum' + | 'originalFileName' +> & + Partial; -export type AssetUpdateOptions = AssetUpdateWithoutRelations | AssetUpdateWithLivePhotoRelation; +type AssetUpdateWithoutRelations = Pick & Partial; + +export type AssetUpdateOptions = AssetUpdateWithoutRelations; export type AssetUpdateAllOptions = Omit, 'id'>; @@ -139,30 +138,28 @@ export interface AssetUpdateDuplicateOptions { duplicateIds: string[]; } +export interface AssetGetByChecksumOptions { + ownerId: string; + checksum: Buffer; + libraryId?: string; +} + export type AssetPathEntity = Pick; export const IAssetRepository = 'IAssetRepository'; export interface IAssetRepository { create(asset: AssetCreate): Promise; - getByIds( - ids: string[], - relations?: FindOptionsRelations, - select?: FindOptionsSelect, - ): Promise; + getByIds(ids: string[], relations?: Prisma.AssetsInclude): Promise; getByIdsWithAllRelations(ids: string[]): Promise; getByDayOfYear(ownerIds: string[], monthDay: MonthDay): Promise; - getByChecksum(options: { ownerId: string; checksum: Buffer; libraryId?: string }): Promise; + getByChecksum(options: AssetGetByChecksumOptions): Promise; getByChecksums(userId: string, checksums: Buffer[]): Promise; getUploadAssetIdByChecksum(ownerId: string, checksum: Buffer): Promise; getByAlbumId(pagination: PaginationOptions, albumId: string): Paginated; getByDeviceIds(ownerId: string, deviceId: string, deviceAssetIds: string[]): Promise; getByUserId(pagination: PaginationOptions, userId: string, options?: AssetSearchOptions): Paginated; - getById( - id: string, - relations?: FindOptionsRelations, - order?: FindOptionsOrder, - ): Promise; + getById(id: string, relations?: Prisma.AssetsInclude): Promise; getWithout(pagination: PaginationOptions, property: WithoutProperty): Paginated; getWith(pagination: PaginationOptions, property: WithProperty, libraryId?: string): Paginated; getRandom(userId: string, count: number): Promise; @@ -176,7 +173,7 @@ export interface IAssetRepository { getLivePhotoCount(motionId: string): Promise; updateAll(ids: string[], options: Partial): Promise; updateDuplicates(options: AssetUpdateDuplicateOptions): Promise; - update(asset: AssetUpdateOptions): Promise; + update(asset: AssetUpdateOptions): Promise; remove(asset: AssetEntity): Promise; softDeleteAll(ids: string[]): Promise; restoreAll(ids: string[]): Promise; @@ -188,7 +185,7 @@ export interface IAssetRepository { upsertJobStatus(...jobStatus: Partial[]): Promise; getAssetIdByCity(userId: string, options: AssetExploreFieldOptions): Promise>; getAssetIdByTag(userId: string, options: AssetExploreFieldOptions): Promise>; - getDuplicates(options: AssetBuilderOptions): Promise; + getDuplicates(userId: string): Promise; getAllForUserFullSync(options: AssetFullSyncOptions): Promise; getChangedDeltaSync(options: AssetDeltaSyncOptions): Promise; } diff --git a/server/src/interfaces/search.interface.ts b/server/src/interfaces/search.interface.ts index c84b56c62e..6c3cf7b0b8 100644 --- a/server/src/interfaces/search.interface.ts +++ b/server/src/interfaces/search.interface.ts @@ -147,13 +147,13 @@ export type SmartSearchOptions = SearchDateOptions & export interface FaceEmbeddingSearch extends SearchEmbeddingOptions { hasPerson?: boolean; numResults: number; - maxDistance?: number; + maxDistance: number; } export interface AssetDuplicateSearch { assetId: string; embedding: number[]; - maxDistance?: number; + maxDistance: number; type: AssetType; userIds: string[]; } diff --git a/server/src/migrations/1713654632360-AddTruncatedDate.ts b/server/src/migrations/1713654632360-AddTruncatedDate.ts new file mode 100644 index 0000000000..f5063a0a81 --- /dev/null +++ b/server/src/migrations/1713654632360-AddTruncatedDate.ts @@ -0,0 +1,14 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddTruncatedDate1713654632360 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + 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 { + await queryRunner.query(`ALTER TABLE assets DROP COLUMN "truncatedDate"`); + } +} diff --git a/server/src/prisma/find-non-deleted.ts b/server/src/prisma/find-non-deleted.ts new file mode 100644 index 0000000000..3a2638493a --- /dev/null +++ b/server/src/prisma/find-non-deleted.ts @@ -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, + }, +}); diff --git a/server/src/prisma/generated/types.ts b/server/src/prisma/generated/types.ts new file mode 100644 index 0000000000..602ed4df8c --- /dev/null +++ b/server/src/prisma/generated/types.ts @@ -0,0 +1,294 @@ +import type { ColumnType } from "kysely"; +export type Generated = T extends ColumnType + ? ColumnType + : ColumnType; +export type Timestamp = ColumnType; + +export type Activity = { + id: Generated; + createdAt: Generated; + updatedAt: Generated; + albumId: string; + userId: string; + assetId: string | null; + comment: string | null; + isLiked: Generated; +}; +export type Albums = { + id: Generated; + ownerId: string; + albumName: Generated; + createdAt: Generated; + albumThumbnailAssetId: string | null; + updatedAt: Generated; + description: Generated; + deletedAt: Timestamp | null; + isActivityEnabled: Generated; + order: Generated; +}; +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; + updatedAt: Generated; + id: Generated; +}; +export type AssetFaces = { + assetId: string; + personId: string | null; + imageWidth: Generated; + imageHeight: Generated; + boundingBoxX1: Generated; + boundingBoxY1: Generated; + boundingBoxX2: Generated; + boundingBoxY2: Generated; + id: Generated; +}; +export type AssetJobStatus = { + assetId: string; + facesRecognizedAt: Timestamp | null; + metadataExtractedAt: Timestamp | null; + duplicatesDetectedAt: Timestamp | null; +}; +export type Assets = { + id: Generated; + deviceAssetId: string; + ownerId: string; + deviceId: string; + type: string; + originalPath: string; + previewPath: string | null; + fileCreatedAt: Timestamp; + fileModifiedAt: Timestamp; + isFavorite: Generated; + duration: string | null; + thumbnailPath: Generated; + encodedVideoPath: Generated; + checksum: Buffer; + isVisible: Generated; + livePhotoVideoId: string | null; + updatedAt: Generated; + createdAt: Generated; + isArchived: Generated; + originalFileName: string; + sidecarPath: string | null; + isReadOnly: Generated; + thumbhash: Buffer | null; + isOffline: Generated; + libraryId: string | null; + isExternal: Generated; + deletedAt: Timestamp | null; + localDateTime: Timestamp; + stackId: string | null; + duplicateId: string | null; + truncatedDate: Generated; +}; +export type AssetStack = { + id: Generated; + primaryAssetId: string; +}; +export type Audit = { + id: Generated; + entityType: string; + entityId: string; + action: string; + ownerId: string; + createdAt: Generated; +}; +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; + 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; + name: string; + ownerId: string; + type: string; + importPaths: string[]; + exclusionPatterns: string[]; + createdAt: Generated; + updatedAt: Generated; + deletedAt: Timestamp | null; + refreshedAt: Timestamp | null; + isVisible: Generated; +}; +export type MoveHistory = { + id: Generated; + entityId: string; + pathType: string; + oldPath: string; + newPath: string; +}; +export type Partners = { + sharedById: string; + sharedWithId: string; + createdAt: Generated; + updatedAt: Generated; + inTimeline: Generated; +}; +export type Person = { + id: Generated; + createdAt: Generated; + updatedAt: Generated; + ownerId: string; + name: Generated; + thumbnailPath: Generated; + isHidden: Generated; + birthDate: Timestamp | null; + faceAssetId: string | null; +}; +export type SharedLinkAsset = { + assetsId: string; + sharedLinksId: string; +}; +export type SharedLinks = { + id: Generated; + description: string | null; + userId: string; + key: Buffer; + type: string; + createdAt: Generated; + expiresAt: Timestamp | null; + allowUpload: Generated; + albumId: string | null; + allowDownload: Generated; + showExif: Generated; + password: string | null; +}; +export type SmartInfo = { + assetId: string; + tags: string[]; + objects: string[]; +}; +export type SmartSearch = { + assetId: string; +}; +export type SocketIoAttachments = { + id: Generated; + created_at: Generated; + payload: Buffer | null; +}; +export type SystemConfig = { + key: string; + value: string | null; +}; +export type SystemMetadata = { + key: string; + value: Generated; +}; +export type TagAsset = { + assetsId: string; + tagsId: string; +}; +export type Tags = { + id: Generated; + type: string; + name: string; + userId: string; + renameTagId: string | null; +}; +export type Users = { + id: Generated; + email: string; + password: Generated; + createdAt: Generated; + profileImagePath: Generated; + isAdmin: Generated; + shouldChangePassword: Generated; + deletedAt: Timestamp | null; + oauthId: Generated; + updatedAt: Generated; + storageLabel: string | null; + memoriesEnabled: Generated; + name: Generated; + avatarColor: string | null; + quotaSizeInBytes: string | null; + quotaUsageInBytes: Generated; + status: Generated; +}; +export type UserToken = { + id: Generated; + token: string; + createdAt: Generated; + updatedAt: Generated; + userId: string; + deviceType: Generated; + deviceOS: Generated; +}; +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; +}; diff --git a/server/src/prisma/kysely.ts b/server/src/prisma/kysely.ts new file mode 100644 index 0000000000..c151158450 --- /dev/null +++ b/server/src/prisma/kysely.ts @@ -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({ + dialect: { + createDriver: () => driver, + createAdapter: () => new PostgresAdapter(), + createIntrospector: (db) => new PostgresIntrospector(db), + createQueryCompiler: () => new PostgresQueryCompiler(), + }, + plugins: [new DeduplicateJoinsPlugin()], + }), +}); diff --git a/server/src/prisma/metrics.ts b/server/src/prisma/metrics.ts new file mode 100644 index 0000000000..84741d61f8 --- /dev/null +++ b/server/src/prisma/metrics.ts @@ -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; + }, + }, + }, +}); diff --git a/server/src/prisma/schema.prisma b/server/src/prisma/schema.prisma new file mode 100644 index 0000000000..fc33f2e102 --- /dev/null +++ b/server/src/prisma/schema.prisma @@ -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") +} diff --git a/server/src/repositories/asset.repository.ts b/server/src/repositories/asset.repository.ts index c11341fdcf..351220c5bd 100644 --- a/server/src/repositories/asset.repository.ts +++ b/server/src/repositories/asset.repository.ts @@ -1,18 +1,18 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; +import { Prisma } from '@prisma/client'; +import { sql } from 'kysely'; import { Chunked, ChunkedArray, DummyValue, GenerateSql } from 'src/decorators'; -import { AlbumEntity, AssetOrder } from 'src/entities/album.entity'; +import { AssetOrder } from 'src/entities/album.entity'; import { AssetJobStatusEntity } from 'src/entities/asset-job-status.entity'; import { AssetEntity, AssetType } from 'src/entities/asset.entity'; import { ExifEntity } from 'src/entities/exif.entity'; -import { PartnerEntity } from 'src/entities/partner.entity'; -import { SmartInfoEntity } from 'src/entities/smart-info.entity'; import { - AssetBuilderOptions, AssetCreate, AssetDeltaSyncOptions, AssetExploreFieldOptions, AssetFullSyncOptions, + AssetGetByChecksumOptions, AssetPathEntity, AssetStats, AssetStatsOptions, @@ -28,142 +28,129 @@ import { WithProperty, WithoutProperty, } from 'src/interfaces/asset.interface'; +import { MapMarker, MapMarkerSearchOptions } from 'src/interfaces/map.interface'; import { AssetSearchOptions, SearchExploreItem } from 'src/interfaces/search.interface'; -import { searchAssetBuilder } from 'src/utils/database'; +import { PrismaRepository } from 'src/repositories/prisma.repository'; +import { searchAssetBuilder, withExif } from 'src/utils/database'; import { Instrumentation } from 'src/utils/instrumentation'; -import { Paginated, PaginationMode, PaginationOptions, paginate, paginatedBuilder } from 'src/utils/pagination'; -import { - Brackets, - FindOptionsOrder, - FindOptionsRelations, - FindOptionsSelect, - FindOptionsWhere, - In, - IsNull, - MoreThan, - Not, - Repository, -} from 'typeorm'; - -const truncateMap: Record = { - [TimeBucketSize.DAY]: 'day', - [TimeBucketSize.MONTH]: 'month', -}; - -const dateTrunc = (options: TimeBucketOptions) => - `(date_trunc('${ - truncateMap[options.size] - }', (asset."localDateTime" at time zone 'UTC')) at time zone 'UTC')::timestamptz`; +import { Paginated, PaginationMode, PaginationOptions, paginatedBuilder, paginationHelper } from 'src/utils/pagination'; +import { Repository } from 'typeorm'; @Instrumentation() @Injectable() export class AssetRepository implements IAssetRepository { constructor( @InjectRepository(AssetEntity) private repository: Repository, - @InjectRepository(ExifEntity) private exifRepository: Repository, - @InjectRepository(AssetJobStatusEntity) private jobStatusRepository: Repository, - @InjectRepository(SmartInfoEntity) private smartInfoRepository: Repository, - @InjectRepository(PartnerEntity) private partnerRepository: Repository, - @InjectRepository(AlbumEntity) private albumRepository: Repository, + private prismaRepository: PrismaRepository, ) {} - async upsertExif(exif: Partial): Promise { - await this.exifRepository.upsert(exif, { conflictPaths: ['assetId'] }); + async upsertExif(exif: Partial & { assetId: string }): Promise { + await this.prismaRepository.exif.upsert({ update: exif, create: exif, where: { assetId: exif.assetId } }); } - async upsertJobStatus(...jobStatus: Partial[]): Promise { - await this.jobStatusRepository.upsert(jobStatus, { conflictPaths: ['assetId'] }); - } - - create(asset: AssetCreate): Promise { - return this.repository.save(asset); - } - - @GenerateSql({ params: [[DummyValue.UUID], { day: 1, month: 1 }] }) - getByDayOfYear(ownerIds: string[], { day, month }: MonthDay): Promise { - return this.repository - .createQueryBuilder('entity') - .where( - `entity.ownerId IN (:...ownerIds) - AND entity.isVisible = true - AND entity.isArchived = false - AND entity.previewPath IS NOT NULL - AND EXTRACT(DAY FROM entity.localDateTime AT TIME ZONE 'UTC') = :day - AND EXTRACT(MONTH FROM entity.localDateTime AT TIME ZONE 'UTC') = :month`, - { - ownerIds, - day, - month, - }, - ) - .leftJoinAndSelect('entity.exifInfo', 'exifInfo') - .orderBy('entity.localDateTime', 'ASC') - .getMany(); - } - - @GenerateSql({ params: [[DummyValue.UUID]] }) - @ChunkedArray() - getByIds( - ids: string[], - relations?: FindOptionsRelations, - select?: FindOptionsSelect, - ): Promise { - return this.repository.find({ - where: { id: In(ids) }, - relations, - select, - withDeleted: true, + async upsertJobStatus(...jobStatus: Partial[] & { assetId: string }): Promise { + await this.prismaRepository.assetJobStatus.upsert({ + update: jobStatus, + create: jobStatus, + where: { assetId: jobStatus.assetId }, }); } + async create(asset: AssetCreate): Promise { + const { ownerId, libraryId, livePhotoVideoId, stackId, ...assetData } = asset; + const res = await this.prismaRepository.assets.create({ + data: { + ...assetData, + livePhotoVideo: livePhotoVideoId ? { connect: { id: livePhotoVideoId } } : undefined, + stack: stackId ? { connect: { id: stackId } } : undefined, + library: libraryId ? { connect: { id: libraryId } } : undefined, + owner: { connect: { id: ownerId } }, + }, + }); + return res as any as AssetEntity; + } + + @GenerateSql({ params: [DummyValue.UUID, { day: 1, month: 1 }] }) + async getByDayOfYear(ownerIds: string[], { day, month }: MonthDay): Promise { + const res = await this.prismaRepository.$kysely + .selectFrom('assets') + .where('ownerId', '=', sql`ANY(ARRAY[${ownerIds}]::uuid[])`) + .where('isVisible', '=', true) + .where('isArchived', '=', false) + .where('previewPath', 'is not', null) + .where(sql`EXTRACT(DAY FROM "localDateTime" AT TIME ZONE 'UTC')`, '=', day) + .where(sql`EXTRACT(MONTH FROM "localDateTime" AT TIME ZONE 'UTC')`, '=', month) + .select((qb) => withExif(qb)) + .orderBy('localDateTime', 'desc') + .execute(); + + return res as any as AssetEntity[]; + } + @GenerateSql({ params: [[DummyValue.UUID]] }) @ChunkedArray() - getByIdsWithAllRelations(ids: string[]): Promise { - return this.repository.find({ - where: { id: In(ids) }, - relations: { + async getByIds(ids: string[], relations?: Prisma.AssetsInclude): Promise { + const res = await this.prismaRepository.assets.findMany({ + where: { id: { in: ids } }, + include: { + ...relations, + library: relations?.library ? { include: { assets: true, owner: true } } : undefined, + }, + }); + return res as any as AssetEntity[]; // typeorm type assumes arbitrary level of recursion + } + + @GenerateSql({ params: [[DummyValue.UUID]] }) + @ChunkedArray() + async getByIdsWithAllRelations(ids: string[]): Promise { + const res = await this.prismaRepository.assets.findMany({ + where: { id: { in: ids } }, + include: { exifInfo: true, smartInfo: true, tags: true, faces: { - person: true, - }, - stack: { - assets: true, + include: { + person: true, + }, }, + stack: { include: { assets: true } }, }, - withDeleted: true, }); + + return res as any as AssetEntity[]; } @GenerateSql({ params: [DummyValue.UUID] }) async deleteAll(ownerId: string): Promise { - await this.repository.delete({ ownerId }); + await this.prismaRepository.assets.deleteMany({ where: { ownerId } }); } - getByAlbumId(pagination: PaginationOptions, albumId: string): Paginated { - return paginate(this.repository, pagination, { + async getByAlbumId(pagination: PaginationOptions, albumId: string): Paginated { + const items = await this.prismaRepository.assets.findMany({ where: { albums: { - id: albumId, + some: { + id: albumId, + }, }, }, - relations: { - albums: true, - exifInfo: true, + orderBy: { + fileCreatedAt: 'desc', }, }); + + return paginationHelper(items as any as AssetEntity[], pagination.take); } async getByDeviceIds(ownerId: string, deviceId: string, deviceAssetIds: string[]): Promise { - const assets = await this.repository.find({ + const assets = await this.prismaRepository.assets.findMany({ select: { deviceAssetId: true }, where: { - deviceAssetId: In(deviceAssetIds), + deviceAssetId: { in: deviceAssetIds }, deviceId, ownerId, }, - withDeleted: true, }); return assets.map((asset) => asset.deviceAssetId); @@ -177,49 +164,32 @@ export class AssetRepository implements IAssetRepository { return this.getAll(pagination, { ...options, userIds: [userId] }); } - @GenerateSql({ params: [{ take: 1, skip: 0 }, DummyValue.UUID] }) - getExternalLibraryAssetPaths(pagination: PaginationOptions, libraryId: string): Paginated { - return paginate(this.repository, pagination, { + @GenerateSql({ params: [[DummyValue.UUID]] }) + async getExternalLibraryAssetPaths(pagination: PaginationOptions, libraryId: string): Paginated { + const items = await this.prismaRepository.assets.findMany({ + where: { libraryId, isExternal: true }, select: { id: true, originalPath: true, isOffline: true }, - where: { library: { id: libraryId }, isExternal: true }, + orderBy: { fileCreatedAt: 'desc' }, + skip: pagination.skip, + take: pagination.take + 1, }); + + return paginationHelper(items as any as AssetPathEntity[], pagination.take); } @GenerateSql({ params: [DummyValue.UUID, DummyValue.STRING] }) - getByLibraryIdAndOriginalPath(libraryId: string, originalPath: string): Promise { - return this.repository.findOne({ - where: { library: { id: libraryId }, originalPath }, - withDeleted: true, - }); + async getByLibraryIdAndOriginalPath(libraryId: string, originalPath: string): Promise { + const res = await this.prismaRepository.assets.findFirst({ where: { libraryId, originalPath } }); + return res as AssetEntity | null; } - @GenerateSql({ params: [DummyValue.UUID, [DummyValue.STRING]] }) - @ChunkedArray({ paramIndex: 1 }) - async getPathsNotInLibrary(libraryId: string, originalPaths: string[]): Promise { - const result = await this.repository.query( - ` - WITH paths AS (SELECT unnest($2::text[]) AS path) - SELECT path FROM paths - WHERE NOT EXISTS (SELECT 1 FROM assets WHERE "libraryId" = $1 AND "originalPath" = path); - `, - [libraryId, originalPaths], - ); - return result.map((row: { path: string }) => row.path); - } - - @GenerateSql({ params: [DummyValue.UUID, [DummyValue.STRING]] }) - @ChunkedArray({ paramIndex: 1 }) - async updateOfflineLibraryAssets(libraryId: string, originalPaths: string[]): Promise { - await this.repository.update( - { library: { id: libraryId }, originalPath: Not(In(originalPaths)), isOffline: false }, - { isOffline: true }, - ); - } - - getAll(pagination: PaginationOptions, options: AssetSearchOptions = {}): Paginated { + getAll( + pagination: PaginationOptions, + { orderDirection, ...options }: AssetSearchOptions = {}, + ): Paginated { let builder = this.repository.createQueryBuilder('asset'); builder = searchAssetBuilder(builder, options); - builder.orderBy('asset.createdAt', options.orderDirection ?? 'ASC'); + builder.orderBy('asset.createdAt', orderDirection ?? 'ASC'); return paginatedBuilder(builder, { mode: PaginationMode.SKIP_TAKE, skip: pagination.skip, @@ -236,14 +206,15 @@ export class AssetRepository implements IAssetRepository { */ @GenerateSql({ params: [DummyValue.UUID, DummyValue.STRING] }) async getAllByDeviceId(ownerId: string, deviceId: string): Promise { - const items = await this.repository.find({ - select: { deviceAssetId: true }, + const items = await this.prismaRepository.assets.findMany({ where: { ownerId, deviceId, isVisible: true, }, - withDeleted: true, + select: { + deviceAssetId: true, + }, }); return items.map((asset) => asset.deviceAssetId); @@ -260,24 +231,15 @@ export class AssetRepository implements IAssetRepository { } @GenerateSql({ params: [DummyValue.UUID] }) - getById( - id: string, - relations: FindOptionsRelations, - order?: FindOptionsOrder, - ): Promise { - return this.repository.findOne({ - where: { id }, - relations, - // We are specifically asking for this asset. Return it even if it is soft deleted - withDeleted: true, - order, - }); + async getById(id: string, relations: Prisma.AssetsInclude): Promise { + const items = await this.prismaRepository.assets.findFirst({ where: { id }, include: relations }); + return items as any as AssetEntity | null; } @GenerateSql({ params: [[DummyValue.UUID], { deviceId: DummyValue.STRING }] }) @Chunked() async updateAll(ids: string[], options: AssetUpdateAllOptions): Promise { - await this.repository.update({ id: In(ids) }, options); + await this.prismaRepository.assets.updateMany({ where: { id: { in: ids } }, data: options }); } @GenerateSql({ @@ -289,95 +251,102 @@ export class AssetRepository implements IAssetRepository { .update() .set({ duplicateId: options.targetDuplicateId }) .where({ - duplicateId: In(options.duplicateIds), + duplicateId: { in: options.duplicateIds }, }) - .orWhere({ id: In(options.assetIds) }) + .orWhere({ id: { in: options.assetIds } }) .execute(); } @Chunked() async softDeleteAll(ids: string[]): Promise { - await this.repository.softDelete({ id: In(ids) }); + await this.prismaRepository.assets.updateMany({ where: { id: { in: ids } }, data: { deletedAt: new Date() } }); } @Chunked() async restoreAll(ids: string[]): Promise { - await this.repository.restore({ id: In(ids) }); + await this.prismaRepository.assets.updateMany({ where: { id: { in: ids } }, data: { deletedAt: null } }); } - async update(asset: AssetUpdateOptions): Promise { - await this.repository.update(asset.id, asset); + async update(asset: AssetUpdateOptions): Promise { + const { ownerId, libraryId, livePhotoVideoId, stackId, ...assetData } = asset; + + const res = await this.prismaRepository.assets.update({ + data: { + ...assetData, + livePhotoVideo: livePhotoVideoId ? { connect: { id: livePhotoVideoId } } : undefined, + stack: stackId ? { connect: { id: stackId } } : undefined, + library: libraryId ? { connect: { id: libraryId } } : undefined, + owner: { connect: { id: ownerId } }, + }, + where: { id: asset.id }, + include: { + exifInfo: true, + smartInfo: true, + tags: true, + faces: { + include: { + person: true, + }, + }, + }, + }); + + return res as any as AssetEntity; // typeorm type assumes all relations are included } async remove(asset: AssetEntity): Promise { - await this.repository.remove(asset); + await this.prismaRepository.assets.delete({ where: { id: asset.id } }); } @GenerateSql({ params: [{ ownerId: DummyValue.UUID, libraryId: DummyValue.UUID, checksum: DummyValue.BUFFER }] }) - getByChecksum({ - ownerId, - libraryId, - checksum, - }: { - ownerId: string; - checksum: Buffer; - libraryId?: string; - }): Promise { - return this.repository.findOne({ + getByChecksum({ ownerId, libraryId, checksum }: AssetGetByChecksumOptions): Promise { + return this.prismaRepository.assets.findFirst({ where: { ownerId, - libraryId: libraryId || IsNull(), + libraryId: libraryId ?? null, checksum, }, - }); + }) as Promise; } @GenerateSql({ params: [DummyValue.UUID, DummyValue.BUFFER] }) - getByChecksums(ownerId: string, checksums: Buffer[]): Promise { - return this.repository.find({ - select: { - id: true, - checksum: true, - }, - where: { - ownerId, - checksum: In(checksums), - }, - withDeleted: true, - }); + getByChecksums(userId: string, checksums: Buffer[]): Promise { + return this.prismaRepository.assets.findMany({ + where: { ownerId: userId, checksum: { in: checksums } }, + }) as any as Promise; } @GenerateSql({ params: [DummyValue.UUID, DummyValue.BUFFER] }) async getUploadAssetIdByChecksum(ownerId: string, checksum: Buffer): Promise { - const asset = await this.repository.findOne({ + const asset = await this.prismaRepository.assets.findFirst({ select: { id: true }, where: { ownerId, checksum, - library: IsNull(), + libraryId: null, }, - withDeleted: true, }); return asset?.id; } - findLivePhotoMatch(options: LivePhotoSearchOptions): Promise { + async findLivePhotoMatch(options: LivePhotoSearchOptions): Promise { const { ownerId, otherAssetId, livePhotoCID, type } = options; - return this.repository.findOne({ + const item = await this.prismaRepository.assets.findFirst({ where: { - id: Not(otherAssetId), + id: { not: otherAssetId }, ownerId, type, exifInfo: { livePhotoCID, }, }, - relations: { + include: { exifInfo: true, }, }); + return item as AssetEntity | null; } @GenerateSql( @@ -388,79 +357,64 @@ export class AssetRepository implements IAssetRepository { params: [DummyValue.PAGINATION, property], })), ) - getWithout(pagination: PaginationOptions, property: WithoutProperty): Paginated { - let relations: FindOptionsRelations = {}; - let where: FindOptionsWhere | FindOptionsWhere[] = {}; + async getWithout(pagination: PaginationOptions, property: WithoutProperty): Paginated { + let relations: Prisma.AssetsInclude = {}; + let where: Prisma.AssetsWhereInput = {}; switch (property) { case WithoutProperty.THUMBNAIL: { - where = [ - { previewPath: IsNull(), isVisible: true }, - { previewPath: '', isVisible: true }, - { thumbnailPath: IsNull(), isVisible: true }, - { thumbnailPath: '', isVisible: true }, - { thumbhash: IsNull(), isVisible: true }, - ]; + where = { + OR: [ + { previewPath: null, isVisible: true }, + { previewPath: '', isVisible: true }, + { thumbnailPath: null, isVisible: true }, + { thumbnailPath: '', isVisible: true }, + { thumbhash: null, isVisible: true }, + ], + }; break; } case WithoutProperty.ENCODED_VIDEO: { - where = [ - { type: AssetType.VIDEO, encodedVideoPath: IsNull() }, - { type: AssetType.VIDEO, encodedVideoPath: '' }, - ]; + where = { + OR: [ + { type: AssetType.VIDEO, encodedVideoPath: null }, + { type: AssetType.VIDEO, encodedVideoPath: '' }, + ], + }; break; } case WithoutProperty.EXIF: { relations = { exifInfo: true, - jobStatus: true, + assetJobStatus: true, }; where = { isVisible: true, - jobStatus: { - metadataExtractedAt: IsNull(), + assetJobStatus: { + metadataExtractedAt: null, }, }; break; } case WithoutProperty.SMART_SEARCH: { - relations = { - smartSearch: true, - }; where = { isVisible: true, - previewPath: Not(IsNull()), - smartSearch: { - embedding: IsNull(), - }, + previewPath: { not: null }, + smartSearch: null, }; break; } case WithoutProperty.DUPLICATE: { where = { - previewPath: Not(IsNull()), + previewPath: { not: null }, isVisible: true, - smartSearch: true, - jobStatus: { - duplicatesDetectedAt: IsNull(), - }, - }; - break; - } - - case WithoutProperty.OBJECT_TAGS: { - relations = { - smartInfo: true, - }; - where = { - previewPath: Not(IsNull()), - isVisible: true, - smartInfo: { - tags: IsNull(), + smartSearch: { isNot: null }, + assetJobStatus: { + duplicatesDetectedAt: null, }, }; break; @@ -469,17 +423,18 @@ export class AssetRepository implements IAssetRepository { case WithoutProperty.FACES: { relations = { faces: true, - jobStatus: true, + assetJobStatus: true, }; where = { - previewPath: Not(IsNull()), + previewPath: { not: null }, isVisible: true, faces: { - assetId: IsNull(), - personId: IsNull(), + some: { + person: null, + }, }, - jobStatus: { - facesRecognizedAt: IsNull(), + assetJobStatus: { + facesRecognizedAt: null, }, }; break; @@ -490,21 +445,24 @@ export class AssetRepository implements IAssetRepository { faces: true, }; where = { - previewPath: Not(IsNull()), + previewPath: { not: null }, isVisible: true, faces: { - assetId: Not(IsNull()), - personId: IsNull(), + some: { + person: null, + }, }, }; break; } case WithoutProperty.SIDECAR: { - where = [ - { sidecarPath: IsNull(), isVisible: true }, - { sidecarPath: '', isVisible: true }, - ]; + where = { + OR: [ + { sidecarPath: null, isVisible: true }, + { sidecarPath: '', isVisible: true }, + ], + }; break; } @@ -513,29 +471,33 @@ export class AssetRepository implements IAssetRepository { } } - return paginate(this.repository, pagination, { - relations, + const items = await this.prismaRepository.assets.findMany({ where, - order: { + orderBy: { // Ensures correct order when paginating - createdAt: 'ASC', + createdAt: 'asc', }, + skip: pagination.skip, + take: pagination.take + 1, + include: relations, }); + + return paginationHelper(items as any as AssetEntity[], pagination.take); } - getWith(pagination: PaginationOptions, property: WithProperty, libraryId?: string): Paginated { - let where: FindOptionsWhere | FindOptionsWhere[] = {}; + async getWith(pagination: PaginationOptions, property: WithProperty, libraryId?: string): Paginated { + let where: Prisma.AssetsWhereInput = {}; switch (property) { case WithProperty.SIDECAR: { - where = [{ sidecarPath: Not(IsNull()), isVisible: true }]; + where = { sidecarPath: { not: null }, isVisible: true }; break; } case WithProperty.IS_OFFLINE: { if (!libraryId) { throw new Error('Library id is required when finding offline assets'); } - where = [{ isOffline: true, libraryId: libraryId }]; + where = { isOffline: true, libraryId: libraryId }; break; } @@ -544,52 +506,111 @@ export class AssetRepository implements IAssetRepository { } } - return paginate(this.repository, pagination, { + const items = await this.prismaRepository.assets.findMany({ where, - order: { + orderBy: { // Ensures correct order when paginating - createdAt: 'ASC', + createdAt: 'asc', + }, + skip: pagination.skip, + take: pagination.take + 1, + }); + + return paginationHelper(items as any as AssetEntity[], pagination.take); + } + + async getFirstAssetForAlbumId(albumId: string): Promise { + const items = await this.prismaRepository.assets.findFirst({ + where: { + albums: { + some: { + id: albumId, + }, + }, + }, + orderBy: { + fileCreatedAt: 'desc', }, }); + + return items as AssetEntity | null; } - getFirstAssetForAlbumId(albumId: string): Promise { - return this.repository.findOne({ - where: { albums: { id: albumId } }, - order: { fileCreatedAt: 'DESC' }, + async getLastUpdatedAssetForAlbumId(albumId: string): Promise { + const items = await this.prismaRepository.assets.findFirst({ + where: { + albums: { + some: { + id: albumId, + }, + }, + }, + orderBy: { + updatedAt: 'desc', + }, }); + + return items as AssetEntity | null; } - getLastUpdatedAssetForAlbumId(albumId: string): Promise { - return this.repository.findOne({ - where: { albums: { id: albumId } }, - order: { updatedAt: 'DESC' }, + async getMapMarkers(ownerIds: string[], options: MapMarkerSearchOptions = {}): Promise { + const { isArchived, isFavorite, fileCreatedAfter, fileCreatedBefore } = options; + + const assets = await this.prismaRepository.assets.findMany({ + select: { + id: true, + exifInfo: { + select: { + city: true, + state: true, + country: true, + latitude: true, + longitude: true, + }, + }, + }, + where: { + ownerId: { + in: ownerIds, + }, + isVisible: true, + isArchived, + exifInfo: { + latitude: { not: null }, + longitude: { not: null }, + }, + isFavorite, + fileCreatedAt: { gte: fileCreatedAfter, lte: fileCreatedBefore }, + }, + orderBy: { + fileCreatedAt: 'desc', + }, }); + + return assets.map((asset) => ({ + id: asset.id, + lat: asset.exifInfo!.latitude!, + lon: asset.exifInfo!.longitude!, + city: asset.exifInfo!.city, + state: asset.exifInfo!.state, + country: asset.exifInfo!.country, + })); } - async getStatistics(ownerId: string, options: AssetStatsOptions): Promise { - const builder = this.repository - .createQueryBuilder('asset') - .select(`COUNT(asset.id)`, 'count') - .addSelect(`asset.type`, 'type') - .where('"ownerId" = :ownerId', { ownerId }) - .andWhere('asset.isVisible = true') - .groupBy('asset.type'); - - const { isArchived, isFavorite, isTrashed } = options; - if (isArchived !== undefined) { - builder.andWhere(`asset.isArchived = :isArchived`, { isArchived }); - } - - if (isFavorite !== undefined) { - builder.andWhere(`asset.isFavorite = :isFavorite`, { isFavorite }); - } - - if (isTrashed !== undefined) { - builder.withDeleted().andWhere(`asset.deletedAt is not null`); - } - - const items = await builder.getRawMany(); + async getStatistics(ownerId: string, { isArchived, isFavorite, isTrashed }: AssetStatsOptions): Promise { + const items = await this.prismaRepository.assets.groupBy({ + by: 'type', + where: { + ownerId, + isVisible: true, + isArchived, + isFavorite, + deletedAt: isTrashed ? { not: null } : null, + }, + _count: { + id: true, + }, + }); const result: AssetStats = { [AssetType.AUDIO]: 0, @@ -599,52 +620,89 @@ export class AssetRepository implements IAssetRepository { }; for (const item of items) { - result[item.type as AssetType] = Number(item.count) || 0; + result[item.type as AssetType] = item._count.id; } return result; } - @GenerateSql({ params: [DummyValue.UUID, DummyValue.NUMBER] }) - getRandom(ownerId: string, count: number): Promise { - const builder = this.getBuilder({ - userIds: [ownerId], - exifInfo: true, - }); + async getRandom(ownerId: string, take: number): Promise { + const where = { + ownerId, + isVisible: true, + }; - return builder.orderBy('RANDOM()').limit(count).getMany(); + const count = await this.prismaRepository.assets.count({ where }); + const skip = Math.floor(Math.random() * Math.max(count - take, 0)); + const items = await this.prismaRepository.assets.findMany({ where, take, skip }); + + return items as any as AssetEntity[]; } @GenerateSql({ params: [{ size: TimeBucketSize.MONTH }] }) - getTimeBuckets(options: TimeBucketOptions): Promise { - const truncated = dateTrunc(options); - return this.getBuilder(options) - .select(`COUNT(asset.id)::int`, 'count') - .addSelect(truncated, 'timeBucket') - .groupBy(truncated) - .orderBy(truncated, options.order === AssetOrder.ASC ? 'ASC' : 'DESC') - .getRawMany(); + async getTimeBuckets(options: TimeBucketOptions): Promise { + const items = await this.prismaRepository.assets.groupBy({ + by: 'truncatedDate', + where: { + ownerId: options.userIds ? { in: options.userIds } : undefined, + isVisible: true, + isArchived: options.isArchived, + isFavorite: options.isFavorite, + deletedAt: options.isTrashed ? { not: null } : null, + albums: options.albumId ? { some: { id: options.albumId } } : undefined, + faces: options.personId ? { some: { personId: options.personId } } : undefined, + type: options.assetType, + }, + _count: { + id: true, + }, + orderBy: { + truncatedDate: 'desc', + }, + }); + + return items.map((item) => ({ + timeBucket: item.truncatedDate.toISOString(), + count: item._count.id, + })); } @GenerateSql({ params: [DummyValue.TIME_BUCKET, { size: TimeBucketSize.MONTH }] }) - getTimeBucket(timeBucket: string, options: TimeBucketOptions): Promise { - const truncated = dateTrunc(options); - return ( - this.getBuilder(options) - .andWhere(`${truncated} = :timeBucket`, { timeBucket: timeBucket.replace(/^[+-]/, '') }) - // First sort by the day in localtime (put it in the right bucket) - .orderBy(truncated, 'DESC') - // and then sort by the actual time - .addOrderBy('asset.fileCreatedAt', options.order === AssetOrder.ASC ? 'ASC' : 'DESC') - .getMany() - ); + async getTimeBucket(timeBucket: string, options: TimeBucketOptions): Promise { + const items = await this.prismaRepository.assets.findMany({ + where: { + ownerId: options.userIds ? { in: options.userIds } : undefined, + isVisible: true, + isArchived: options.isArchived, + isFavorite: options.isFavorite, + deletedAt: options.isTrashed ? { not: null } : null, + truncatedDate: timeBucket.replace(/^[+-]/, ''), + albums: options.albumId ? { some: { id: options.albumId } } : undefined, + faces: options.personId ? { some: { personId: options.personId } } : undefined, + type: options.assetType, + }, + orderBy: { fileCreatedAt: options.order === AssetOrder.ASC ? 'asc' : 'desc' }, + include: { + owner: true, + exifInfo: options.exifInfo, + stack: options.withStacked ? { include: { assets: true } } : undefined, + }, + relationLoadStrategy: 'query', // this seems faster than 'join' in this case + }); + + return items as any as AssetEntity[]; } @GenerateSql({ params: [{ userIds: [DummyValue.UUID, DummyValue.UUID] }] }) - getDuplicates(options: AssetBuilderOptions): Promise { - return this.getBuilder({ ...options, isDuplicate: true }) - .orderBy('asset.duplicateId') - .getMany(); + getDuplicates(userId: string): Promise { + return this.prismaRepository.assets.groupBy({ + by: 'duplicateId', + where: { + duplicateId: { not: null }, + ownerId: userId, + }, + orderBy: { duplicateId: 'desc' }, + }) as Promise; } @GenerateSql({ params: [DummyValue.UUID, { minAssetsPerField: 5, maxFields: 12 }] }) @@ -652,28 +710,44 @@ export class AssetRepository implements IAssetRepository { ownerId: string, { minAssetsPerField, maxFields }: AssetExploreFieldOptions, ): Promise> { - const cte = this.exifRepository - .createQueryBuilder('e') - .select('city') - .groupBy('city') - .having('count(city) >= :minAssetsPerField', { minAssetsPerField }); + const res = await this.prismaRepository.exif.groupBy({ + by: 'city', + where: { + assets: { ownerId, isVisible: true, isArchived: false, type: AssetType.IMAGE }, + city: { not: null }, + }, + having: { + assetId: { + _count: { + gte: minAssetsPerField, + }, + }, + }, + take: maxFields, + orderBy: { + city: 'desc', + }, + }); - const items = await this.getBuilder({ - userIds: [ownerId], - exifInfo: false, - assetType: AssetType.IMAGE, - isArchived: false, - }) - .select('c.city', 'value') - .addSelect('asset.id', 'data') - .distinctOn(['c.city']) - .innerJoin('exif', 'e', 'asset.id = e."assetId"') - .addCommonTableExpression(cte, 'cities') - .innerJoin('cities', 'c', 'c.city = e.city') - .limit(maxFields) - .getRawMany(); + const cities = res.map((item) => item.city!); - return { fieldName: 'exifInfo.city', items }; + const items = await this.prismaRepository.exif.findMany({ + where: { + city: { + in: cities, + }, + }, + select: { + city: true, + assetId: true, + }, + distinct: ['city'], + }); + + return { + fieldName: 'exifInfo.city', + items: items.map((item) => ({ value: item.city!, data: item.assetId })), + }; } @GenerateSql({ params: [DummyValue.UUID, { minAssetsPerField: 5, maxFields: 12 }] }) @@ -681,135 +755,92 @@ export class AssetRepository implements IAssetRepository { ownerId: string, { minAssetsPerField, maxFields }: AssetExploreFieldOptions, ): Promise> { - const cte = this.smartInfoRepository - .createQueryBuilder('si') - .select('unnest(tags)', 'tag') - .groupBy('tag') - .having('count(*) >= :minAssetsPerField', { minAssetsPerField }); + const res = await this.prismaRepository.smartInfo.groupBy({ + by: 'tags', + where: { + assets: { ownerId, isVisible: true, isArchived: false, type: AssetType.IMAGE }, + }, + having: { + assetId: { + _count: { + gte: minAssetsPerField, + }, + }, + }, + take: maxFields, + orderBy: { + tags: 'desc', + }, + }); - const items = await this.getBuilder({ - userIds: [ownerId], - exifInfo: false, - assetType: AssetType.IMAGE, - isArchived: false, - }) - .select('unnest(si.tags)', 'value') - .addSelect('asset.id', 'data') - .distinctOn(['unnest(si.tags)']) - .innerJoin('smart_info', 'si', 'asset.id = si."assetId"') - .addCommonTableExpression(cte, 'random_tags') - .innerJoin('random_tags', 't', 'si.tags @> ARRAY[t.tag]') - .limit(maxFields) - .getRawMany(); + const tags = res.flatMap((item) => item.tags!); - return { fieldName: 'smartInfo.tags', items }; - } + const items = await this.prismaRepository.smartInfo.findMany({ + where: { + tags: { + hasSome: tags, + }, + }, + select: { + tags: true, + assetId: true, + }, + }); - private getBuilder(options: AssetBuilderOptions) { - const builder = this.repository.createQueryBuilder('asset').where('asset.isVisible = true'); - if (options.assetType !== undefined) { - builder.andWhere('asset.type = :assetType', { assetType: options.assetType }); - } - - let stackJoined = false; - - if (options.exifInfo !== false) { - stackJoined = true; - builder - .leftJoinAndSelect('asset.exifInfo', 'exifInfo') - .leftJoinAndSelect('asset.stack', 'stack') - .leftJoinAndSelect('stack.assets', 'stackedAssets'); - } - - if (options.albumId) { - builder.leftJoin('asset.albums', 'album').andWhere('album.id = :albumId', { albumId: options.albumId }); - } - - if (options.userIds) { - builder.andWhere('asset.ownerId IN (:...userIds )', { userIds: options.userIds }); - } - - if (options.isArchived !== undefined) { - builder.andWhere('asset.isArchived = :isArchived', { isArchived: options.isArchived }); - } - - if (options.isFavorite !== undefined) { - builder.andWhere('asset.isFavorite = :isFavorite', { isFavorite: options.isFavorite }); - } - - if (options.isTrashed !== undefined) { - builder.andWhere(`asset.deletedAt ${options.isTrashed ? 'IS NOT NULL' : 'IS NULL'}`).withDeleted(); - } - - if (options.isDuplicate !== undefined) { - builder.andWhere(`asset.duplicateId ${options.isDuplicate ? 'IS NOT NULL' : 'IS NULL'}`); - } - - if (options.personId !== undefined) { - builder - .innerJoin('asset.faces', 'faces') - .innerJoin('faces.person', 'person') - .andWhere('person.id = :personId', { personId: options.personId }); - } - - if (options.withStacked) { - if (!stackJoined) { - builder.leftJoinAndSelect('asset.stack', 'stack').leftJoinAndSelect('stack.assets', 'stackedAssets'); - } - builder.andWhere( - new Brackets((qb) => qb.where('stack.primaryAssetId = asset.id').orWhere('asset.stackId IS NULL')), - ); - } - - return builder; + return { + fieldName: 'smartInfo.tags', + items: items.map((item) => ({ value: item.tags![0], data: item.assetId })), + }; } @GenerateSql({ params: [ { ownerId: DummyValue.UUID, - lastCreationDate: DummyValue.DATE, lastId: DummyValue.UUID, updatedUntil: DummyValue.DATE, limit: 10, }, ], }) - getAllForUserFullSync(options: AssetFullSyncOptions): Promise { + async getAllForUserFullSync(options: AssetFullSyncOptions): Promise { const { ownerId, lastId, updatedUntil, limit } = options; - const builder = this.getBuilder({ - userIds: [ownerId], - exifInfo: false, // need to do this manually because `exifInfo: true` also loads stacked assets messing with `limit` - withStacked: false, // return all assets individually as expected by the app - }) - .leftJoinAndSelect('asset.exifInfo', 'exifInfo') - .leftJoinAndSelect('asset.stack', 'stack') - .loadRelationCountAndMap('stack.assetCount', 'stack.assets', 'stackedAssetsCount'); + const res = await this.prismaRepository.assets.findMany({ + where: { + ownerId, + isVisible: true, + updatedAt: { lte: updatedUntil }, + id: { gt: lastId }, + OR: [{ deletedAt: null }, { deletedAt: { not: null } }], + }, + include: { + exifInfo: true, + stack: true, + }, + orderBy: { + id: 'asc', + }, + take: limit, + }); - if (lastId !== undefined) { - builder.andWhere('asset.id > :lastId', { lastId }); - } - builder - .andWhere('asset.updatedAt <= :updatedUntil', { updatedUntil }) - .orderBy('asset.id', 'ASC') - .limit(limit) // cannot use `take` for performance reasons - .withDeleted(); - return builder.getMany(); + return res as any as AssetEntity[]; } @GenerateSql({ params: [{ userIds: [DummyValue.UUID], updatedAfter: DummyValue.DATE }] }) - getChangedDeltaSync(options: AssetDeltaSyncOptions): Promise { - const builder = this.getBuilder({ - userIds: options.userIds, - exifInfo: false, // need to do this manually because `exifInfo: true` also loads stacked assets messing with `limit` - withStacked: false, // return all assets individually as expected by the app - }) - .leftJoinAndSelect('asset.exifInfo', 'exifInfo') - .leftJoinAndSelect('asset.stack', 'stack') - .loadRelationCountAndMap('stack.assetCount', 'stack.assets', 'stackedAssetsCount') - .andWhere({ updatedAt: MoreThan(options.updatedAfter) }) - .limit(options.limit) // cannot use `take` for performance reasons - .withDeleted(); - return builder.getMany(); + async getChangedDeltaSync(options: AssetDeltaSyncOptions): Promise { + const res = await this.prismaRepository.assets.findMany({ + where: { + ownerId: { in: options.userIds }, + isVisible: true, + updatedAt: { gt: options.updatedAfter }, + OR: [{ deletedAt: null }, { deletedAt: { not: null } }], + }, + include: { + exifInfo: true, + stack: true, + }, + }); + + return res as any as AssetEntity[]; } } diff --git a/server/src/repositories/index.ts b/server/src/repositories/index.ts index 758e82d9e7..b7ec53aa2d 100644 --- a/server/src/repositories/index.ts +++ b/server/src/repositories/index.ts @@ -54,6 +54,7 @@ import { MoveRepository } from 'src/repositories/move.repository'; import { NotificationRepository } from 'src/repositories/notification.repository'; import { PartnerRepository } from 'src/repositories/partner.repository'; import { PersonRepository } from 'src/repositories/person.repository'; +import { PrismaRepository } from 'src/repositories/prisma.repository'; import { SearchRepository } from 'src/repositories/search.repository'; import { ServerInfoRepository } from 'src/repositories/server-info.repository'; import { SessionRepository } from 'src/repositories/session.repository'; @@ -96,4 +97,5 @@ export const repositories = [ { provide: ISystemMetadataRepository, useClass: SystemMetadataRepository }, { provide: ITagRepository, useClass: TagRepository }, { provide: IUserRepository, useClass: UserRepository }, + { provide: PrismaRepository, useClass: PrismaRepository }, ]; diff --git a/server/src/repositories/prisma.repository.ts b/server/src/repositories/prisma.repository.ts new file mode 100644 index 0000000000..b06fa1da7d --- /dev/null +++ b/server/src/repositories/prisma.repository.ts @@ -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[0]) { + super(options); + + return extendClient(this) as this; + } +} + +const ExtendedPrismaClient = UntypedExtendedClient as unknown as new ( + options?: ConstructorParameters[0], +) => ReturnType; + +@Injectable() +export class PrismaRepository extends ExtendedPrismaClient implements OnModuleInit, OnModuleDestroy { + async onModuleInit() { + await this.$connect(); + } + + async onModuleDestroy() { + await this.$disconnect(); + } +} diff --git a/server/src/repositories/search.repository.ts b/server/src/repositories/search.repository.ts index a4c7edab91..c9a829185e 100644 --- a/server/src/repositories/search.repository.ts +++ b/server/src/repositories/search.repository.ts @@ -1,17 +1,14 @@ import { Inject, Injectable } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; -import { getVectorExtension } from 'src/database.config'; +import { Kysely, OrderByDirectionExpression, sql } from 'kysely'; import { DummyValue, GenerateSql } from 'src/decorators'; import { AssetFaceEntity } from 'src/entities/asset-face.entity'; import { AssetEntity, AssetType } from 'src/entities/asset.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 { AssetDuplicateResult, AssetDuplicateSearch, + AssetSearchBuilderOptions, AssetSearchOptions, FaceEmbeddingSearch, FaceSearchResult, @@ -19,40 +16,22 @@ import { SearchPaginationOptions, SmartSearchOptions, } 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 { 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 { Repository, SelectQueryBuilder } from 'typeorm'; @Instrumentation() @Injectable() export class SearchRepository implements ISearchRepository { - private faceColumns: string[]; - private assetsByCityQuery: string; - constructor( - @InjectRepository(SmartInfoEntity) private repository: Repository, - @InjectRepository(AssetEntity) private assetRepository: Repository, - @InjectRepository(AssetFaceEntity) private assetFaceRepository: Repository, - @InjectRepository(SmartSearchEntity) private smartSearchRepository: Repository, - @InjectRepository(GeodataPlacesEntity) private geodataPlacesRepository: Repository, @Inject(ILoggerRepository) private logger: ILoggerRepository, + private prismaRepository: PrismaRepository, ) { 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 { @@ -80,23 +59,16 @@ export class SearchRepository implements ISearchRepository { ], }) async searchMetadata(pagination: SearchPaginationOptions, options: AssetSearchOptions): Paginated { - let builder = this.assetRepository.createQueryBuilder('asset'); - builder = searchAssetBuilder(builder, options); + const orderDirection = (options.orderDirection?.toLowerCase() || 'desc') as OrderByDirectionExpression; + 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'); - return paginatedBuilder(builder, { - mode: PaginationMode.SKIP_TAKE, - skip: (pagination.page - 1) * pagination.size, - take: pagination.size, - }); - } - - private createPersonFilter(builder: SelectQueryBuilder, 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 }); + const items = (await builder.execute()) as any as AssetEntity[]; + const hasNextPage = items.length > pagination.size; + items.splice(pagination.size); + return { items, hasNextPage }; } @GenerateSql({ @@ -112,39 +84,26 @@ export class SearchRepository implements ISearchRepository { }, ], }) - async searchSmart( - pagination: SearchPaginationOptions, - { embedding, userIds, personIds, ...options }: SmartSearchOptions, - ): Paginated { - let results: PaginationResult = { items: [], hasNextPage: false }; + async searchSmart(pagination: SearchPaginationOptions, options: SmartSearchOptions): Paginated { + if (!isValidInteger(pagination.size, { min: 1, max: 1000 })) { + throw new Error(`Invalid value for 'size': ${pagination.size}`); + } - await this.assetRepository.manager.transaction(async (manager) => { - let builder = manager.createQueryBuilder(AssetEntity, 'asset'); + let items: AssetEntity[] = []; + 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) { - 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(builder, { - mode: PaginationMode.LIMIT_OFFSET, - skip: (pagination.page - 1) * pagination.size, - take: pagination.size, - }); + items = (await builder.execute()) as any as AssetEntity[]; }); - return results; + const hasNextPage = items.length > pagination.size; + items.splice(pagination.size); + return { items, hasNextPage }; } @GenerateSql({ @@ -163,31 +122,28 @@ export class SearchRepository implements ISearchRepository { type, userIds, }: AssetDuplicateSearch): Promise { - const cte = this.assetRepository.createQueryBuilder('asset'); - cte - .select('search.assetId', 'assetId') - .addSelect('asset.duplicateId', 'duplicateId') - .addSelect(`search.embedding <=> :embedding`, 'distance') - .innerJoin('asset.smartSearch', 'search') - .where('asset.ownerId IN (:...userIds )') - .andWhere('asset.id != :assetId') - .andWhere('asset.isVisible = :isVisible') - .andWhere('asset.type = :type') - .orderBy('search.embedding <=> :embedding') - .limit(64) - .setParameters({ assetId, embedding: asVector(embedding), isVisible: true, type, userIds }); - - const builder = this.assetRepository.manager - .createQueryBuilder() - .addCommonTableExpression(cte, 'cte') - .from('cte', 'res') - .select('res.*'); - - if (maxDistance) { - builder.where('res.distance <= :maxDistance', { maxDistance }); - } - - return builder.getRawMany() as Promise; + const vector = asVector(embedding); + return this.prismaRepository.$kysely + .with('cte', (qb) => + qb + .selectFrom('assets') + .select([ + 'assets.id as assetId', + 'assets.duplicateId', + sql`smart_search.embedding <=> ${vector}::vector`.as('distance'), + ]) + .innerJoin('smart_search', 'assets.id', 'smart_search.assetId') + .where('assets.ownerId', '=', sql`ANY(ARRAY[${userIds}]::uuid[])`) + .where('assets.isVisible', '=', true) + .where('assets.type', '=', type) + .where('assets.id', '!=', assetId) + .orderBy(sql`smart_search.embedding <=> ${vector}::vector`) + .limit(64), + ) + .selectFrom('cte') + .selectAll() + .where('cte.distance', '<=', maxDistance as number) + .execute(); } @GenerateSql({ @@ -200,104 +156,107 @@ export class SearchRepository implements ISearchRepository { }, ], }) - async searchFaces({ + searchFaces({ userIds, embedding, numResults, maxDistance, hasPerson, }: FaceEmbeddingSearch): Promise { - if (!isValidInteger(numResults, { min: 1 })) { + if (!isValidInteger(numResults, { min: 1, max: 1000 })) { throw new Error(`Invalid value for 'numResults': ${numResults}`); } // setting this too low messes with prefilter recall numResults = Math.max(numResults, 64); - - let results: Array = []; - await this.assetRepository.manager.transaction(async (manager) => { - const cte = manager - .createQueryBuilder(AssetFaceEntity, 'faces') - .select('search.embedding <=> :embedding', 'distance') - .innerJoin('faces.asset', 'asset') - .innerJoin('faces.faceSearch', 'search') - .where('asset.ownerId IN (:...userIds )') - .orderBy('search.embedding <=> :embedding') - .setParameters({ userIds, embedding: asVector(embedding) }); - - cte.limit(numResults); - - if (hasPerson) { - cte.andWhere('faces."personId" IS NOT NULL'); - } - - for (const col of this.faceColumns) { - cte.addSelect(`faces.${col}`, col); - } - - 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(); + const vector = asVector(embedding); + return this.prismaRepository.$transaction(async (tx) => { + await tx.$queryRawUnsafe(`SET LOCAL vectors.hnsw_ef_search = ${numResults}`); + return tx.$kysely + .with('cte', (qb) => + qb + .selectFrom('asset_faces') + .select([ + (eb) => eb.fn.toJson(sql`asset_faces.*`).as('face'), + sql`asset_faces.embedding <=> ${vector}::vector`.as('distance'), + ]) + .innerJoin('assets', 'assets.id', 'asset_faces.assetId') + .where('assets.ownerId', '=', sql`ANY(ARRAY[${userIds}]::uuid[])`) + .$if(!!hasPerson, (qb) => qb.where('asset_faces.personId', 'is not', null)) + .orderBy(sql`asset_faces.embedding <=> ${vector}::vector`) + .limit(numResults), + ) + .selectFrom('cte') + .selectAll() + .where('cte.distance', '<=', maxDistance) + .execute() as any as Array<{ face: AssetFaceEntity; distance: number }>; }); - return results.map((row) => ({ - face: this.assetFaceRepository.create(row), - distance: row.distance, - })); } @GenerateSql({ params: [DummyValue.STRING] }) - async searchPlaces(placeName: string): Promise { - return await this.geodataPlacesRepository - .createQueryBuilder('geoplaces') - .where(`f_unaccent(name) %>> f_unaccent(:placeName)`) - .orWhere(`f_unaccent("admin2Name") %>> f_unaccent(:placeName)`) - .orWhere(`f_unaccent("admin1Name") %>> f_unaccent(:placeName)`) - .orWhere(`f_unaccent("alternateNames") %>> f_unaccent(:placeName)`) + searchPlaces(placeName: string): Promise { + const contains = '%>>' as any as 'ilike'; + return this.prismaRepository.$kysely + .selectFrom('geodata_places') + .selectAll() + .where((eb) => + 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( - ` - COALESCE(f_unaccent(name) <->>> 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("alternateNames") <->>> f_unaccent(:placeName), 0.1) + sql` + COALESCE(f_unaccent(name) <->>> 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("alternateNames") <->>> f_unaccent(${placeName}), 0.1) `, ) - .setParameters({ placeName }) .limit(20) - .getMany(); + .execute() as Promise; } @GenerateSql({ params: [[DummyValue.UUID]] }) - async getAssetsByCity(userIds: string[]): Promise { - const parameters = [userIds, true, false, AssetType.IMAGE]; - const rawRes = await this.repository.query(this.assetsByCityQuery, parameters); + getAssetsByCity(userIds: string[]): Promise { + // the performance difference between this and the normal way is too huge to ignore, e.g. 3s vs 4ms + 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[] = []; - for (const res of rawRes) { - const item = { exifInfo: {} as Record } as Record; - 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); - } + UNION ALL - 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 { - await this.smartSearchRepository.upsert( - { assetId, embedding: () => asVector(embedding, true) }, - { conflictPaths: ['assetId'] }, - ); + await this.prismaRepository.$kysely + .insertInto('smart_search') + .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 { @@ -312,28 +271,28 @@ export class SearchRepository implements ISearchRepository { this.logger.log(`Updating database CLIP dimension size to ${dimSize}.`); - await this.smartSearchRepository.manager.transaction(async (manager) => { - await manager.clear(SmartSearchEntity); - await manager.query(`ALTER TABLE smart_search ALTER COLUMN embedding SET DATA TYPE vector(${dimSize})`); - await manager.query(`REINDEX INDEX clip_index`); + await this.prismaRepository.$transaction(async (tx) => { + await tx.$queryRawUnsafe(`TRUNCATE smart_search`); + await tx.$queryRawUnsafe(`ALTER TABLE smart_search ALTER COLUMN embedding SET DATA TYPE vector(${dimSize})`); + await tx.$queryRawUnsafe(`REINDEX INDEX clip_index`); }); this.logger.log(`Successfully updated database CLIP dimension size from ${curDimSize} to ${dimSize}.`); } deleteAllSearchEmbeddings(): Promise { - return this.smartSearchRepository.clear(); + return this.prismaRepository.$queryRawUnsafe(`TRUNCATE smart_search`); } private async getDimSize(): Promise { - const res = await this.smartSearchRepository.manager.query(` + const res = await this.prismaRepository.$queryRaw<[{ dimsize: number }]>` SELECT atttypmod as dimsize FROM pg_attribute f JOIN pg_class c ON c.oid = f.attrelid WHERE c.relkind = 'r'::char AND f.attnum > 0 AND c.relname = 'smart_search' - AND f.attname = 'embedding'`); + AND f.attname = 'embedding'`; const dimSize = res[0]['dimsize']; if (!isValidInteger(dimSize, { min: 1, max: 2 ** 16 })) { @@ -342,43 +301,90 @@ export class SearchRepository implements ISearchRepository { return dimSize; } - private getRuntimeConfig(numResults?: number): string { - if (getVectorExtension() === DatabaseExtension.VECTOR) { - return 'SET LOCAL hnsw.ef_search = 1000;'; // mitigate post-filter recall - } - - let runtimeConfig = 'SET LOCAL vectors.enable_prefilter=on; SET LOCAL vectors.search_mode=vbase;'; - if (numResults) { - runtimeConfig += ` SET LOCAL vectors.hnsw_ef_search = ${numResults};`; - } - - return runtimeConfig; + private searchAssetBuilder(options: AssetSearchBuilderOptions, kysely: Kysely = this.prismaRepository.$kysely) { + options.isArchived ??= options.withArchived ? undefined : false; + options.withDeleted ??= !!(options.trashedAfter || options.trashedBefore); + const query = kysely + .selectFrom('assets') + .selectAll('assets') + .$if(!!options.createdBefore, (qb) => qb.where('assets.createdAt', '<=', options.createdBefore as Date)) + .$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)) + .$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`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 -) -`; diff --git a/server/src/services/asset-media.service.ts b/server/src/services/asset-media.service.ts index 8895e1c369..593d319b65 100644 --- a/server/src/services/asset-media.service.ts +++ b/server/src/services/asset-media.service.ts @@ -371,7 +371,7 @@ export class AssetMediaService { localDateTime: dto.fileCreatedAt, duration: dto.duration || null, - livePhotoVideo: null, + livePhotoVideoId: null, sidecarPath: sidecarPath || null, }); diff --git a/server/src/services/asset.service.spec.ts b/server/src/services/asset.service.spec.ts index c2edc63985..264e930af2 100755 --- a/server/src/services/asset.service.spec.ts +++ b/server/src/services/asset.service.spec.ts @@ -218,14 +218,20 @@ describe(AssetService.name, () => { it('should update the asset', async () => { accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1'])); assetMock.getById.mockResolvedValue(assetStub.image); + assetMock.update.mockResolvedValue(assetStub.image); + await sut.update(authStub.admin, 'asset-1', { isFavorite: true }); + expect(assetMock.update).toHaveBeenCalledWith({ id: 'asset-1', isFavorite: true }); }); it('should update the exif description', async () => { accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1'])); assetMock.getById.mockResolvedValue(assetStub.image); + assetMock.update.mockResolvedValue(assetStub.image); + await sut.update(authStub.admin, 'asset-1', { description: 'Test description' }); + expect(assetMock.upsertExif).toHaveBeenCalledWith({ assetId: 'asset-1', description: 'Test description' }); }); }); diff --git a/server/src/services/asset.service.ts b/server/src/services/asset.service.ts index 2a3d5aceb2..fdec5660dd 100644 --- a/server/src/services/asset.service.ts +++ b/server/src/services/asset.service.ts @@ -110,29 +110,26 @@ export class AssetService { async get(auth: AuthDto, id: string): Promise { await this.access.requirePermission(auth, Permission.ASSET_READ, id); - const asset = await this.assetRepository.getById( - id, - { - exifInfo: true, - tags: true, - sharedLinks: true, - smartInfo: true, - owner: true, - faces: { - person: true, - }, - stack: { + const asset = await this.assetRepository.getById(id, { + exifInfo: true, + tags: true, + sharedLinks: true, + smartInfo: true, + owner: true, + faces: { + include: { person: true }, + orderBy: { boundingBoxX1: 'asc' }, + }, + stack: { + include: { assets: { - exifInfo: true, + include: { + exifInfo: true, + }, }, }, }, - { - faces: { - boundingBoxX1: 'ASC', - }, - }, - ); + }); if (!asset) { throw new BadRequestException('Asset not found'); @@ -161,16 +158,7 @@ export class AssetService { const { description, dateTimeOriginal, latitude, longitude, ...rest } = dto; await this.updateMetadata({ id, description, dateTimeOriginal, latitude, longitude }); - await this.assetRepository.update({ id, ...rest }); - const asset = await this.assetRepository.getById(id, { - exifInfo: true, - owner: true, - smartInfo: true, - tags: true, - faces: { - person: true, - }, - }); + const asset = await this.assetRepository.update({ id, ...rest }); if (!asset) { throw new BadRequestException('Asset not found'); } @@ -196,14 +184,16 @@ export class AssetService { } else if (options.stackParentId) { //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); - 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) { throw new BadRequestException('Asset not found for given stackParentId'); } let stack = primaryAsset.stack; 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( ...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, { faces: { - person: true, + include: { person: true }, }, library: true, - stack: { assets: true }, + stack: { include: { assets: true } }, exifInfo: true, }); @@ -351,11 +341,11 @@ export class AssetService { const childIds: string[] = []; const oldParent = await this.assetRepository.getById(oldParentId, { faces: { - person: true, + include: { person: true }, }, library: true, stack: { - assets: true, + include: { assets: true }, }, }); if (!oldParent?.stackId) { diff --git a/server/src/services/duplicate.service.ts b/server/src/services/duplicate.service.ts index ae9d101c58..377280a9f0 100644 --- a/server/src/services/duplicate.service.ts +++ b/server/src/services/duplicate.service.ts @@ -37,7 +37,7 @@ export class DuplicateService { } async getDuplicates(auth: AuthDto): Promise { - 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 }))); } diff --git a/server/src/services/person.service.ts b/server/src/services/person.service.ts index 05034dc6f9..47b90ed2a0 100644 --- a/server/src/services/person.service.ts +++ b/server/src/services/person.service.ts @@ -319,13 +319,7 @@ export class PersonService { return JobStatus.SKIPPED; } - const relations = { - exifInfo: true, - faces: { - person: false, - }, - }; - const [asset] = await this.assetRepository.getByIds([id], relations); + const [asset] = await this.assetRepository.getByIds([id], { exifInfo: true, faces: true }); if (!asset || !asset.previewPath || asset.faces?.length > 0) { return JobStatus.FAILED; } diff --git a/server/src/services/timeline.service.spec.ts b/server/src/services/timeline.service.spec.ts index 981fc11c3f..93918bc03c 100644 --- a/server/src/services/timeline.service.spec.ts +++ b/server/src/services/timeline.service.spec.ts @@ -4,6 +4,7 @@ import { IPartnerRepository } from 'src/interfaces/partner.interface'; import { TimelineService } from 'src/services/timeline.service'; import { assetStub } from 'test/fixtures/asset.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 { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock'; import { newPartnerRepositoryMock } from 'test/repositories/partner.repository.mock'; @@ -52,6 +53,7 @@ describe(TimelineService.name, () => { size: TimeBucketSize.DAY, timeBucket: 'bucket', albumId: 'album-id', + userIds: [authStub.admin.user.id], }); }); @@ -66,12 +68,15 @@ describe(TimelineService.name, () => { userId: authStub.admin.user.id, }), ).resolves.toEqual(expect.arrayContaining([expect.objectContaining({ id: 'asset-id' })])); - expect(assetMock.getTimeBucket).toHaveBeenCalledWith('bucket', { - size: TimeBucketSize.DAY, - timeBucket: 'bucket', - isArchived: true, - userIds: [authStub.admin.user.id], - }); + expect(assetMock.getTimeBucket).toHaveBeenCalledWith( + 'bucket', + expect.objectContaining({ + size: TimeBucketSize.DAY, + 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 () => { @@ -84,11 +89,14 @@ describe(TimelineService.name, () => { userId: authStub.admin.user.id, }), ).resolves.toEqual(expect.arrayContaining([expect.objectContaining({ id: 'asset-id' })])); - expect(assetMock.getTimeBucket).toHaveBeenCalledWith('bucket', { - size: TimeBucketSize.DAY, - timeBucket: 'bucket', - userIds: [authStub.admin.user.id], - }); + expect(assetMock.getTimeBucket).toHaveBeenCalledWith( + 'bucket', + expect.objectContaining({ + 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 () => { diff --git a/server/src/services/timeline.service.ts b/server/src/services/timeline.service.ts index b82a16f139..d98107a331 100644 --- a/server/src/services/timeline.service.ts +++ b/server/src/services/timeline.service.ts @@ -60,15 +60,6 @@ export class TimelineService { private async timeBucketChecks(auth: AuthDto, dto: TimeBucketDto) { if (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) { diff --git a/server/src/utils/database.ts b/server/src/utils/database.ts index 944978bddd..13f1526fc2 100644 --- a/server/src/utils/database.ts +++ b/server/src/utils/database.ts @@ -1,6 +1,9 @@ +import { ExpressionBuilder } from 'kysely'; +import { jsonArrayFrom, jsonObjectFrom } from 'kysely/helpers/postgres'; import _ from 'lodash'; import { AssetEntity } from 'src/entities/asset.entity'; import { AssetSearchBuilderOptions } from 'src/interfaces/search.interface'; +import { DB } from 'src/prisma/generated/types'; import { Between, IsNull, LessThanOrEqual, MoreThanOrEqual, Not, SelectQueryBuilder } from 'typeorm'; /** @@ -142,3 +145,20 @@ export function searchAssetBuilder( return builder; } + +export const withExif = (eb: ExpressionBuilder) => + jsonObjectFrom(eb.selectFrom('exif').selectAll().whereRef('exif.assetId', '=', 'assets.id')).as('exifInfo'); + +export const withSmartInfo = (eb: ExpressionBuilder) => + jsonObjectFrom(eb.selectFrom('smart_info').selectAll().whereRef('smart_info.assetId', '=', 'assets.id')).as( + 'smartInfo', + ); + +export const withFaces = (eb: ExpressionBuilder) => + jsonArrayFrom(eb.selectFrom('asset_faces').selectAll().whereRef('asset_faces.assetId', '=', 'assets.id')).as('faces'); + +export const withPeople = (eb: ExpressionBuilder) => + jsonObjectFrom(eb.selectFrom('person').selectAll().whereRef('asset_faces.personId', '=', 'person.id')).as('people'); + +export const withOwner = (eb: ExpressionBuilder) => + jsonObjectFrom(eb.selectFrom('users').selectAll().whereRef('users.id', '=', 'assets.ownerId')).as('owner'); diff --git a/server/src/utils/pagination.ts b/server/src/utils/pagination.ts index dec1a9de0c..6ca2767649 100644 --- a/server/src/utils/pagination.ts +++ b/server/src/utils/pagination.ts @@ -37,7 +37,10 @@ export async function* usePagination( } } -function paginationHelper(items: Entity[], take: number): PaginationResult { +export function paginationHelper( + items: Entity[], + take: number, +): PaginationResult { const hasNextPage = items.length > take; items.splice(take); diff --git a/server/tsconfig.json b/server/tsconfig.json index 3e84bcf7d7..55cf59a046 100644 --- a/server/tsconfig.json +++ b/server/tsconfig.json @@ -18,7 +18,8 @@ "preserveWatchOutput": true, "baseUrl": "./", "jsx": "react", - "types": ["vitest/globals"] + "types": ["vitest/globals"], + "noErrorTruncation": true }, "exclude": ["dist", "node_modules", "upload"] }