Adding Wasabi

This commit is contained in:
blaisadmin
2026-05-02 01:17:43 -04:00
parent d2d130d960
commit 1bb3002baf
9 changed files with 159 additions and 5 deletions
+3
View File
@@ -482,6 +482,9 @@ const normalizeBird = (row: BirdRow) => ({
gotchaDay: row.gotcha_day,
chartColor: row.chart_color,
photoDataUrl: row.photo_data_url,
photoObjectKey: row.photo_object_key,
photoContentType: row.photo_content_type,
photoUpdatedAt: row.photo_updated_at,
notifyOnDob: row.notify_on_dob,
notifyOnGotchaDay: row.notify_on_gotcha_day,
memorializedAt: row.memorialized_at,
+10
View File
@@ -221,6 +221,9 @@ export const ensureSchema = async (database: DatabaseClient = db) => {
gotcha_day DATE,
chart_color VARCHAR(7) NOT NULL DEFAULT '#cb3a35',
photo_data_url TEXT,
photo_object_key TEXT,
photo_content_type VARCHAR(80),
photo_updated_at TIMESTAMPTZ,
notify_on_dob BOOLEAN NOT NULL DEFAULT FALSE,
notify_on_gotcha_day BOOLEAN NOT NULL DEFAULT FALSE,
memorialized_at TIMESTAMPTZ,
@@ -237,6 +240,9 @@ export const ensureSchema = async (database: DatabaseClient = db) => {
ADD COLUMN IF NOT EXISTS gotcha_day DATE,
ADD COLUMN IF NOT EXISTS chart_color VARCHAR(7) NOT NULL DEFAULT '#cb3a35',
ADD COLUMN IF NOT EXISTS photo_data_url TEXT,
ADD COLUMN IF NOT EXISTS photo_object_key TEXT,
ADD COLUMN IF NOT EXISTS photo_content_type VARCHAR(80),
ADD COLUMN IF NOT EXISTS photo_updated_at TIMESTAMPTZ,
ADD COLUMN IF NOT EXISTS notify_on_dob BOOLEAN NOT NULL DEFAULT FALSE,
ADD COLUMN IF NOT EXISTS notify_on_gotcha_day BOOLEAN NOT NULL DEFAULT FALSE,
ADD COLUMN IF NOT EXISTS memorialized_at TIMESTAMPTZ,
@@ -287,6 +293,10 @@ export const ensureSchema = async (database: DatabaseClient = db) => {
AND LOWER(BTRIM(tag_id)) NOT IN ('unknown', 'not recorded', 'n/a', 'na', 'none')
AND memorialized_at IS NULL;
CREATE INDEX IF NOT EXISTS idx_birds_photo_object_key
ON birds (photo_object_key)
WHERE photo_object_key IS NOT NULL;
CREATE TABLE IF NOT EXISTS pending_bird_transfers (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
bird_id UUID NOT NULL REFERENCES birds(id) ON DELETE CASCADE,
+8 -5
View File
@@ -25,6 +25,9 @@ const birdSelectFields = `
birds.gotcha_day::text,
birds.chart_color,
birds.photo_data_url,
birds.photo_object_key,
birds.photo_content_type,
birds.photo_updated_at,
birds.notify_on_dob,
birds.notify_on_gotcha_day,
birds.memorialized_at,
@@ -277,7 +280,7 @@ export const createBird = async ({
const result = await db.query<BirdRow>(
`INSERT INTO birds (workspace_id, name, tag_id, species, gender, date_of_birth, gotcha_day, chart_color, photo_data_url, notify_on_dob, notify_on_gotcha_day)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
RETURNING id, workspace_id, name, tag_id, species, gender, date_of_birth::text, gotcha_day::text, chart_color, photo_data_url, notify_on_dob, notify_on_gotcha_day, memorialized_at, memorialized_on::text, memorial_note, notify_on_memorial_day, created_at, NULL::text AS latest_weight_grams, NULL::text AS latest_recorded_on`,
RETURNING id, workspace_id, name, tag_id, species, gender, date_of_birth::text, gotcha_day::text, chart_color, photo_data_url, photo_object_key, photo_content_type, photo_updated_at, notify_on_dob, notify_on_gotcha_day, memorialized_at, memorialized_on::text, memorial_note, notify_on_memorial_day, created_at, NULL::text AS latest_weight_grams, NULL::text AS latest_recorded_on`,
[workspaceId, name, tagId, species, gender, dateOfBirth, gotchaDay, chartColor, photoDataUrl, notifyOnDob, notifyOnGotchaDay],
);
@@ -326,7 +329,7 @@ export const updateBird = async ({
WHERE id = $1
AND workspace_id = $12
AND memorialized_at IS NULL
RETURNING id, workspace_id, name, tag_id, species, gender, date_of_birth::text, gotcha_day::text, chart_color, photo_data_url, notify_on_dob, notify_on_gotcha_day, memorialized_at, memorialized_on::text, memorial_note, notify_on_memorial_day, created_at,
RETURNING id, workspace_id, name, tag_id, species, gender, date_of_birth::text, gotcha_day::text, chart_color, photo_data_url, photo_object_key, photo_content_type, photo_updated_at, notify_on_dob, notify_on_gotcha_day, memorialized_at, memorialized_on::text, memorial_note, notify_on_memorial_day, created_at,
(
SELECT weight_grams::text
FROM weight_records
@@ -369,7 +372,7 @@ export const memorializeBird = async ({
WHERE id = $1
AND workspace_id = $2
AND memorialized_at IS NULL
RETURNING id, workspace_id, name, tag_id, species, gender, date_of_birth::text, gotcha_day::text, chart_color, photo_data_url, notify_on_dob, notify_on_gotcha_day, memorialized_at, memorialized_on::text, memorial_note, notify_on_memorial_day, created_at,
RETURNING id, workspace_id, name, tag_id, species, gender, date_of_birth::text, gotcha_day::text, chart_color, photo_data_url, photo_object_key, photo_content_type, photo_updated_at, notify_on_dob, notify_on_gotcha_day, memorialized_at, memorialized_on::text, memorial_note, notify_on_memorial_day, created_at,
(
SELECT weight_grams::text
FROM weight_records
@@ -405,7 +408,7 @@ export const updateMemorialReminderPreference = async ({
WHERE id = $1
AND workspace_id = $2
AND memorialized_at IS NOT NULL
RETURNING id, workspace_id, name, tag_id, species, gender, date_of_birth::text, gotcha_day::text, chart_color, photo_data_url, notify_on_dob, notify_on_gotcha_day, memorialized_at, memorialized_on::text, memorial_note, notify_on_memorial_day, created_at,
RETURNING id, workspace_id, name, tag_id, species, gender, date_of_birth::text, gotcha_day::text, chart_color, photo_data_url, photo_object_key, photo_content_type, photo_updated_at, notify_on_dob, notify_on_gotcha_day, memorialized_at, memorialized_on::text, memorial_note, notify_on_memorial_day, created_at,
(
SELECT weight_grams::text
FROM weight_records
@@ -445,7 +448,7 @@ export const transferBirdToWorkspace = async (birdId: string, sourceWorkspaceId:
WHERE id = $1
AND workspace_id = $2
AND memorialized_at IS NULL
RETURNING id, workspace_id, name, tag_id, species, gender, date_of_birth::text, gotcha_day::text, chart_color, photo_data_url, notify_on_dob, notify_on_gotcha_day, memorialized_at, memorialized_on::text, memorial_note, notify_on_memorial_day, created_at,
RETURNING id, workspace_id, name, tag_id, species, gender, date_of_birth::text, gotcha_day::text, chart_color, photo_data_url, photo_object_key, photo_content_type, photo_updated_at, notify_on_dob, notify_on_gotcha_day, memorialized_at, memorialized_on::text, memorial_note, notify_on_memorial_day, created_at,
(
SELECT weight_grams::text
FROM weight_records
+67
View File
@@ -0,0 +1,67 @@
export type ImageStorageProvider = 'database' | 's3';
export type S3ImageStorageConfig = {
provider: 's3';
endpoint: string;
region: string;
bucket: string;
accessKeyId: string;
secretAccessKey: string;
publicBaseUrl: string | null;
keyPrefix: string;
};
const trimOptional = (value: string | undefined) => {
const trimmed = value?.trim();
return trimmed ? trimmed : null;
};
export const getImageStorageProvider = (): ImageStorageProvider =>
process.env.IMAGE_STORAGE_PROVIDER === 's3' ? 's3' : 'database';
export const getS3ImageStorageConfig = (): S3ImageStorageConfig | null => {
if (getImageStorageProvider() !== 's3') {
return null;
}
const endpoint = trimOptional(process.env.S3_ENDPOINT);
const region = trimOptional(process.env.S3_REGION);
const bucket = trimOptional(process.env.S3_BUCKET);
const accessKeyId = trimOptional(process.env.S3_ACCESS_KEY_ID);
const secretAccessKey = trimOptional(process.env.S3_SECRET_ACCESS_KEY);
if (!endpoint || !region || !bucket || !accessKeyId || !secretAccessKey) {
return null;
}
return {
provider: 's3',
endpoint,
region,
bucket,
accessKeyId,
secretAccessKey,
publicBaseUrl: trimOptional(process.env.S3_PUBLIC_BASE_URL),
keyPrefix: trimOptional(process.env.S3_KEY_PREFIX) ?? 'bird-photos',
};
};
export const isS3ImageStorageConfigured = () => getS3ImageStorageConfig() !== null;
export const buildBirdPhotoObjectKey = ({
workspaceId,
birdId,
extension,
now = new Date(),
}: {
workspaceId: number;
birdId: string;
extension: string;
now?: Date;
}) => {
const prefix = trimOptional(process.env.S3_KEY_PREFIX) ?? 'bird-photos';
const safeExtension = extension.replace(/^\./, '').toLowerCase() || 'bin';
const timestamp = now.toISOString().replace(/[:.]/g, '-');
return `${prefix}/workspace-${workspaceId}/${birdId}/${timestamp}.${safeExtension}`;
};
+3
View File
@@ -103,6 +103,9 @@ export type BirdRow = {
gotcha_day: string | null;
chart_color: string;
photo_data_url: string | null;
photo_object_key: string | null;
photo_content_type: string | null;
photo_updated_at: string | null;
notify_on_dob: boolean;
notify_on_gotcha_day: boolean;
memorialized_at: string | null;