diff --git a/package.json b/package.json index 0db7038e..862423db 100644 --- a/package.json +++ b/package.json @@ -28,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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1421db2f..322f3c15 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -65,9 +65,6 @@ importers: '@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.2.3(@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)) @@ -690,9 +687,6 @@ 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==} @@ -1363,9 +1357,6 @@ packages: '@types/pg@8.11.5': resolution: {integrity: sha512-2xMjVviMxneZHDHX5p5S6tsRRs7TpDHeeK7kTTMe/kAC/mRRNjWHjZg0rkiY+e17jXSZV3zJYDxXV8Cy72/Vuw==} - '@types/pg@8.6.6': - resolution: {integrity: sha512-O2xNmXebtwVekJDD+02udOncjVcMZQuTEQEMpKJ0ZRf5E7/9JJX3izhKUcUifBkyKpljyUM6BTgy2trmviKlpw==} - '@types/prop-types@15.7.12': resolution: {integrity: sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==} @@ -1514,10 +1505,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: @@ -4174,18 +4161,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'} @@ -5257,10 +5232,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) @@ -6056,12 +6027,6 @@ snapshots: pg-protocol: 1.6.1 pg-types: 4.0.2 - '@types/pg@8.6.6': - dependencies: - '@types/node': 20.12.7 - pg-protocol: 1.6.1 - pg-types: 2.2.0 - '@types/prop-types@15.7.12': {} '@types/react-dom@18.3.0': @@ -6247,13 +6212,6 @@ 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.2.3(@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.2.3(@babel/core@7.24.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -6608,6 +6566,7 @@ snapshots: bufferutil@4.0.8: dependencies: node-gyp-build: 4.8.0 + optional: true busboy@1.6.0: dependencies: @@ -8266,7 +8225,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: {} @@ -9169,6 +9129,7 @@ snapshots: utf-8-validate@6.0.3: dependencies: node-gyp-build: 4.8.0 + optional: true util-deprecate@1.0.2: {} @@ -9309,11 +9270,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..b4952e14 100644 --- a/src/services/vercel-postgres.ts +++ b/src/photo/db.ts @@ -1,4 +1,8 @@ -import { db, sql } from '@vercel/postgres'; +import { + sql, + directQuery, + 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 ( @@ -420,7 +420,7 @@ export const getPhotos = async (options: GetPhotosOptions = {}) => { values.push(limit, offset); return safelyQueryPhotos(async () => { - return db.query(sql.join(' '), values); + return directQuery(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 directQuery( ` WITH twi AS ( SELECT *, row_number() diff --git a/src/services/postgres.ts b/src/services/postgres.ts new file mode 100644 index 00000000..5e5df419 --- /dev/null +++ b/src/services/postgres.ts @@ -0,0 +1,46 @@ +import { Client, QueryResultRow } from 'pg'; + +export type Primitive = string | number | boolean | undefined | null; + +export const directQuery = async ( + queryString: string, + values: Primitive[], +) => { + const client = new Client({ + connectionString: process.env.POSTGRES_URL, + ssl: true, + }); + await client.connect(); + const response = await client.query(queryString, values); + await client.end(); + 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 directQuery(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) + ); +};