diff --git a/.vscode/settings.json b/.vscode/settings.json index 1e20aba9..9e710f6c 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -39,6 +39,7 @@ "skippable", "sonner", "Streamable", + "Supabase", "thephotoblog", "trpc", "unnest", diff --git a/README.md b/README.md index 393de133..09e8faf5 100644 --- a/README.md +++ b/README.md @@ -29,19 +29,19 @@ Installation 1. Click [Deploy](https://vercel.com/new/clone?demo-title=Photo+Blog&demo-description=Store+photos+with+original+camera+data&demo-url=https%3A%2F%2Fphotos.sambecker.com&demo-image=https%3A%2F%2Fphotos.sambecker.com%2Ftemplate-image-tight&project-name=Photo+Blog&repository-name=exif-photo-blog&repository-url=https%3A%2F%2Fgithub.com%2Fsambecker%2Fexif-photo-blog&from=templates&skippable-integrations=1&teamCreateStatus=hidden&stores=%5B%7B%22type%22%3A%22postgres%22%7D%2C%7B%22type%22%3A%22blob%22%7D%5D) 2. Add required storage ([Vercel Postgres](https://vercel.com/docs/storage/vercel-postgres/quickstart#create-a-postgres-database) + [Vercel Blob](https://vercel.com/docs/storage/vercel-blob/quickstart#create-a-blob-store)) as part of template installation 3. Configure environment variables from project settings: -- `NEXT_PUBLIC_SITE_TITLE` (e.g., My Photos) -- `NEXT_PUBLIC_SITE_DOMAIN` (e.g., photos.domain.com) -- `NEXT_PUBLIC_SITE_DESCRIPTION` (optional—mainly used for OG meta) + - `NEXT_PUBLIC_SITE_TITLE` (e.g., My Photos) + - `NEXT_PUBLIC_SITE_DOMAIN` (e.g., photos.domain.com) + - `NEXT_PUBLIC_SITE_DESCRIPTION` (optional—mainly used for OG meta) ### 2. Setup Auth 1. [Generate auth secret](https://generate-secret.vercel.app/32) and add to environment variables: -- `AUTH_SECRET` + - `AUTH_SECRET` 2. Add admin user to environment variables: -- `ADMIN_EMAIL` -- `ADMIN_PASSWORD` + - `ADMIN_EMAIL` + - `ADMIN_PASSWORD` 3. Trigger redeploy -- Visit project on Vercel, navigate to "Deployments" tab, click ••• button next to most recent deployment, and select "Redeploy" + - Visit project on Vercel, navigate to "Deployments" tab, click ••• button next to most recent deployment, and select "Redeploy" ### 3. Upload your first photo 🎉 1. Visit `/admin` @@ -105,6 +105,14 @@ Application behavior can be changed by configuring the following environment var - `NEXT_PUBLIC_GRID_ASPECT_RATIO = 1.5` sets aspect ratio for grid tiles (defaults to `1`—setting to `0` removes the constraint) - `NEXT_PUBLIC_OG_TEXT_ALIGNMENT = BOTTOM` keeps OG image text bottom aligned (default is top) +## Alternate database providers (experimental) + +Vercel Postgres can be switched to another Postgres-compatible, pooling provider by updating `POSTGRES_URL`. Some providers only work when SSL is disabled, which can configured by setting `DISABLE_POSTGRES_SSL = 1`. + +### Supabase +1. Ensure connection string is set to "Transaction Mode" via port `6543` +2. Disable SSL by setting `DISABLE_POSTGRES_SSL = 1` + ## Alternate storage providers Only one storage adapter—Vercel Blob, Cloudflare R2, or AWS S3—can be used at a time. Ideally, this is configured before photos are uploaded (see [Issue #34](https://github.com/sambecker/exif-photo-blog/issues/34) for migration considerations). If you have multiple adapters, you can set one as preferred by storing "aws-s3," "cloudflare-r2," or "vercel-blob" in `NEXT_PUBLIC_STORAGE_PREFERENCE`. diff --git a/package.json b/package.json index a088a2d2..9ef6f8ec 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "@testing-library/react": "^15.0.5", "@types/jest": "^29.5.12", "@types/node": "^20.12.7", + "@types/pg": "^8.11.5", "@types/react": "18.3.1", "@types/react-dom": "18.3.0", "@typescript-eslint/eslint-plugin": "^7.7.1", @@ -27,7 +28,6 @@ "@vercel/analytics": "^1.2.2", "@vercel/blob": "^0.23.2", "@vercel/kv": "^1.0.1", - "@vercel/postgres": "^0.8.0", "@vercel/speed-insights": "^1.0.10", "ai": "^3.0.34", "autoprefixer": "10.4.19", @@ -42,10 +42,11 @@ "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", "nanoid": "^5.0.7", - "next": "14.3.0-canary.29", + "next": "14.3.0-canary.37", "next-auth": "5.0.0-beta.15", "next-themes": "^0.3.0", "openai": "^4.38.5", + "pg": "^8.11.5", "postcss": "8.4.38", "react": "18.3.1", "react-dom": "18.3.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d485f9cf..777cae6e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -38,6 +38,9 @@ importers: '@types/node': specifier: ^20.12.7 version: 20.12.7 + '@types/pg': + specifier: ^8.11.5 + version: 8.11.5 '@types/react': specifier: 18.3.1 version: 18.3.1 @@ -55,19 +58,16 @@ importers: version: 1.1.3 '@vercel/analytics': specifier: ^1.2.2 - version: 1.2.2(next@14.3.0-canary.29(@babel/core@7.24.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) + version: 1.2.2(next@14.3.0-canary.37(@babel/core@7.24.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) '@vercel/blob': specifier: ^0.23.2 version: 0.23.2 '@vercel/kv': specifier: ^1.0.1 version: 1.0.1 - '@vercel/postgres': - specifier: ^0.8.0 - version: 0.8.0 '@vercel/speed-insights': specifier: ^1.0.10 - version: 1.0.10(next@14.3.0-canary.29(@babel/core@7.24.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)(svelte@4.2.15)(vue@3.4.25(typescript@5.4.5)) + version: 1.0.10(next@14.3.0-canary.37(@babel/core@7.24.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)(svelte@4.2.15)(vue@3.4.25(typescript@5.4.5)) ai: specifier: ^3.0.34 version: 3.0.34(react@18.3.1)(solid-js@1.8.17)(svelte@4.2.15)(vue@3.4.25(typescript@5.4.5))(zod@3.23.4) @@ -108,17 +108,20 @@ importers: specifier: ^5.0.7 version: 5.0.7 next: - specifier: 14.3.0-canary.29 - version: 14.3.0-canary.29(@babel/core@7.24.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + specifier: 14.3.0-canary.37 + version: 14.3.0-canary.37(@babel/core@7.24.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) next-auth: specifier: 5.0.0-beta.15 - version: 5.0.0-beta.15(next@14.3.0-canary.29(@babel/core@7.24.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) + version: 5.0.0-beta.15(next@14.3.0-canary.37(@babel/core@7.24.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) next-themes: specifier: ^0.3.0 version: 0.3.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) openai: specifier: ^4.38.5 version: 4.38.5 + pg: + specifier: ^8.11.5 + version: 8.11.5 postcss: specifier: 8.4.38 version: 8.4.38 @@ -800,68 +803,65 @@ packages: '@jridgewell/trace-mapping@0.3.25': resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} - '@neondatabase/serverless@0.7.2': - resolution: {integrity: sha512-wU3WA2uTyNO7wjPs3Mg0G01jztAxUxzd9/mskMmtPwPTjf7JKWi9AW5/puOGXLxmZ9PVgRFeBVRVYq5nBPhsCg==} - '@next/bundle-analyzer@14.2.3': resolution: {integrity: sha512-Z88hbbngMs7njZKI8kTJIlpdLKYfMSLwnsqYe54AP4aLmgL70/Ynx/J201DQ+q2Lr6FxFw1uCeLGImDrHOl2ZA==} - '@next/env@14.3.0-canary.29': - resolution: {integrity: sha512-h5It2nnx97Y+VjVIEP+gLrQ/byTN34bAtHhdQui1fkdlsaBD/B7sjt6BITSJK4zk1IPztdoPHHaI8sMONVbeeg==} + '@next/env@14.3.0-canary.37': + resolution: {integrity: sha512-pZMCjC6cG4MEemm3mG+Ac1qzUgAABrqCnB71hjgPXe1adacL/wK7YliSUtoyC9Eurw4Pm0mBi4SCg0z6ymbMVg==} '@next/eslint-plugin-next@14.2.3': resolution: {integrity: sha512-L3oDricIIjgj1AVnRdRor21gI7mShlSwU/1ZGHmqM3LzHhXXhdkrfeNY5zif25Bi5Dd7fiJHsbhoZCHfXYvlAw==} - '@next/swc-darwin-arm64@14.3.0-canary.29': - resolution: {integrity: sha512-ePqJ/d/7dYgdf+7YefmAdW08m5jysGw6PtB309fZbpm0BIUTLfSdShJKG+t6Ivft2jzYIwcfp3LoFHotG8XZTg==} + '@next/swc-darwin-arm64@14.3.0-canary.37': + resolution: {integrity: sha512-AMH81oibPiLqhDpSuTinVwO27mvGtXCgq7puAhuFhVTTGfATBh/flwed7utC+/K6bC8dkd5QsRMV4+sxySdLeA==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@next/swc-darwin-x64@14.3.0-canary.29': - resolution: {integrity: sha512-H/Ese4aLROI3V65DpzKDUUEPtyQir+oNAdrZM1PPFPTRDRMpHz2LvbdcsxQ/aeIkYfRAc+foj5unnFjpIdyFIg==} + '@next/swc-darwin-x64@14.3.0-canary.37': + resolution: {integrity: sha512-fsFpGflXJyEXwD5k9fjMHOie6hj641hKAn4em8e3ohagH8nnb7OpC2QAtxv9WCkdSnH2Lv3OcLqsL4quieMKmg==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@next/swc-linux-arm64-gnu@14.3.0-canary.29': - resolution: {integrity: sha512-hW1DOdQAw4oOV6rAwBZh4R1i24xYwjoXgAKHbz5BlVq9QFEW5Zku0srzSl4eDPG2VEgZWcFCkmMamqR+gZheJA==} + '@next/swc-linux-arm64-gnu@14.3.0-canary.37': + resolution: {integrity: sha512-+PtZuhB5WwMICXMfmtK6Ax3S+CZkgj2++M/G/8wJfifghoeesVQgmVlFLgVtjApE2wQXwYJture3LlqteeM7vw==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@next/swc-linux-arm64-musl@14.3.0-canary.29': - resolution: {integrity: sha512-tn2OT3NUyd/B2q5jnwtbM2pta/r+eizQFgM8ZMxhGHsl0Md3qmCE8TqR6PjiTTipcrG7KP7qElxwWkyYiEGYIg==} + '@next/swc-linux-arm64-musl@14.3.0-canary.37': + resolution: {integrity: sha512-tgvJKQq68cGyrsIjm0X5jkGOlHMvJwRfUf7UAHTHcqtbwXF/SLLDkQHlCtnPXgaBYEEIH6GG9loo3+v/nzLzQg==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@next/swc-linux-x64-gnu@14.3.0-canary.29': - resolution: {integrity: sha512-S2EfPr1qbU2y/+dJPdnr3aJIOxS0vsYz+VDAccUdbmXhzM3RSdsaE0okZWztih8AmKiiCS69MFWo6hUKnlOL4A==} + '@next/swc-linux-x64-gnu@14.3.0-canary.37': + resolution: {integrity: sha512-tmQmL6WJ6DUFOLcIy8BFHwTzjJKLK7xp35OWdoOV/GibL4T9OSDkMAtKxRALvFsQ71LsIF+O69qDrvZ+35i+rA==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@next/swc-linux-x64-musl@14.3.0-canary.29': - resolution: {integrity: sha512-DfTjNpx1Szk82LEHRq1u57tSjGBrg3+kgIfM5/O0OUdU+6ESEOdaLlf7YrD1gIu3k4lDfn37b4S7R/BmPme0tg==} + '@next/swc-linux-x64-musl@14.3.0-canary.37': + resolution: {integrity: sha512-kXs2JVLdL1MWkFKMfVavlRJl9+Ep0xFLyrrJrxxyxBFp2qNXYqKXjEXXqYpKv0btnSHf7Tokt89TZztRxoaJlw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@next/swc-win32-arm64-msvc@14.3.0-canary.29': - resolution: {integrity: sha512-C9Igll3rnPoRFgUUFCJMOQkmAFEDiQnVPO2tfxRyleXQP9jpSYY55PYpHGFOp6L0iNy8zc/PuznmR3pcS3E2fQ==} + '@next/swc-win32-arm64-msvc@14.3.0-canary.37': + resolution: {integrity: sha512-HU18Co5cE7Hgrjwac6CzdtxErJJ1KHVBoVegMpH6BWBjWASv05vM9yK6fJyRsWG1mJu152Ozr0TgpXBOAkA7UQ==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@next/swc-win32-ia32-msvc@14.3.0-canary.29': - resolution: {integrity: sha512-5ItF8rCzPMFhG9mlcPl5YpccUyhj3G5ElkjacRJYnqok2GQVmXtu6Ot5+LEGT86TCpizzTx+NkkY/lBSu28RRw==} + '@next/swc-win32-ia32-msvc@14.3.0-canary.37': + resolution: {integrity: sha512-sSq2frpcGuriFymNBFyppV7Vli8q6+jEyYBbqJo9QHXj5swvTksirnws9vwPM0PALfM6HPMgA1YkEW3DITWq9Q==} engines: {node: '>= 10'} cpu: [ia32] os: [win32] - '@next/swc-win32-x64-msvc@14.3.0-canary.29': - resolution: {integrity: sha512-Bjd22hlcK4Kf4/jqPz6GyRiZ+h2Oa/h64RnZFrAvFJJfpBBMIQlihYLoS6HRwdS+bAXPrlf6cOz4TLVdFnum4w==} + '@next/swc-win32-x64-msvc@14.3.0-canary.37': + resolution: {integrity: sha512-Qs4JJYMRG7wq/BTDHoNIz8kmbEk4O08dswfgCH3nKbDKCYJ1z/UYedksNrydRFmVyHmYMLyqtxlbUXgPwI02yA==} engines: {node: '>= 10'} cpu: [x64] os: [win32] @@ -1467,8 +1467,8 @@ packages: '@types/node@20.12.7': resolution: {integrity: sha512-wq0cICSkRLVaf3UGLMGItu/PtdY7oaXaI/RVU+xliKVOtRna3PRY57ZDfztpDL0n11vfymMUnXv8QwYCO7L1wg==} - '@types/pg@8.6.6': - resolution: {integrity: sha512-O2xNmXebtwVekJDD+02udOncjVcMZQuTEQEMpKJ0ZRf5E7/9JJX3izhKUcUifBkyKpljyUM6BTgy2trmviKlpw==} + '@types/pg@8.11.5': + resolution: {integrity: sha512-2xMjVviMxneZHDHX5p5S6tsRRs7TpDHeeK7kTTMe/kAC/mRRNjWHjZg0rkiY+e17jXSZV3zJYDxXV8Cy72/Vuw==} '@types/prop-types@15.7.12': resolution: {integrity: sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==} @@ -1618,10 +1618,6 @@ packages: resolution: {integrity: sha512-uTKddsqVYS2GRAM/QMNNXCTuw9N742mLoGRXoNDcyECaxEXvIHG0dEY+ZnYISV4Vz534VwJO+64fd9XeSggSKw==} engines: {node: '>=14.6'} - '@vercel/postgres@0.8.0': - resolution: {integrity: sha512-/QUV9ExwaNdKooRjOQqvrKNVnRvsaXeukPNI5DB1ovUTesglfR/fparw7ngo1KUWWKIVpEj2TRrA+ObRHRdaLg==} - engines: {node: '>=14.6'} - '@vercel/speed-insights@1.0.10': resolution: {integrity: sha512-4uzdKB0RW6Ff2FkzshzjZ+RlJfLPxgm/00i0XXgxfMPhwnnsk92YgtqsxT9OcPLdJUyVU1DqFlSWWjIQMPkh0g==} peerDependencies: @@ -3260,8 +3256,8 @@ packages: react: ^16.8 || ^17 || ^18 react-dom: ^16.8 || ^17 || ^18 - next@14.3.0-canary.29: - resolution: {integrity: sha512-+4mx9dCIVBcW5jMs67jQ2ZCnb3VQ6AQPNBW6t8j17T+9Mp+h0c1XLzrCbfRETpDfdE0nfID2ilUYpQY/8oairg==} + next@14.3.0-canary.37: + resolution: {integrity: sha512-tWIT0Lep3IS+O3RELN4RUWqNHsQeqWJgf43riJgV3mJoWdRSlX610xyif63ZGwaz8Qd6MbvuGbtNWDnWCBZf0w==} engines: {node: '>=18.17.0'} hasBin: true peerDependencies: @@ -3358,6 +3354,9 @@ packages: resolution: {integrity: sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ==} engines: {node: '>= 0.4'} + obuf@1.1.2: + resolution: {integrity: sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==} + once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} @@ -3434,10 +3433,25 @@ packages: periscopic@3.1.0: resolution: {integrity: sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw==} + pg-cloudflare@1.1.1: + resolution: {integrity: sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q==} + + pg-connection-string@2.6.4: + resolution: {integrity: sha512-v+Z7W/0EO707aNMaAEfiGnGL9sxxumwLl2fJvCQtMn9Fxsg+lPpPkdcyBSv/KFgpGdYkMfn+EI1Or2EHjpgLCA==} + pg-int8@1.0.1: resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} engines: {node: '>=4.0.0'} + pg-numeric@1.0.2: + resolution: {integrity: sha512-BM/Thnrw5jm2kKLE5uJkXqqExRUY/toLHda65XgFTBTFYZyopbKjBe29Ii3RbkvlsMoFwD+tHeGaCjjv0gHlyw==} + engines: {node: '>=4'} + + pg-pool@3.6.2: + resolution: {integrity: sha512-Htjbg8BlwXqSBQ9V8Vjtc+vzf/6fVUuak/3/XXKA9oxZprwW3IMDQTGHP+KDmVL7rtd+R1QjbnCFPuTHm3G4hg==} + peerDependencies: + pg: '>=8.0' + pg-protocol@1.6.1: resolution: {integrity: sha512-jPIlvgoD63hrEuihvIg+tJhoGjUsLPn6poJY9N5CnlPd91c2T18T/9zBtLxZSb1EhYxBRoZJtzScCaWlYLtktg==} @@ -3445,6 +3459,22 @@ packages: resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} engines: {node: '>=4'} + pg-types@4.0.2: + resolution: {integrity: sha512-cRL3JpS3lKMGsKaWndugWQoLOCoP+Cic8oseVcbr0qhPzYD5DWXK+RZ9LY9wxRf7RQia4SCwQlXk0q6FCPrVng==} + engines: {node: '>=10'} + + pg@8.11.5: + resolution: {integrity: sha512-jqgNHSKL5cbDjFlHyYsCXmQDrfIX/3RsNwYqpd4N0Kt8niLuNoRNH+aazv6cOd43gPh9Y4DjQCtb+X0MH0Hvnw==} + engines: {node: '>= 8.0.0'} + peerDependencies: + pg-native: '>=3.0.1' + peerDependenciesMeta: + pg-native: + optional: true + + pgpass@1.0.5: + resolution: {integrity: sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==} + picocolors@1.0.0: resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} @@ -3517,18 +3547,37 @@ packages: resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} engines: {node: '>=4'} + postgres-array@3.0.2: + resolution: {integrity: sha512-6faShkdFugNQCLwucjPcY5ARoW1SlbnrZjmGl0IrrqewpvxvhSLHimCVzqeuULCbG0fQv7Dtk1yDbG3xv7Veog==} + engines: {node: '>=12'} + postgres-bytea@1.0.0: resolution: {integrity: sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==} engines: {node: '>=0.10.0'} + postgres-bytea@3.0.0: + resolution: {integrity: sha512-CNd4jim9RFPkObHSjVHlVrxoVQXz7quwNFpz7RY1okNNme49+sVyiTvTRobiLV548Hx/hb1BG+iE7h9493WzFw==} + engines: {node: '>= 6'} + postgres-date@1.0.7: resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==} engines: {node: '>=0.10.0'} + postgres-date@2.1.0: + resolution: {integrity: sha512-K7Juri8gtgXVcDfZttFKVmhglp7epKb1K4pgrkLxehjqkrgPhfG6OO8LHLkfaqkbpjNRnra018XwAr1yQFWGcA==} + engines: {node: '>=12'} + postgres-interval@1.2.0: resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} engines: {node: '>=0.10.0'} + postgres-interval@3.0.0: + resolution: {integrity: sha512-BSNDnbyZCXSxgA+1f5UU2GmwhoI0aU5yMxRGO8CdFEcY2BQF9xm/7MqKnYoM1nJDk8nONNWDk9WeSmePFhQdlw==} + engines: {node: '>=12'} + + postgres-range@1.1.4: + resolution: {integrity: sha512-i/hbxIE9803Alj/6ytL7UHQxRvZkI9O4Sy+J3HGc4F4oo/2eQAjTSNJ0bfxyse3bH0nuVesCk+3IRLaMtG3H6w==} + preact-render-to-string@5.2.3: resolution: {integrity: sha512-aPDxUn5o3GhWdtJtW0svRC2SS/l8D9MAgo2+AWml+BhDImb27ALf04Q2d+AHqUUOc6RdSXFIBVa2gxzgMKgtZA==} peerDependencies: @@ -3821,6 +3870,10 @@ packages: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} engines: {node: '>=0.10.0'} + split2@4.2.0: + resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} + engines: {node: '>= 10.x'} + sprintf-js@1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} @@ -4242,18 +4295,6 @@ packages: utf-8-validate: optional: true - ws@8.14.2: - resolution: {integrity: sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==} - engines: {node: '>=10.0.0'} - peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: '>=5.0.2' - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true - ws@8.16.0: resolution: {integrity: sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==} engines: {node: '>=10.0.0'} @@ -5405,10 +5446,6 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.4.15 - '@neondatabase/serverless@0.7.2': - dependencies: - '@types/pg': 8.6.6 - '@next/bundle-analyzer@14.2.3(bufferutil@4.0.8)(utf-8-validate@6.0.3)': dependencies: webpack-bundle-analyzer: 4.10.1(bufferutil@4.0.8)(utf-8-validate@6.0.3) @@ -5416,37 +5453,37 @@ snapshots: - bufferutil - utf-8-validate - '@next/env@14.3.0-canary.29': {} + '@next/env@14.3.0-canary.37': {} '@next/eslint-plugin-next@14.2.3': dependencies: glob: 10.3.10 - '@next/swc-darwin-arm64@14.3.0-canary.29': + '@next/swc-darwin-arm64@14.3.0-canary.37': optional: true - '@next/swc-darwin-x64@14.3.0-canary.29': + '@next/swc-darwin-x64@14.3.0-canary.37': optional: true - '@next/swc-linux-arm64-gnu@14.3.0-canary.29': + '@next/swc-linux-arm64-gnu@14.3.0-canary.37': optional: true - '@next/swc-linux-arm64-musl@14.3.0-canary.29': + '@next/swc-linux-arm64-musl@14.3.0-canary.37': optional: true - '@next/swc-linux-x64-gnu@14.3.0-canary.29': + '@next/swc-linux-x64-gnu@14.3.0-canary.37': optional: true - '@next/swc-linux-x64-musl@14.3.0-canary.29': + '@next/swc-linux-x64-musl@14.3.0-canary.37': optional: true - '@next/swc-win32-arm64-msvc@14.3.0-canary.29': + '@next/swc-win32-arm64-msvc@14.3.0-canary.37': optional: true - '@next/swc-win32-ia32-msvc@14.3.0-canary.29': + '@next/swc-win32-ia32-msvc@14.3.0-canary.37': optional: true - '@next/swc-win32-x64-msvc@14.3.0-canary.29': + '@next/swc-win32-x64-msvc@14.3.0-canary.37': optional: true '@nodelib/fs.scandir@2.1.5': @@ -6195,11 +6232,11 @@ snapshots: dependencies: undici-types: 5.26.5 - '@types/pg@8.6.6': + '@types/pg@8.11.5': dependencies: '@types/node': 20.12.7 pg-protocol: 1.6.1 - pg-types: 2.2.0 + pg-types: 4.0.2 '@types/prop-types@15.7.12': {} @@ -6368,11 +6405,11 @@ snapshots: dependencies: crypto-js: 4.2.0 - '@vercel/analytics@1.2.2(next@14.3.0-canary.29(@babel/core@7.24.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)': + '@vercel/analytics@1.2.2(next@14.3.0-canary.37(@babel/core@7.24.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)': dependencies: server-only: 0.0.1 optionalDependencies: - next: 14.3.0-canary.29(@babel/core@7.24.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + next: 14.3.0-canary.37(@babel/core@7.24.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 '@vercel/blob@0.23.2': @@ -6386,16 +6423,9 @@ snapshots: dependencies: '@upstash/redis': 1.25.1 - '@vercel/postgres@0.8.0': - dependencies: - '@neondatabase/serverless': 0.7.2 - bufferutil: 4.0.8 - utf-8-validate: 6.0.3 - ws: 8.14.2(bufferutil@4.0.8)(utf-8-validate@6.0.3) - - '@vercel/speed-insights@1.0.10(next@14.3.0-canary.29(@babel/core@7.24.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)(svelte@4.2.15)(vue@3.4.25(typescript@5.4.5))': + '@vercel/speed-insights@1.0.10(next@14.3.0-canary.37(@babel/core@7.24.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)(svelte@4.2.15)(vue@3.4.25(typescript@5.4.5))': optionalDependencies: - next: 14.3.0-canary.29(@babel/core@7.24.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + next: 14.3.0-canary.37(@babel/core@7.24.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 svelte: 4.2.15 vue: 3.4.25(typescript@5.4.5) @@ -6747,6 +6777,7 @@ snapshots: bufferutil@4.0.8: dependencies: node-gyp-build: 4.8.0 + optional: true busboy@1.6.0: dependencies: @@ -8381,10 +8412,10 @@ snapshots: natural-compare@1.4.0: {} - next-auth@5.0.0-beta.15(next@14.3.0-canary.29(@babel/core@7.24.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1): + next-auth@5.0.0-beta.15(next@14.3.0-canary.37(@babel/core@7.24.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1): dependencies: '@auth/core': 0.28.0 - next: 14.3.0-canary.29(@babel/core@7.24.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + next: 14.3.0-canary.37(@babel/core@7.24.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 next-themes@0.3.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): @@ -8392,9 +8423,9 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - next@14.3.0-canary.29(@babel/core@7.24.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + next@14.3.0-canary.37(@babel/core@7.24.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: - '@next/env': 14.3.0-canary.29 + '@next/env': 14.3.0-canary.37 '@swc/helpers': 0.5.11 busboy: 1.6.0 caniuse-lite: 1.0.30001612 @@ -8404,15 +8435,15 @@ snapshots: react-dom: 18.3.1(react@18.3.1) styled-jsx: 5.1.1(@babel/core@7.24.4)(react@18.3.1) optionalDependencies: - '@next/swc-darwin-arm64': 14.3.0-canary.29 - '@next/swc-darwin-x64': 14.3.0-canary.29 - '@next/swc-linux-arm64-gnu': 14.3.0-canary.29 - '@next/swc-linux-arm64-musl': 14.3.0-canary.29 - '@next/swc-linux-x64-gnu': 14.3.0-canary.29 - '@next/swc-linux-x64-musl': 14.3.0-canary.29 - '@next/swc-win32-arm64-msvc': 14.3.0-canary.29 - '@next/swc-win32-ia32-msvc': 14.3.0-canary.29 - '@next/swc-win32-x64-msvc': 14.3.0-canary.29 + '@next/swc-darwin-arm64': 14.3.0-canary.37 + '@next/swc-darwin-x64': 14.3.0-canary.37 + '@next/swc-linux-arm64-gnu': 14.3.0-canary.37 + '@next/swc-linux-arm64-musl': 14.3.0-canary.37 + '@next/swc-linux-x64-gnu': 14.3.0-canary.37 + '@next/swc-linux-x64-musl': 14.3.0-canary.37 + '@next/swc-win32-arm64-msvc': 14.3.0-canary.37 + '@next/swc-win32-ia32-msvc': 14.3.0-canary.37 + '@next/swc-win32-x64-msvc': 14.3.0-canary.37 sharp: 0.33.3 transitivePeerDependencies: - '@babel/core' @@ -8424,7 +8455,8 @@ snapshots: dependencies: whatwg-url: 5.0.0 - node-gyp-build@4.8.0: {} + node-gyp-build@4.8.0: + optional: true node-int64@0.4.0: {} @@ -8488,6 +8520,8 @@ snapshots: define-properties: 1.2.1 es-object-atoms: 1.0.0 + obuf@1.1.2: {} + once@1.4.0: dependencies: wrappy: 1.0.2 @@ -8574,8 +8608,19 @@ snapshots: estree-walker: 3.0.3 is-reference: 3.0.2 + pg-cloudflare@1.1.1: + optional: true + + pg-connection-string@2.6.4: {} + pg-int8@1.0.1: {} + pg-numeric@1.0.2: {} + + pg-pool@3.6.2(pg@8.11.5): + dependencies: + pg: 8.11.5 + pg-protocol@1.6.1: {} pg-types@2.2.0: @@ -8586,6 +8631,30 @@ snapshots: postgres-date: 1.0.7 postgres-interval: 1.2.0 + pg-types@4.0.2: + dependencies: + pg-int8: 1.0.1 + pg-numeric: 1.0.2 + postgres-array: 3.0.2 + postgres-bytea: 3.0.0 + postgres-date: 2.1.0 + postgres-interval: 3.0.0 + postgres-range: 1.1.4 + + pg@8.11.5: + dependencies: + pg-connection-string: 2.6.4 + pg-pool: 3.6.2(pg@8.11.5) + pg-protocol: 1.6.1 + pg-types: 2.2.0 + pgpass: 1.0.5 + optionalDependencies: + pg-cloudflare: 1.1.1 + + pgpass@1.0.5: + dependencies: + split2: 4.2.0 + picocolors@1.0.0: {} picomatch@2.3.1: {} @@ -8645,14 +8714,26 @@ snapshots: postgres-array@2.0.0: {} + postgres-array@3.0.2: {} + postgres-bytea@1.0.0: {} + postgres-bytea@3.0.0: + dependencies: + obuf: 1.1.2 + postgres-date@1.0.7: {} + postgres-date@2.1.0: {} + postgres-interval@1.2.0: dependencies: xtend: 4.0.2 + postgres-interval@3.0.0: {} + + postgres-range@1.1.4: {} + preact-render-to-string@5.2.3(preact@10.11.3): dependencies: preact: 10.11.3 @@ -8960,6 +9041,8 @@ snapshots: source-map@0.6.1: {} + split2@4.2.0: {} + sprintf-js@1.0.3: {} sswr@2.0.0(svelte@4.2.15): @@ -9308,6 +9391,7 @@ snapshots: utf-8-validate@6.0.3: dependencies: node-gyp-build: 4.8.0 + optional: true util-deprecate@1.0.2: {} @@ -9448,11 +9532,6 @@ snapshots: bufferutil: 4.0.8 utf-8-validate: 6.0.3 - ws@8.14.2(bufferutil@4.0.8)(utf-8-validate@6.0.3): - optionalDependencies: - bufferutil: 4.0.8 - utf-8-validate: 6.0.3 - ws@8.16.0(bufferutil@4.0.8)(utf-8-validate@6.0.3): optionalDependencies: bufferutil: 4.0.8 diff --git a/src/app/admin/photos/page.tsx b/src/app/admin/photos/page.tsx index 41de1c79..9cdd2f29 100644 --- a/src/app/admin/photos/page.tsx +++ b/src/app/admin/photos/page.tsx @@ -5,7 +5,7 @@ import { getPhotosCountIncludingHiddenCached } from '@/photo/cache'; import StorageUrls from '@/admin/StorageUrls'; import { PRO_MODE_ENABLED } from '@/site/config'; import { getStoragePhotoUrlsNoStore } from '@/services/storage/cache'; -import { getPhotos } from '@/services/vercel-postgres'; +import { getPhotos } from '@/photo/db'; import { revalidatePath } from 'next/cache'; import AdminPhotoTable from '@/admin/AdminPhotoTable'; import AdminPhotoTableInfinite from diff --git a/src/app/admin/tags/[tag]/edit/page.tsx b/src/app/admin/tags/[tag]/edit/page.tsx index f6838a0c..7ba178ed 100644 --- a/src/app/admin/tags/[tag]/edit/page.tsx +++ b/src/app/admin/tags/[tag]/edit/page.tsx @@ -4,7 +4,7 @@ import { getPhotosCached } from '@/photo/cache'; import TagForm from '@/tag/TagForm'; import { PATH_ADMIN, PATH_ADMIN_TAGS, pathForTag } from '@/site/paths'; import PhotoLightbox from '@/photo/PhotoLightbox'; -import { getPhotosTagMeta } from '@/services/vercel-postgres'; +import { getPhotosTagMeta } from '@/photo/db'; import AdminTagBadge from '@/admin/AdminTagBadge'; const MAX_PHOTO_TO_SHOW = 6; diff --git a/src/app/grid/page.tsx b/src/app/grid/page.tsx index ef047ca3..d1160a63 100644 --- a/src/app/grid/page.tsx +++ b/src/app/grid/page.tsx @@ -10,7 +10,7 @@ import { MAX_PHOTOS_TO_SHOW_OG } from '@/image-response'; import { Metadata } from 'next/types'; import PhotoGridSidebar from '@/photo/PhotoGridSidebar'; import { getPhotoSidebarData } from '@/photo/data'; -import { getPhotos } from '@/services/vercel-postgres'; +import { getPhotos } from '@/photo/db'; import { cache } from 'react'; import PhotoGridInfinite from '@/photo/PhotoGridInfinite'; diff --git a/src/app/page.tsx b/src/app/page.tsx index 2104f14d..973f6263 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -8,7 +8,7 @@ import { Metadata } from 'next/types'; import { MAX_PHOTOS_TO_SHOW_OG } from '@/image-response'; import PhotosLarge from '@/photo/PhotosLarge'; import { cache } from 'react'; -import { getPhotos, getPhotosCount } from '@/services/vercel-postgres'; +import { getPhotos, getPhotosCount } from '@/photo/db'; import PhotosLargeInfinite from '@/photo/PhotosLargeInfinite'; export const dynamic = 'force-static'; diff --git a/src/photo/actions.ts b/src/photo/actions.ts index 030b4e22..21dcc7f4 100644 --- a/src/photo/actions.ts +++ b/src/photo/actions.ts @@ -8,7 +8,7 @@ import { sqlRenamePhotoTagGlobally, getPhoto, getPhotos, -} from '@/services/vercel-postgres'; +} from '@/photo/db'; import { PhotoFormData, convertFormDataToPhotoDbInsert, diff --git a/src/photo/cache.ts b/src/photo/cache.ts index 93f63d8a..5305b8c5 100644 --- a/src/photo/cache.ts +++ b/src/photo/cache.ts @@ -20,7 +20,7 @@ import { getPhotosDateRange, getPhotosNearId, getPhotosMostRecentUpdate, -} from '@/services/vercel-postgres'; +} from '@/photo/db'; import { parseCachedPhotoDates, parseCachedPhotosDates } from '@/photo'; import { createCameraKey } from '@/camera'; import { diff --git a/src/photo/data.ts b/src/photo/data.ts index e99b4a61..884284c0 100644 --- a/src/photo/data.ts +++ b/src/photo/data.ts @@ -9,7 +9,7 @@ import { getUniqueCameras, getUniqueFilmSimulations, getUniqueTags, -} from '@/services/vercel-postgres'; +} from '@/photo/db'; import { SHOW_FILM_SIMULATIONS } from '@/site/config'; import { sortTagsObject } from '@/tag'; diff --git a/src/services/vercel-postgres.ts b/src/photo/db.ts similarity index 98% rename from src/services/vercel-postgres.ts rename to src/photo/db.ts index fa4d1778..49e4ffb4 100644 --- a/src/services/vercel-postgres.ts +++ b/src/photo/db.ts @@ -1,4 +1,8 @@ -import { db, sql } from '@vercel/postgres'; +import { + sql, + query, + convertArrayToPostgresString, +} from '@/services/postgres'; import { PhotoDb, PhotoDbInsert, @@ -16,10 +20,6 @@ import { screenForPPR } from '@/utility/ppr'; const PHOTO_DEFAULT_LIMIT = 100; -export const convertArrayToPostgresString = (array?: string[]) => array - ? `{${array.join(',')}}` - : null; - const sqlCreatePhotosTable = () => sql` CREATE TABLE IF NOT EXISTS photos ( @@ -353,7 +353,7 @@ export const getPhotos = async (options: GetPhotosOptions = {}) => { sortBy = PRIORITY_ORDER_ENABLED ? 'priority' : 'takenAt', limit = PHOTO_DEFAULT_LIMIT, offset = 0, - query, + query: queryOption, tag, camera, simulation, @@ -379,10 +379,10 @@ export const getPhotos = async (options: GetPhotosOptions = {}) => { wheres.push(`taken_at <= $${valueIndex++}`); values.push(takenAfterInclusive.toISOString()); } - if (query) { + if (queryOption) { // eslint-disable-next-line max-len wheres.push(`CONCAT(title, ' ', caption, ' ', semantic_description) ILIKE $${valueIndex++}`); - values.push(`%${query.toLocaleLowerCase()}%`); + values.push(`%${queryOption.toLocaleLowerCase()}%`); } if (tag) { wheres.push(`$${valueIndex++}=ANY(tags)`); @@ -420,7 +420,7 @@ export const getPhotos = async (options: GetPhotosOptions = {}) => { values.push(limit, offset); return safelyQueryPhotos(async () => { - return db.query(sql.join(' '), values); + return query(sql.join(' '), values); }, sql.join(' ')) .then(({ rows }) => rows.map(parsePhotoFromDb)); }; @@ -434,7 +434,7 @@ export const getPhotosNearId = async ( : 'ORDER BY taken_at DESC'; return safelyQueryPhotos(async () => { - return db.query( + return query( ` WITH twi AS ( SELECT *, row_number() diff --git a/src/services/postgres.ts b/src/services/postgres.ts new file mode 100644 index 00000000..c6442e92 --- /dev/null +++ b/src/services/postgres.ts @@ -0,0 +1,54 @@ +import { POSTGRES_SSL_ENABLED } from '@/site/config'; +import { Pool, QueryResult, QueryResultRow } from 'pg'; + +const pool = new Pool({ + connectionString: process.env.POSTGRES_URL, + ...POSTGRES_SSL_ENABLED && { ssl: true }, +}); + +export type Primitive = string | number | boolean | undefined | null; + +export const query = async ( + queryString: string, + values: Primitive[], +) => { + const client = await pool.connect(); + let response: QueryResult; + try { + response = await client.query(queryString, values); + } catch (error) { + throw error; + } finally { + client.release(); + } + return response; +}; + +export const sql = ( + strings: TemplateStringsArray, + ...values: Primitive[] +) => { + if (!isTemplateStringsArray(strings) || !Array.isArray(values)) { + throw new Error('Invalid template literal argument'); + } + + let result = strings[0] ?? ''; + + for (let i = 1; i < strings.length; i++) { + result += `$${i}${strings[i] ?? ''}`; + } + + return query(result, values); +}; + +export const convertArrayToPostgresString = (array?: string[]) => array + ? `{${array.join(',')}}` + : null; + +const isTemplateStringsArray = ( + strings: unknown, +): strings is TemplateStringsArray => { + return ( + Array.isArray(strings) && 'raw' in strings && Array.isArray(strings.raw) + ); +}; diff --git a/src/site/SiteChecklistClient.tsx b/src/site/SiteChecklistClient.tsx index fea161ea..c62075c2 100644 --- a/src/site/SiteChecklistClient.tsx +++ b/src/site/SiteChecklistClient.tsx @@ -23,6 +23,8 @@ import { labelForStorage } from '@/services/storage'; import { HiSparkles } from 'react-icons/hi'; export default function SiteChecklistClient({ + hasDatabase, + isPostgresSSLEnabled, hasVercelPostgres, hasVercelKV, hasStorageProvider, @@ -148,16 +150,28 @@ export default function SiteChecklistClient({ > - {renderLink( - // eslint-disable-next-line max-len - 'https://vercel.com/docs/storage/vercel-postgres/quickstart#create-a-postgres-database', - 'Create Vercel Postgres store', - )} - {' '} - and connect to project + {hasVercelPostgres + ? renderSubStatus('checked', 'Vercel Postgres: connected') + : renderSubStatus('optional', <> + Vercel Postgres: + {' '} + {renderLink( + // eslint-disable-next-line max-len + 'https://vercel.com/docs/storage/vercel-postgres/quickstart#create-a-postgres-database', + 'create store', + )} + {' '} + and connect to project + )} + {hasDatabase && !hasVercelPostgres && + renderSubStatus('checked', <> + Postgres-compatible: connected + {' '} + (SSL {isPostgresSSLEnabled ? 'enabled' : 'disabled'}) + )} - {renderSubStatus( - hasVercelBlobStorage ? 'checked' : 'optional', - <> + {hasVercelBlobStorage + ? renderSubStatus('checked', 'Vercel Blob: connected') + : renderSubStatus('optional', <> {labelForStorage('vercel-blob')}: {' '} {renderLink( @@ -181,30 +195,28 @@ export default function SiteChecklistClient({ )} {' '} and connect to project - , - )} - {renderSubStatus( - hasCloudflareR2Storage ? 'checked' : 'optional', - <> + + )} + {hasCloudflareR2Storage + ? renderSubStatus('checked', 'Cloudflare R2: connected') + : renderSubStatus('optional', <> {labelForStorage('cloudflare-r2')}: {' '} {renderLink( 'https://github.com/sambecker/exif-photo-blog#cloudflare-r2', 'create/configure bucket', )} - - )} - {renderSubStatus( - hasAwsS3Storage ? 'checked' : 'optional', - <> + )} + {hasAwsS3Storage + ? renderSubStatus('checked', 'AWS S3: connected') + : renderSubStatus('optional', <> {labelForStorage('aws-s3')}: {' '} {renderLink( 'https://github.com/sambecker/exif-photo-blog#aws-s3', 'create/configure bucket', )} - - )} + )} 0; - +// STORAGE: DATABASE +export const HAS_DATABASE = + (process.env.POSTGRES_URL ?? '').length > 0; +export const POSTGRES_SSL_ENABLED = + process.env.DISABLE_POSTGRES_SSL === '1' ? false : true; // STORAGE: VERCEL KV export const HAS_VERCEL_KV = (process.env.KV_URL ?? '').length > 0; @@ -134,7 +135,10 @@ export const ADMIN_DEBUG_TOOLS_ENABLED = process.env.ADMIN_DEBUG_TOOLS === '1'; export const HIGH_DENSITY_GRID = GRID_ASPECT_RATIO <= 1; export const CONFIG_CHECKLIST_STATUS = { - hasVercelPostgres: HAS_VERCEL_POSTGRES, + hasDatabase: HAS_DATABASE, + isPostgresSSLEnabled: POSTGRES_SSL_ENABLED, + hasVercelPostgres: /\.vercel-storage\.com\// + .test(process.env.POSTGRES_URL ?? ''), hasVercelKV: HAS_VERCEL_KV, hasVercelBlobStorage: HAS_VERCEL_BLOB_STORAGE, hasCloudflareR2Storage: HAS_CLOUDFLARE_R2_STORAGE, @@ -177,7 +181,7 @@ export const CONFIG_CHECKLIST_STATUS = { export type ConfigChecklistStatus = typeof CONFIG_CHECKLIST_STATUS; export const IS_SITE_READY = - CONFIG_CHECKLIST_STATUS.hasVercelPostgres && + CONFIG_CHECKLIST_STATUS.hasDatabase && CONFIG_CHECKLIST_STATUS.hasStorageProvider && CONFIG_CHECKLIST_STATUS.hasAuthSecret && CONFIG_CHECKLIST_STATUS.hasAdminUser;