fixed db scripts

This commit is contained in:
blaisadmin
2026-05-02 10:24:58 -04:00
parent 1bb3002baf
commit 22f344a998
8 changed files with 409 additions and 14 deletions
+165 -4
View File
@@ -60,6 +60,13 @@ import {
updateVetVisitForBird,
} from './repositories/birdRepository.js';
import { createIntegrationTokenRecord, listIntegrationTokens, revokeIntegrationToken } from './repositories/integrationTokenRepository.js';
import {
buildBirdPhotoObjectKey,
getImageExtensionFromContentType,
getImageStorageProvider,
getS3ImageStorageConfig,
} from './storage/imageStorageConfig.js';
import { deleteS3Object, getSignedS3ObjectUrl, putS3Object } from './storage/s3Client.js';
import {
cancelRescueVerificationRequest,
claimWorkspaceInvites,
@@ -160,6 +167,7 @@ const photoDataUrlSchema = z
.string()
.regex(/^data:image\/(?:png|jpeg|jpg|webp|gif);base64,[A-Za-z0-9+/=]+$/)
.max(1_500_000);
const photoUrlSchema = z.string().trim().url().max(2000);
const magicLinkRequestSchema = z.object({
name: z.string().trim().max(160).optional().or(z.literal('')),
@@ -230,7 +238,7 @@ const birdSchema = z.object({
dateOfBirth: dateStringSchema.optional().or(z.literal('')),
gotchaDay: dateStringSchema.optional().or(z.literal('')),
chartColor: chartColorSchema.optional(),
photoDataUrl: photoDataUrlSchema.optional().or(z.literal('')),
photoDataUrl: z.union([photoDataUrlSchema, photoUrlSchema, z.literal('')]).optional(),
notifyOnDob: z.boolean().optional(),
notifyOnGotchaDay: z.boolean().optional(),
});
@@ -471,6 +479,24 @@ const normalizeWorkspaceMember = (row: WorkspaceMemberRow) => ({
createdAt: row.created_at,
});
const getBirdPhotoUrl = (row: BirdRow) => {
if (!row.photo_object_key) {
return row.photo_data_url;
}
const s3Config = getS3ImageStorageConfig();
if (!s3Config) {
return row.photo_data_url;
}
return getSignedS3ObjectUrl({
config: s3Config,
objectKey: row.photo_object_key,
expiresInSeconds: 15 * 60,
});
};
const normalizeBird = (row: BirdRow) => ({
id: row.id,
workspaceId: row.workspace_id,
@@ -481,7 +507,7 @@ const normalizeBird = (row: BirdRow) => ({
dateOfBirth: row.date_of_birth,
gotchaDay: row.gotcha_day,
chartColor: row.chart_color,
photoDataUrl: row.photo_data_url,
photoDataUrl: getBirdPhotoUrl(row),
photoObjectKey: row.photo_object_key,
photoContentType: row.photo_content_type,
photoUpdatedAt: row.photo_updated_at,
@@ -1021,6 +1047,107 @@ const parseDataImage = (dataUrl: string) => {
};
};
const isDataImageUrl = (value: string | null | undefined) => Boolean(value && value.startsWith('data:image/'));
const resolveBirdPhotoStorage = async ({
birdId,
workspaceId,
photoDataUrl,
existingBird,
}: {
birdId: string;
workspaceId: number;
photoDataUrl: string | null;
existingBird?: BirdRow | null;
}) => {
if (!photoDataUrl) {
return {
photoDataUrl: null,
photoObjectKey: null,
photoContentType: null,
photoUpdatedAt: null,
objectKeyToDelete: existingBird?.photo_object_key ?? null,
};
}
if (!isDataImageUrl(photoDataUrl)) {
if (existingBird?.photo_object_key) {
return {
photoDataUrl: null,
photoObjectKey: existingBird.photo_object_key,
photoContentType: existingBird.photo_content_type,
photoUpdatedAt: existingBird.photo_updated_at,
objectKeyToDelete: null,
};
}
return {
photoDataUrl,
photoObjectKey: null,
photoContentType: null,
photoUpdatedAt: null,
objectKeyToDelete: existingBird?.photo_object_key ?? null,
};
}
const parsedImage = parseDataImage(photoDataUrl);
if (!parsedImage) {
throw new Error('Unable to process bird photo.');
}
if (getImageStorageProvider() !== 's3') {
return {
photoDataUrl,
photoObjectKey: null,
photoContentType: null,
photoUpdatedAt: null,
objectKeyToDelete: existingBird?.photo_object_key ?? null,
};
}
const s3Config = getS3ImageStorageConfig();
if (!s3Config) {
throw new Error('S3 image storage is enabled but not fully configured.');
}
const extension = getImageExtensionFromContentType(parsedImage.contentType);
const objectKey = buildBirdPhotoObjectKey({ workspaceId, birdId, extension });
await putS3Object({
config: s3Config,
objectKey,
content: parsedImage.content,
contentType: parsedImage.contentType,
});
return {
photoDataUrl: null,
photoObjectKey: objectKey,
photoContentType: parsedImage.contentType,
photoUpdatedAt: new Date().toISOString(),
objectKeyToDelete: existingBird?.photo_object_key ?? null,
};
};
const deleteBirdPhotoObjectIfNeeded = async (objectKey: string | null) => {
if (!objectKey) {
return;
}
const s3Config = getS3ImageStorageConfig();
if (!s3Config) {
return;
}
try {
await deleteS3Object({ config: s3Config, objectKey });
} catch (error) {
console.warn(`Unable to delete old bird photo object ${objectKey}:`, error);
}
};
const getDefaultBirdPhotoAttachment = () => {
const defaultPhotoPath = path.join(process.cwd(), 'assets', 'yoda-default.png');
@@ -2558,8 +2685,18 @@ app.post('/api/birds', requireAuth, requireWriteAccess, requireWorkspaceRole(['o
return;
}
let uploadedObjectKeyToCleanup: string | null = null;
try {
const birdId = crypto.randomUUID();
const photoStorage = await resolveBirdPhotoStorage({
birdId,
workspaceId: req.auth!.workspace.id,
photoDataUrl: emptyToNull(parsed.data.photoDataUrl),
});
uploadedObjectKeyToCleanup = photoStorage.photoObjectKey;
const bird = await createBird({
birdId,
workspaceId: req.auth!.workspace.id,
name: parsed.data.name,
tagId: normalizeBandId(parsed.data.tagId),
@@ -2568,13 +2705,19 @@ app.post('/api/birds', requireAuth, requireWriteAccess, requireWorkspaceRole(['o
dateOfBirth: emptyToNull(parsed.data.dateOfBirth),
gotchaDay: emptyToNull(parsed.data.gotchaDay),
chartColor: parsed.data.chartColor ?? '#cb3a35',
photoDataUrl: emptyToNull(parsed.data.photoDataUrl),
photoDataUrl: photoStorage.photoDataUrl,
photoObjectKey: photoStorage.photoObjectKey,
photoContentType: photoStorage.photoContentType,
photoUpdatedAt: photoStorage.photoUpdatedAt,
notifyOnDob: parsed.data.notifyOnDob ?? false,
notifyOnGotchaDay: parsed.data.notifyOnGotchaDay ?? false,
});
uploadedObjectKeyToCleanup = null;
res.status(201).json({ bird: normalizeBird(bird!) });
} catch (error) {
await deleteBirdPhotoObjectIfNeeded(uploadedObjectKeyToCleanup);
if (typeof error === 'object' && error && 'code' in error && error.code === '23505') {
res.status(409).json({ error: 'That band/tag ID is already in use in this flock.' });
return;
@@ -2667,6 +2810,8 @@ app.put('/api/birds/:birdId', requireAuth, requireWriteAccess, requireWorkspaceR
return;
}
let uploadedObjectKeyToCleanup: string | null = null;
try {
const existingBird = await getBirdById(req.params.birdId, req.auth!.workspace.id);
@@ -2679,6 +2824,14 @@ app.put('/api/birds/:birdId', requireAuth, requireWriteAccess, requireWorkspaceR
return;
}
const photoStorage = await resolveBirdPhotoStorage({
birdId: req.params.birdId,
workspaceId: req.auth!.workspace.id,
photoDataUrl: emptyToNull(parsed.data.photoDataUrl),
existingBird,
});
uploadedObjectKeyToCleanup =
photoStorage.photoObjectKey && photoStorage.photoObjectKey !== existingBird.photo_object_key ? photoStorage.photoObjectKey : null;
const bird = await updateBird({
birdId: req.params.birdId,
workspaceId: req.auth!.workspace.id,
@@ -2689,7 +2842,10 @@ app.put('/api/birds/:birdId', requireAuth, requireWriteAccess, requireWorkspaceR
dateOfBirth: emptyToNull(parsed.data.dateOfBirth),
gotchaDay: emptyToNull(parsed.data.gotchaDay),
chartColor: parsed.data.chartColor ?? '#cb3a35',
photoDataUrl: emptyToNull(parsed.data.photoDataUrl),
photoDataUrl: photoStorage.photoDataUrl,
photoObjectKey: photoStorage.photoObjectKey,
photoContentType: photoStorage.photoContentType,
photoUpdatedAt: photoStorage.photoUpdatedAt,
notifyOnDob: parsed.data.notifyOnDob ?? false,
notifyOnGotchaDay: parsed.data.notifyOnGotchaDay ?? false,
});
@@ -2699,8 +2855,12 @@ app.put('/api/birds/:birdId', requireAuth, requireWriteAccess, requireWorkspaceR
return;
}
uploadedObjectKeyToCleanup = null;
await deleteBirdPhotoObjectIfNeeded(photoStorage.objectKeyToDelete);
res.json({ bird: normalizeBird(bird) });
} catch (error) {
await deleteBirdPhotoObjectIfNeeded(uploadedObjectKeyToCleanup);
if (typeof error === 'object' && error && 'code' in error && error.code === '23505') {
res.status(409).json({ error: 'That band/tag ID is already in use in this flock.' });
return;
@@ -2731,6 +2891,7 @@ app.delete('/api/birds/:birdId', requireAuth, requireWriteAccess, requireWorkspa
}
res.status(204).send();
await deleteBirdPhotoObjectIfNeeded(bird.photo_object_key);
} catch (error) {
next(error);
}