Adding Wasabi
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}`;
|
||||
};
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user