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 { FocalLengths } from '@/focal';
import { Lenses, createLensKey } from '@/lens';
import { migrationForError } from './migration';
const createPhotosTable = () =>
sql`
@ -50,6 +51,7 @@ const createPhotosTable = () =>
latitude DOUBLE PRECISION,
longitude DOUBLE PRECISION,
film_simulation VARCHAR(255),
fujifilm_recipe JSONB,
priority_order REAL,
taken_at TIMESTAMP WITH TIME ZONE 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
const safelyQueryPhotos = async <T>(
callback: () => Promise<T>,
@ -89,19 +73,10 @@ const safelyQueryPhotos = async <T>(
try {
result = await callback();
} catch (e: any) {
if (MIGRATION_FIELDS_01.some(field => new RegExp(
`column "${field}" of relation "photos" does not exist`,
'i',
).test(e.message))) {
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();
const migration = migrationForError(e);
if (migration) {
console.log(`Running Migration ${migration.label} ...`);
await migration.run();
result = await callback();
} else if (/relation "photos" does not exist/i.test(e.message)) {
// If the table does not exist, create it
@ -163,6 +138,7 @@ export const insertPhoto = (photo: PhotoDbInsert) =>
latitude,
longitude,
film_simulation,
fujifilm_recipe,
priority_order,
hidden,
taken_at,
@ -192,6 +168,7 @@ export const insertPhoto = (photo: PhotoDbInsert) =>
${photo.latitude},
${photo.longitude},
${photo.filmSimulation},
${JSON.stringify(photo.fujifilmRecipe)},
${photo.priorityOrder},
${photo.hidden},
${photo.takenAt},
@ -224,6 +201,7 @@ export const updatePhoto = (photo: PhotoDbInsert) =>
latitude=${photo.latitude},
longitude=${photo.longitude},
film_simulation=${photo.filmSimulation},
fujifilm_recipe=${JSON.stringify(photo.fujifilmRecipe)},
priority_order=${photo.priorityOrder || null},
hidden=${photo.hidden},
taken_at=${photo.takenAt},

View File

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

View File

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

View File

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