Add recipe to db model, refactor migrations

This commit is contained in:
Sam Becker 2025-02-19 18:12:01 -06:00
parent 64a49c85a3
commit faad28e6f7
5 changed files with 68 additions and 35 deletions

40
src/photo/db/migration.ts Normal file
View File

@ -0,0 +1,40 @@
import { sql } from '@/platforms/postgres';
interface Migration {
label: string
fields: string[]
run: () => ReturnType<typeof sql>
}
export const MIGRATIONS: Migration[] = [{
label: '01: AI Text Generation',
fields: ['caption', 'semantic_description'],
run: () => sql`
ALTER TABLE photos
ADD COLUMN IF NOT EXISTS caption TEXT,
ADD COLUMN IF NOT EXISTS semantic_description TEXT
`,
}, {
label: '02: Lens Metadata',
fields: ['lens_make', 'lens_model'],
run: () => sql`
ALTER TABLE photos
ADD COLUMN IF NOT EXISTS lens_make VARCHAR(255),
ADD COLUMN IF NOT EXISTS lens_model VARCHAR(255)
`,
}, {
label: '03: Fujifilm Recipe',
fields: ['fujifilm_recipe'],
run: () => sql`
ALTER TABLE photos
ADD COLUMN IF NOT EXISTS fujifilm_recipe JSONB
`,
}];
export const migrationForError = (e: any) =>
MIGRATIONS.find(migration =>
migration.fields.some(field =>
new RegExp(`column "${field}" of relation "photos" does not exist`, 'i')
.test(e.message),
),
);

View File

@ -23,6 +23,7 @@ import {
import { getWheresFromOptions } from '.'; import { getWheresFromOptions } from '.';
import { FocalLengths } from '@/focal'; import { FocalLengths } from '@/focal';
import { Lenses, createLensKey } from '@/lens'; import { Lenses, createLensKey } from '@/lens';
import { migrationForError } from './migration';
const createPhotosTable = () => const createPhotosTable = () =>
sql` sql`
@ -50,6 +51,7 @@ const createPhotosTable = () =>
latitude DOUBLE PRECISION, latitude DOUBLE PRECISION,
longitude DOUBLE PRECISION, longitude DOUBLE PRECISION,
film_simulation VARCHAR(255), film_simulation VARCHAR(255),
fujifilm_recipe JSONB,
priority_order REAL, priority_order REAL,
taken_at TIMESTAMP WITH TIME ZONE NOT NULL, taken_at TIMESTAMP WITH TIME ZONE NOT NULL,
taken_at_naive VARCHAR(255) NOT NULL, taken_at_naive VARCHAR(255) NOT NULL,
@ -59,24 +61,6 @@ const createPhotosTable = () =>
) )
`; `;
// Migration 01
const MIGRATION_FIELDS_01 = ['caption', 'semantic_description'];
const runMigration01 = () =>
sql`
ALTER TABLE photos
ADD COLUMN IF NOT EXISTS caption TEXT,
ADD COLUMN IF NOT EXISTS semantic_description TEXT
`;
// Migration 02
const MIGRATION_FIELDS_02 = ['lens_make', 'lens_model'];
const runMigration02 = () =>
sql`
ALTER TABLE photos
ADD COLUMN IF NOT EXISTS lens_make VARCHAR(255),
ADD COLUMN IF NOT EXISTS lens_model VARCHAR(255)
`;
// Wrapper for most queries for JIT table creation/migration running // Wrapper for most queries for JIT table creation/migration running
const safelyQueryPhotos = async <T>( const safelyQueryPhotos = async <T>(
callback: () => Promise<T>, callback: () => Promise<T>,
@ -89,19 +73,10 @@ const safelyQueryPhotos = async <T>(
try { try {
result = await callback(); result = await callback();
} catch (e: any) { } catch (e: any) {
if (MIGRATION_FIELDS_01.some(field => new RegExp( const migration = migrationForError(e);
`column "${field}" of relation "photos" does not exist`, if (migration) {
'i', console.log(`Running Migration ${migration.label} ...`);
).test(e.message))) { await migration.run();
console.log('Running migration 01 ...');
await runMigration01();
result = await callback();
} else if (MIGRATION_FIELDS_02.some(field => new RegExp(
`column "${field}" of relation "photos" does not exist`,
'i',
).test(e.message))) {
console.log('Running migration 02 ...');
await runMigration02();
result = await callback(); result = await callback();
} else if (/relation "photos" does not exist/i.test(e.message)) { } else if (/relation "photos" does not exist/i.test(e.message)) {
// If the table does not exist, create it // If the table does not exist, create it
@ -163,6 +138,7 @@ export const insertPhoto = (photo: PhotoDbInsert) =>
latitude, latitude,
longitude, longitude,
film_simulation, film_simulation,
fujifilm_recipe,
priority_order, priority_order,
hidden, hidden,
taken_at, taken_at,
@ -192,6 +168,7 @@ export const insertPhoto = (photo: PhotoDbInsert) =>
${photo.latitude}, ${photo.latitude},
${photo.longitude}, ${photo.longitude},
${photo.filmSimulation}, ${photo.filmSimulation},
${JSON.stringify(photo.fujifilmRecipe)},
${photo.priorityOrder}, ${photo.priorityOrder},
${photo.hidden}, ${photo.hidden},
${photo.takenAt}, ${photo.takenAt},
@ -224,6 +201,7 @@ export const updatePhoto = (photo: PhotoDbInsert) =>
latitude=${photo.latitude}, latitude=${photo.latitude},
longitude=${photo.longitude}, longitude=${photo.longitude},
film_simulation=${photo.filmSimulation}, film_simulation=${photo.filmSimulation},
fujifilm_recipe=${JSON.stringify(photo.fujifilmRecipe)},
priority_order=${photo.priorityOrder || null}, priority_order=${photo.priorityOrder || null},
hidden=${photo.hidden}, hidden=${photo.hidden},
taken_at=${photo.takenAt}, taken_at=${photo.takenAt},

View File

@ -23,6 +23,7 @@ import { FilmSimulation } from '@/simulation';
import { GEO_PRIVACY_ENABLED } from '@/app/config'; import { GEO_PRIVACY_ENABLED } from '@/app/config';
import { TAG_FAVS, getValidationMessageForTags } from '@/tag'; import { TAG_FAVS, getValidationMessageForTags } from '@/tag';
import { MAKE_FUJIFILM } from '@/platforms/fujifilm'; import { MAKE_FUJIFILM } from '@/platforms/fujifilm';
import { FujifilmRecipe } from '@/platforms/fujifilm/recipe';
type VirtualFields = 'favorite'; type VirtualFields = 'favorite';
@ -107,6 +108,11 @@ const FORM_METADATA = (
selectOptionsDefaultLabel: 'Unknown', selectOptionsDefaultLabel: 'Unknown',
shouldHide: ({ make }) => make !== MAKE_FUJIFILM, shouldHide: ({ make }) => make !== MAKE_FUJIFILM,
}, },
fujifilmRecipe: {
type: 'textarea',
label: 'fujifilm recipe',
shouldHide: ({ make }) => make !== MAKE_FUJIFILM,
},
focalLength: { label: 'focal length' }, focalLength: { label: 'focal length' },
focalLengthIn35MmFormat: { label: 'focal length 35mm-equivalent' }, focalLengthIn35MmFormat: { label: 'focal length 35mm-equivalent' },
lensMake: { label: 'lens make' }, lensMake: { label: 'lens make' },
@ -200,6 +206,7 @@ export const convertPhotoToFormData = (
export const convertExifToFormData = ( export const convertExifToFormData = (
data: ExifData, data: ExifData,
filmSimulation?: FilmSimulation, filmSimulation?: FilmSimulation,
fujifilmRecipe?: Partial<FujifilmRecipe>,
): Omit< ): Omit<
Record<keyof PhotoExif, string | undefined>, Record<keyof PhotoExif, string | undefined>,
'takenAt' | 'takenAtNaive' 'takenAt' | 'takenAtNaive'
@ -223,6 +230,7 @@ export const convertExifToFormData = (
longitude: longitude:
!GEO_PRIVACY_ENABLED ? data.tags?.GPSLongitude?.toString() : undefined, !GEO_PRIVACY_ENABLED ? data.tags?.GPSLongitude?.toString() : undefined,
filmSimulation, filmSimulation,
fujifilmRecipe: JSON.stringify(fujifilmRecipe),
...data.tags?.DateTimeOriginal && { ...data.tags?.DateTimeOriginal && {
takenAt: convertTimestampWithOffsetToPostgresString( takenAt: convertTimestampWithOffsetToPostgresString(
data.tags.DateTimeOriginal, data.tags.DateTimeOriginal,
@ -267,7 +275,10 @@ export const convertFormDataToPhotoDbInsert = (
}); });
return { return {
...(photoForm as PhotoFormData & { filmSimulation?: FilmSimulation }), ...(photoForm as PhotoFormData & {
filmSimulation?: FilmSimulation
fujifilmRecipe?: FujifilmRecipe
}),
...!photoForm.id && { id: generateNanoid() }, ...!photoForm.id && { id: generateNanoid() },
// Delete array field when empty // Delete array field when empty
tags: tags.length > 0 ? tags : undefined, tags: tags.length > 0 ? tags : undefined,

View File

@ -20,6 +20,7 @@ import { parameterize } from '@/utility/string';
import camelcaseKeys from 'camelcase-keys'; import camelcaseKeys from 'camelcase-keys';
import { isBefore } from 'date-fns'; import { isBefore } from 'date-fns';
import type { Metadata } from 'next'; import type { Metadata } from 'next';
import { FujifilmRecipe } from '@/platforms/fujifilm/recipe';
export const OUTDATED_THRESHOLD = new Date('2024-06-16'); export const OUTDATED_THRESHOLD = new Date('2024-06-16');
@ -66,6 +67,7 @@ export interface PhotoExif {
latitude?: number latitude?: number
longitude?: number longitude?: number
filmSimulation?: FilmSimulation filmSimulation?: FilmSimulation
fujifilmRecipe?: string
takenAt?: string takenAt?: string
takenAtNaive?: string takenAtNaive?: string
} }
@ -88,11 +90,13 @@ export interface PhotoDbInsert extends PhotoExif {
} }
// Raw db response // Raw db response
export interface PhotoDb extends Omit<PhotoDbInsert, 'takenAt' | 'tags'> { export interface PhotoDb extends
Omit<PhotoDbInsert, 'takenAt' | 'tags' | 'fujifilmRecipe'> {
updatedAt: Date updatedAt: Date
createdAt: Date createdAt: Date
takenAt: Date takenAt: Date
tags: string[] tags: string[]
fujifilmRecipe?: Partial<FujifilmRecipe>
} }
// Parsed db response // Parsed db response
@ -159,6 +163,7 @@ export const convertPhotoToPhotoDbInsert = (
): PhotoDbInsert => ({ ): PhotoDbInsert => ({
...photo, ...photo,
takenAt: photo.takenAt.toISOString(), takenAt: photo.takenAt.toISOString(),
fujifilmRecipe: JSON.stringify(photo.fujifilmRecipe),
}); });
export const photoStatsAsString = (photo: Photo) => [ export const photoStatsAsString = (photo: Photo) => [

View File

@ -83,7 +83,6 @@ export const extractImageDataFromBlobPath = async (
if (Buffer.isBuffer(makerNote)) { if (Buffer.isBuffer(makerNote)) {
filmSimulation = getFujifilmSimulationFromMakerNote(makerNote); filmSimulation = getFujifilmSimulationFromMakerNote(makerNote);
recipe = getFujifilmRecipeFromMakerNote(makerNote); recipe = getFujifilmRecipeFromMakerNote(makerNote);
console.log(recipe);
} }
} }
@ -117,7 +116,7 @@ export const extractImageDataFromBlobPath = async (
url, url,
}, },
...generateBlurData && { blurData }, ...generateBlurData && { blurData },
...convertExifToFormData(exifData, filmSimulation), ...convertExifToFormData(exifData, filmSimulation, recipe),
}, },
}, },
imageResizedBase64, imageResizedBase64,