feat(api/v1): accept category_id on POST /events/:id/photos
The v1 photo upload endpoint previously ignored any caller-supplied category and inserted photos with category_id=NULL. That meant programmatic uploads via API tokens (e.g. a photobox sidecar) landed in picpeak as uncategorized, forcing operators to bulk-assign category in the admin UI after each event. Mirror the adminPhotos.js category-handling logic on v1: - Read optional `category_id` from the multipart form body. - Reject unknown ids with 400 (with the id in the error) so callers fail fast on misconfigured envs instead of silently uncategorized uploads. - Set photos.category_id on insert. - Flip photos.type to 'collage' when the category's slug is collage/collages, matching adminPhotos. Backwards-compatible: omitting category_id keeps the prior behavior (insert with NULL category, type='individual'). OpenAPI spec + 201 response body updated to include the new field. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
2619049a95
commit
6901e2661e
@@ -338,6 +338,13 @@ router.get('/events/:id', apiTokenAuth, requireApiScope('read'), async (req, res
|
||||
* required: [photo]
|
||||
* properties:
|
||||
* photo: { type: string, format: binary }
|
||||
* category_id:
|
||||
* type: integer
|
||||
* description: |
|
||||
* Optional. If provided, the photo is filed under the
|
||||
* given photo_categories.id (must belong to the event
|
||||
* or be a global category). If omitted, the photo
|
||||
* lands uncategorized.
|
||||
* responses:
|
||||
* 201:
|
||||
* description: Photo uploaded
|
||||
@@ -351,6 +358,7 @@ router.get('/events/:id', apiTokenAuth, requireApiScope('read'), async (req, res
|
||||
* path: { type: string }
|
||||
* thumbnail_path: { type: string, nullable: true }
|
||||
* size_bytes: { type: integer }
|
||||
* category_id: { type: integer, nullable: true }
|
||||
* 400: { description: No file or invalid type }
|
||||
* 404: { description: Event not found }
|
||||
*/
|
||||
@@ -368,6 +376,25 @@ router.post(
|
||||
const event = await db('events').where({ id: req.params.id }).first();
|
||||
if (!event) return res.status(404).json({ error: 'Event not found' });
|
||||
|
||||
// Optional category assignment, mirroring the admin upload route
|
||||
// (adminPhotos.js). Multipart form field `category_id`. If the
|
||||
// category looks up to a "collage" slug, the photo's `type` flips
|
||||
// accordingly so existing collage-aware UI paths still work.
|
||||
const rawCategoryId = req.body?.category_id;
|
||||
const parsedCategoryId = rawCategoryId ? parseInt(rawCategoryId, 10) : NaN;
|
||||
let categoryId = null;
|
||||
let photoType = 'individual';
|
||||
if (!Number.isNaN(parsedCategoryId)) {
|
||||
const category = await db('photo_categories').where({ id: parsedCategoryId }).first();
|
||||
if (!category) {
|
||||
return res.status(400).json({ error: `Unknown category_id ${parsedCategoryId}` });
|
||||
}
|
||||
categoryId = category.id;
|
||||
if (category.slug === 'collage' || category.slug === 'collages') {
|
||||
photoType = 'collage';
|
||||
}
|
||||
}
|
||||
|
||||
const ext = path.extname(req.file.originalname);
|
||||
const finalName = `${Date.now()}_${crypto.randomBytes(4).toString('hex')}${ext}`;
|
||||
// photo.path is stored relative to events/active so resolvePhotoStorageKey
|
||||
@@ -408,7 +435,8 @@ router.post(
|
||||
original_filename: req.file.originalname,
|
||||
path: relPath,
|
||||
thumbnail_path: thumbRel,
|
||||
type: 'individual',
|
||||
type: photoType,
|
||||
category_id: categoryId,
|
||||
size_bytes: stat.size,
|
||||
width,
|
||||
height,
|
||||
@@ -432,7 +460,14 @@ router.post(
|
||||
});
|
||||
} catch (e) { /* non-fatal */ }
|
||||
|
||||
res.status(201).json({ id, filename: finalName, path: relPath, thumbnail_path: thumbRel, size_bytes: stat.size });
|
||||
res.status(201).json({
|
||||
id,
|
||||
filename: finalName,
|
||||
path: relPath,
|
||||
thumbnail_path: thumbRel,
|
||||
size_bytes: stat.size,
|
||||
category_id: categoryId
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('v1 POST /events/:id/photos failed', { error: error.message });
|
||||
if (tempPath) await fs.unlink(tempPath).catch(() => {});
|
||||
|
||||
Reference in New Issue
Block a user