TKCG7EZ3MYG2FFWYRLGQTQXIRDSNGICDHK6CE6OZJPTBK6CNYFJAC /*** Protected API route to update an existing species** @route PUT /api/species/:id* @authentication Required* @param {string} id - Species ID in URL path* @param {Object} body - Species data to update (label, description, ebirdCode, active)* @returns {Object} Response containing:* - data: Updated species object with call types* @description Updates an existing species. Requires EDIT permission on the dataset.* When soft deleting (active=false), also soft deletes all related call types.*/
return c.json({ error: "Not implemented yet" }, 501);
try {const jwtPayload = (c as unknown as { jwtPayload: JWTPayload }).jwtPayload;const userId = jwtPayload.sub;const speciesId = c.req.param("id");const body = await c.req.json();const { label, description, ebirdCode, active } = body;const db = createDatabase(c.env);// Check if species exists and get its associated datasetsconst existingSpecies = await db.select({id: species.id,active: species.active}).from(species).where(eq(species.id, speciesId)).limit(1);if (existingSpecies.length === 0) {return c.json({error: "Species not found"}, 404);}// Get datasets associated with this species to check permissionsconst associatedDatasets = await db.select({ datasetId: speciesDataset.datasetId }).from(speciesDataset).where(eq(speciesDataset.speciesId, speciesId));if (associatedDatasets.length === 0) {return c.json({error: "Species not associated with any dataset"}, 400);}// Check if user has EDIT permission on at least one associated datasetlet hasPermission = false;for (const assoc of associatedDatasets) {if (await checkUserPermission(db, userId, assoc.datasetId, 'EDIT')) {hasPermission = true;break;}}if (!hasPermission) {return c.json({error: "You don't have permission to edit this species"}, 403);}// Start transaction for atomic updateconst result = await db.transaction(async (tx) => {// Build update object with only provided fieldsconst updateData: Record<string, unknown> = {lastModified: new Date(),modifiedBy: userId,};// Validate and add label if providedif (label !== undefined) {if (typeof label !== 'string' || label.trim().length === 0) {return c.json({error: "Field 'label' must be a non-empty string"}, 400);}if (label.length > 100) {return c.json({error: "Field 'label' must be 100 characters or less"}, 400);}updateData.label = label.trim();}// Validate and add description if providedif (description !== undefined) {if (description !== null && typeof description !== 'string') {return c.json({error: "Field 'description' must be a string or null"}, 400);}if (description && description.length > 255) {return c.json({error: "Field 'description' must be 255 characters or less"}, 400);}updateData.description = description?.trim() || null;}// Validate and add eBird code if providedif (ebirdCode !== undefined) {if (ebirdCode !== null && typeof ebirdCode !== 'string') {return c.json({error: "Field 'ebirdCode' must be a string or null"}, 400);}if (ebirdCode && ebirdCode.length > 12) {return c.json({error: "Field 'ebirdCode' must be 12 characters or less"}, 400);}updateData.ebirdCode = ebirdCode?.trim() || null;updateData.taxonomyVersion = ebirdCode ? '2024' : null;}// Add active status if provided (for soft delete)if (active !== undefined) {if (typeof active !== 'boolean') {return c.json({error: "Field 'active' must be a boolean"}, 400);}updateData.active = active;// If soft deleting species, also soft delete related recordsif (!active) {// Soft delete all call types for this speciesawait tx.update(callType).set({active: false,lastModified: new Date(),modifiedBy: userId}).where(eq(callType.speciesId, speciesId));// Delete all species-dataset associations for this species (hard delete since no active field)await tx.delete(speciesDataset).where(eq(speciesDataset.speciesId, speciesId));}}// Update the speciesconst [updatedSpecies] = await tx.update(species).set(updateData).where(eq(species.id, speciesId)).returning({id: species.id,label: species.label,ebirdCode: species.ebirdCode,taxonomyVersion: species.taxonomyVersion,description: species.description,createdAt: species.createdAt,createdBy: species.createdBy,lastModified: species.lastModified,modifiedBy: species.modifiedBy,active: species.active,});// Get updated call typesconst callTypes = await tx.select({id: callType.id,label: callType.label,}).from(callType).where(and(eq(callType.speciesId, speciesId), eq(callType.active, true)));return {...updatedSpecies,callTypes};});console.log("Updated species:", speciesId, "by user:", userId);return c.json({data: result});} catch (error) {console.error("Error updating species:", error);return c.json({error: "Failed to update species",details: error instanceof Error ? error.message : String(error),},500);}
/*** Protected API route to create a new cluster** @route POST /api/clusters* @authentication Required* @param {Object} body - Cluster data including datasetId, locationId, name, description, timezoneId, sampleRate, recordingPattern* - recordingPattern?: Object (optional) - { recordS: number, sleepS: number }* @returns {Object} Response containing:* - data: Created cluster object with optional recordingPattern* @description Creates a new cluster with the authenticated user as owner.* Requires EDIT permission on the dataset to create clusters within it.* If recordingPattern is provided, creates a new cyclic recording pattern record.*/
// Implementation would go here - similar to locations patternreturn c.json({ error: "Not implemented yet" }, 501);
try {const jwtPayload = (c as unknown as { jwtPayload: JWTPayload }).jwtPayload;const userId = jwtPayload.sub;const db = createDatabase(c.env);// Parse and validate request bodyconst body = await c.req.json();const { id, datasetId, locationId, name, description, timezoneId, sampleRate, recordingPattern } = body;// Field validationif (!id || typeof id !== 'string') {return c.json({error: "Missing or invalid required field: id"}, 400);}if (!datasetId || typeof datasetId !== 'string') {return c.json({error: "Missing or invalid required field: datasetId"}, 400);}if (!locationId || typeof locationId !== 'string') {return c.json({error: "Missing or invalid required field: locationId"}, 400);}if (!name || typeof name !== 'string' || name.trim().length === 0) {return c.json({error: "Missing or invalid required field: name"}, 400);}if (!sampleRate || typeof sampleRate !== 'number' || sampleRate <= 0) {return c.json({error: "Missing or invalid required field: sampleRate"}, 400);}// Length validationif (id.length !== 12) {return c.json({error: "Field 'id' must be exactly 12 characters (nanoid)"}, 400);}if (name.length > 140) {return c.json({error: "Field 'name' must be 140 characters or less"}, 400);}if (description && description.length > 255) {return c.json({error: "Field 'description' must be 255 characters or less"}, 400);}if (timezoneId && timezoneId.length > 40) {return c.json({error: "Field 'timezoneId' must be 40 characters or less"}, 400);}// Validate recording pattern if providedif (recordingPattern) {if (typeof recordingPattern !== 'object' || recordingPattern === null) {return c.json({error: "Field 'recordingPattern' must be an object"}, 400);}const { recordS, sleepS } = recordingPattern;if (typeof recordS !== 'number' || recordS <= 0 || !Number.isInteger(recordS)) {return c.json({error: "Field 'recordingPattern.recordS' must be a positive integer"}, 400);}if (typeof sleepS !== 'number' || sleepS <= 0 || !Number.isInteger(sleepS)) {return c.json({error: "Field 'recordingPattern.sleepS' must be a positive integer"}, 400);}}// Check if user has EDIT permission on the datasetconst hasPermission = await checkUserPermission(db, userId, datasetId.trim(), 'EDIT');if (!hasPermission) {return c.json({error: "You don't have permission to create clusters in this dataset"}, 403);}// Create cluster with optional recording pattern in transactionconst result = await db.transaction(async (tx) => {const now = new Date();let cyclicRecordingPatternId = null;// Create cyclic recording pattern if providedif (recordingPattern) {const patternId = nanoid(12);const [createdPattern] = await tx.insert(cyclicRecordingPattern).values({id: patternId,recordS: recordingPattern.recordS,sleepS: recordingPattern.sleepS,createdBy: userId,createdAt: now,lastModified: now,modifiedBy: userId,}).returning({id: cyclicRecordingPattern.id,});cyclicRecordingPatternId = createdPattern.id;}// Create cluster objectconst newCluster = {id: id.trim(),datasetId: datasetId.trim(),locationId: locationId.trim(),name: name.trim(),description: description?.trim() || null,timezoneId: timezoneId?.trim() || null,cyclicRecordingPatternId: cyclicRecordingPatternId,sampleRate: sampleRate,createdBy: userId,createdAt: now,lastModified: now,modifiedBy: userId,active: true,};// Insert the clusterconst [createdCluster] = await tx.insert(cluster).values(newCluster).returning({id: cluster.id,datasetId: cluster.datasetId,locationId: cluster.locationId,name: cluster.name,description: cluster.description,timezoneId: cluster.timezoneId,cyclicRecordingPatternId: cluster.cyclicRecordingPatternId,sampleRate: cluster.sampleRate,createdAt: cluster.createdAt,createdBy: cluster.createdBy,lastModified: cluster.lastModified,modifiedBy: cluster.modifiedBy,active: cluster.active,});// If recording pattern was created, include it in the responseconst recordingPatternData = recordingPattern ? {recordS: recordingPattern.recordS,sleepS: recordingPattern.sleepS} : null;return {...createdCluster,recordingPattern: recordingPatternData};});console.log("Created cluster:", result.id, "for location:", locationId, "by user:", userId);return c.json({data: result}, 201);} catch (error) {console.error("Error creating cluster:", error);// Handle unique constraint violationsif (error instanceof Error && error.message.includes('duplicate key')) {return c.json({error: "A cluster with this ID already exists"}, 400);}return c.json({error: "Failed to create cluster",details: error instanceof Error ? error.message : String(error),},500);}
/*** Protected API route to update an existing cluster** @route PUT /api/clusters/:id* @authentication Required* @param {string} id - Cluster ID in URL path* @param {Object} body - Cluster data to update (name, description, timezoneId, sampleRate, active, recordingPattern)* - recordingPattern?: Object | null (optional) - { recordS: number, sleepS: number } or null to remove* @returns {Object} Response containing:* - data: Updated cluster object with recordingPattern if present* @description Updates an existing cluster. Requires EDIT permission on the dataset.* recordingPattern can be provided to add/update or set to null to remove recording pattern.*/
// Implementation would go here - similar to locations patternreturn c.json({ error: "Not implemented yet" }, 501);
try {const jwtPayload = (c as unknown as { jwtPayload: JWTPayload }).jwtPayload;const userId = jwtPayload.sub;const clusterId = c.req.param("id");const body = await c.req.json();const { name, description, timezoneId, sampleRate, active, recordingPattern } = body;const db = createDatabase(c.env);// Check if cluster exists and get its dataset ID and current recording patternconst existingCluster = await db.select({id: cluster.id,datasetId: cluster.datasetId,active: cluster.active,cyclicRecordingPatternId: cluster.cyclicRecordingPatternId}).from(cluster).where(eq(cluster.id, clusterId)).limit(1);if (existingCluster.length === 0) {return c.json({error: "Cluster not found"}, 404);}const clusterRecord = existingCluster[0];// Check if user has EDIT permission on the datasetconst hasEditPermission = await checkUserPermission(db, userId, clusterRecord.datasetId, 'EDIT');if (!hasEditPermission) {return c.json({error: "You don't have permission to edit this cluster"}, 403);}// Build update object with only provided fieldsconst updateData: Record<string, unknown> = {lastModified: new Date(),modifiedBy: userId,};// Validate and add name if providedif (name !== undefined) {if (typeof name !== 'string' || name.trim().length === 0) {return c.json({error: "Field 'name' must be a non-empty string"}, 400);}if (name.length > 140) {return c.json({error: "Field 'name' must be 140 characters or less"}, 400);}updateData.name = name.trim();}// Validate and add description if providedif (description !== undefined) {if (description !== null && typeof description !== 'string') {return c.json({error: "Field 'description' must be a string or null"}, 400);}if (description && description.length > 255) {return c.json({error: "Field 'description' must be 255 characters or less"}, 400);}updateData.description = description?.trim() || null;}// Validate and add timezoneId if providedif (timezoneId !== undefined) {if (timezoneId !== null && typeof timezoneId !== 'string') {return c.json({error: "Field 'timezoneId' must be a string or null"}, 400);}if (timezoneId && timezoneId.length > 40) {return c.json({error: "Field 'timezoneId' must be 40 characters or less"}, 400);}updateData.timezoneId = timezoneId?.trim() || null;}// Validate and add sampleRate if providedif (sampleRate !== undefined) {if (typeof sampleRate !== 'number' || sampleRate <= 0) {return c.json({error: "Field 'sampleRate' must be a positive number"}, 400);}updateData.sampleRate = sampleRate;}// Validate recording pattern if providedif (recordingPattern !== undefined) {if (recordingPattern !== null && typeof recordingPattern !== 'object') {return c.json({error: "Field 'recordingPattern' must be an object or null"}, 400);}if (recordingPattern) {const { recordS, sleepS } = recordingPattern;if (typeof recordS !== 'number' || recordS <= 0 || !Number.isInteger(recordS)) {return c.json({error: "Field 'recordingPattern.recordS' must be a positive integer"}, 400);}if (typeof sleepS !== 'number' || sleepS <= 0 || !Number.isInteger(sleepS)) {return c.json({error: "Field 'recordingPattern.sleepS' must be a positive integer"}, 400);}}}// Add active status if provided (for soft delete)if (active !== undefined) {if (typeof active !== 'boolean') {return c.json({error: "Field 'active' must be a boolean"}, 400);}updateData.active = active;}// Update cluster with optional recording pattern changes in transactionconst result = await db.transaction(async (tx) => {const currentPatternId = clusterRecord.cyclicRecordingPatternId;// Handle recording pattern updatesif (recordingPattern !== undefined) {if (recordingPattern === null) {// Remove recording patternif (currentPatternId) {await tx.delete(cyclicRecordingPattern).where(eq(cyclicRecordingPattern.id, currentPatternId));updateData.cyclicRecordingPatternId = null;}} else {// Add or update recording patternif (currentPatternId) {// Update existing patternawait tx.update(cyclicRecordingPattern).set({recordS: recordingPattern.recordS,sleepS: recordingPattern.sleepS,lastModified: new Date(),modifiedBy: userId,}).where(eq(cyclicRecordingPattern.id, currentPatternId));} else {// Create new patternconst patternId = nanoid(12);await tx.insert(cyclicRecordingPattern).values({id: patternId,recordS: recordingPattern.recordS,sleepS: recordingPattern.sleepS,createdBy: userId,createdAt: new Date(),lastModified: new Date(),modifiedBy: userId,});updateData.cyclicRecordingPatternId = patternId;}}}// Update the clusterconst [updatedCluster] = await tx.update(cluster).set(updateData).where(eq(cluster.id, clusterId)).returning({id: cluster.id,datasetId: cluster.datasetId,locationId: cluster.locationId,name: cluster.name,description: cluster.description,timezoneId: cluster.timezoneId,cyclicRecordingPatternId: cluster.cyclicRecordingPatternId,sampleRate: cluster.sampleRate,createdAt: cluster.createdAt,createdBy: cluster.createdBy,lastModified: cluster.lastModified,modifiedBy: cluster.modifiedBy,active: cluster.active,});// Include recording pattern data in response if it existslet recordingPatternData = null;if (updatedCluster.cyclicRecordingPatternId) {if (recordingPattern !== undefined && recordingPattern !== null) {// Use the provided pattern datarecordingPatternData = {recordS: recordingPattern.recordS,sleepS: recordingPattern.sleepS};} else {// Fetch existing pattern dataconst existingPattern = await tx.select({recordS: cyclicRecordingPattern.recordS,sleepS: cyclicRecordingPattern.sleepS}).from(cyclicRecordingPattern).where(eq(cyclicRecordingPattern.id, updatedCluster.cyclicRecordingPatternId)).limit(1);if (existingPattern.length > 0) {recordingPatternData = existingPattern[0];}}}return {...updatedCluster,recordingPattern: recordingPatternData};});if (!result) {return c.json({error: "Failed to update cluster"}, 500);}console.log("Updated cluster:", result.id, "by user:", userId);return c.json({data: result});} catch (error) {console.error("Error updating cluster:", error);return c.json({error: "Failed to update cluster",details: error instanceof Error ? error.message : String(error),},500);}
/*** Protected API route to delete a cluster** @route DELETE /api/clusters/:id* @authentication Required* @param {string} id - Cluster ID in URL path* @returns {Object} Response containing:* - message: Success message* @description Soft deletes a cluster (sets active=false) and hard deletes its associated cyclic recording pattern if present.* Requires EDIT permission on the dataset.*/
// Implementation would go here - similar to locations patternreturn c.json({ error: "Not implemented yet" }, 501);
try {const jwtPayload = (c as unknown as { jwtPayload: JWTPayload }).jwtPayload;const userId = jwtPayload.sub;const clusterId = c.req.param("id");const db = createDatabase(c.env);// Check if cluster exists and get its dataset ID and recording patternconst existingCluster = await db.select({id: cluster.id,datasetId: cluster.datasetId,cyclicRecordingPatternId: cluster.cyclicRecordingPatternId}).from(cluster).where(eq(cluster.id, clusterId)).limit(1);if (existingCluster.length === 0) {return c.json({error: "Cluster not found"}, 404);}const clusterRecord = existingCluster[0];// Check if user has EDIT permission on the datasetconst hasEditPermission = await checkUserPermission(db, userId, clusterRecord.datasetId, 'EDIT');if (!hasEditPermission) {return c.json({error: "You don't have permission to delete this cluster"}, 403);}// Soft delete cluster and hard delete associated recording pattern in transactionawait db.transaction(async (tx) => {// Soft delete the cluster (set active = false)await tx.update(cluster).set({active: false,lastModified: new Date(),modifiedBy: userId}).where(eq(cluster.id, clusterId));// Hard delete associated cyclic recording pattern if it exists (no active field in this table)if (clusterRecord.cyclicRecordingPatternId) {await tx.delete(cyclicRecordingPattern).where(eq(cyclicRecordingPattern.id, clusterRecord.cyclicRecordingPatternId));}});console.log("Soft deleted cluster:", clusterId, "and hard deleted associated recording pattern by user:", userId);return c.json({message: "Cluster deleted successfully"});} catch (error) {console.error("Error deleting cluster:", error);return c.json({error: "Failed to delete cluster",details: error instanceof Error ? error.message : String(error),},500);}
import { authenticate } from "../middleware/auth";import type { Env } from "../types";
import { eq } from "drizzle-orm";import {callType,speciesDataset} from "../../../db/schema";import { authenticate, checkUserPermission } from "../middleware/auth";import { createDatabase } from "../utils/database";import type { Env, JWTPayload } from "../types";
/*** Protected API route to create a new call type** @route POST /api/callTypes* @authentication Required* @param {Object} body - Call type data including id, speciesId, label* @returns {Object} Response containing:* - data: Created call type object* @description Creates a new call type for a species. Requires EDIT permission on any dataset containing the species.*/
return c.json({ error: "Not implemented yet" }, 501);
try {const jwtPayload = (c as unknown as { jwtPayload: JWTPayload }).jwtPayload;const userId = jwtPayload.sub;const db = createDatabase(c.env);// Parse and validate request bodyconst body = await c.req.json();const { id, speciesId, label } = body;// Field validationif (!id || typeof id !== 'string' || id.length !== 12) {return c.json({error: "Field 'id' must be exactly 12 characters (nanoid)"}, 400);}if (!speciesId || typeof speciesId !== 'string') {return c.json({error: "Missing or invalid required field: speciesId"}, 400);}if (!label || typeof label !== 'string' || label.trim().length === 0) {return c.json({error: "Missing or invalid required field: label"}, 400);}if (label.length > 100) {return c.json({error: "Field 'label' must be 100 characters or less"}, 400);}// Check if species exists and get its associated datasetsconst associatedDatasets = await db.select({ datasetId: speciesDataset.datasetId }).from(speciesDataset).where(eq(speciesDataset.speciesId, speciesId));if (associatedDatasets.length === 0) {return c.json({error: "Species not found or not associated with any dataset"}, 404);}// Check if user has EDIT permission on at least one associated datasetlet hasPermission = false;for (const assoc of associatedDatasets) {if (await checkUserPermission(db, userId, assoc.datasetId, 'EDIT')) {hasPermission = true;break;}}if (!hasPermission) {return c.json({error: "You don't have permission to create call types for this species"}, 403);}// Create call typeconst now = new Date();const [createdCallType] = await db.insert(callType).values({id: id.trim(),speciesId: speciesId.trim(),label: label.trim(),createdBy: userId,createdAt: now,lastModified: now,modifiedBy: userId,active: true,}).returning({id: callType.id,speciesId: callType.speciesId,label: callType.label,createdAt: callType.createdAt,active: callType.active,});console.log("Created call type:", createdCallType.id, "for species:", speciesId, "by user:", userId);return c.json({data: createdCallType}, 201);} catch (error) {console.error("Error creating call type:", error);if (error instanceof Error && error.message.includes('duplicate key')) {return c.json({error: "A call type with this ID already exists"}, 400);}return c.json({error: "Failed to create call type",details: error instanceof Error ? error.message : String(error),},500);}
/*** Protected API route to update an existing call type** @route PUT /api/callTypes/:id* @authentication Required* @param {string} id - Call type ID in URL path* @param {Object} body - Call type data to update (label, active)* @returns {Object} Response containing:* - data: Updated call type object* @description Updates an existing call type. Requires EDIT permission on any dataset containing the parent species.*/
return c.json({ error: "Not implemented yet" }, 501);
try {const jwtPayload = (c as unknown as { jwtPayload: JWTPayload }).jwtPayload;const userId = jwtPayload.sub;const callTypeId = c.req.param("id");const body = await c.req.json();const { label, active } = body;const db = createDatabase(c.env);// Check if call type exists and get its speciesconst existingCallType = await db.select({id: callType.id,speciesId: callType.speciesId,active: callType.active}).from(callType).where(eq(callType.id, callTypeId)).limit(1);if (existingCallType.length === 0) {return c.json({error: "Call type not found"}, 404);}const speciesId = existingCallType[0].speciesId;// Get datasets associated with the species to check permissionsconst associatedDatasets = await db.select({ datasetId: speciesDataset.datasetId }).from(speciesDataset).where(eq(speciesDataset.speciesId, speciesId));// Check if user has EDIT permission on at least one associated datasetlet hasPermission = false;for (const assoc of associatedDatasets) {if (await checkUserPermission(db, userId, assoc.datasetId, 'EDIT')) {hasPermission = true;break;}}if (!hasPermission) {return c.json({error: "You don't have permission to edit this call type"}, 403);}// Build update object with only provided fieldsconst updateData: Record<string, unknown> = {lastModified: new Date(),modifiedBy: userId,};// Validate and add label if providedif (label !== undefined) {if (typeof label !== 'string' || label.trim().length === 0) {return c.json({error: "Field 'label' must be a non-empty string"}, 400);}if (label.length > 100) {return c.json({error: "Field 'label' must be 100 characters or less"}, 400);}updateData.label = label.trim();}// Add active status if provided (for soft delete)if (active !== undefined) {if (typeof active !== 'boolean') {return c.json({error: "Field 'active' must be a boolean"}, 400);}updateData.active = active;}// Update the call typeconst [updatedCallType] = await db.update(callType).set(updateData).where(eq(callType.id, callTypeId)).returning({id: callType.id,speciesId: callType.speciesId,label: callType.label,createdAt: callType.createdAt,lastModified: callType.lastModified,active: callType.active,});if (!updatedCallType) {return c.json({error: "Failed to update call type"}, 500);}console.log("Updated call type:", callTypeId, "by user:", userId);return c.json({data: updatedCallType});} catch (error) {console.error("Error updating call type:", error);return c.json({error: "Failed to update call type",details: error instanceof Error ? error.message : String(error),},500);}