GDEZHFJTAHWBJGQ3UD6DAZNWORGIR5RV6VM3IURGL3R7OGXSNFMAC * @param {Object} body - Cluster data including datasetId, locationId, name, description, timezoneId, sampleRate
* @param {Object} body - Cluster data including datasetId, locationId, name, description, timezoneId, sampleRate, recordingPattern* - recordingPattern?: Object (optional) - { recordS: number, sleepS: number }
}// 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);}
// Create cluster objectconst now = new Date();const newCluster = {id: id.trim(),datasetId: datasetId.trim(),locationId: locationId.trim(),name: name.trim(),description: description?.trim() || null,timezoneId: timezoneId?.trim() || null,cyclicRecordingPatternId: null, // Not supported in form yetsampleRate: sampleRate,createdBy: userId,createdAt: now,lastModified: now,modifiedBy: userId,active: true,};
// 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;
// Insert the clusterconst result = await db.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,
return {...createdCluster,recordingPattern: recordingPatternData};
* @param {Object} body - Cluster data to update (name, description, timezoneId, sampleRate, active)
* @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
// 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);}
// Update the clusterconst result = await db.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,});
// 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;}}}
if (result.length === 0) {
// 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) {
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.*/app.delete("/api/clusters/:id", authenticate, async (c) => {try {// Get user from JWTconst jwtPayload = (c as unknown as { jwtPayload: JWTPayload }).jwtPayload;const userId = jwtPayload.sub;// Get cluster ID from URL parametersconst clusterId = c.req.param("id");// Connect to databaseconst sql = neon(c.env.DATABASE_URL);const db = drizzle(sql);// 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",
const updatePayload: Record<string, unknown> = {name: formData.name,description: formData.description || null,timezoneId: formData.timezoneId || null,sampleRate: parseInt(formData.sampleRate),};// Add recording pattern if enabledif (formData.hasRecordingPattern && formData.recordS && formData.sleepS) {updatePayload.recordingPattern = {recordS: parseInt(formData.recordS),sleepS: parseInt(formData.sleepS)};} else if (!formData.hasRecordingPattern) {// Explicitly remove recording patternupdatePayload.recordingPattern = null;}
const createPayload: Record<string, unknown> = {id,datasetId,locationId,name: formData.name,description: formData.description || null,timezoneId: formData.timezoneId || null,sampleRate: parseInt(formData.sampleRate),};// Add recording pattern if enabledif (formData.hasRecordingPattern && formData.recordS && formData.sleepS) {createPayload.recordingPattern = {recordS: parseInt(formData.recordS),sleepS: parseInt(formData.sleepS)};}
sampleRate: String(cluster.sampleRate)
sampleRate: String(cluster.sampleRate),hasRecordingPattern: !!cluster.recordingPattern,recordS: cluster.recordingPattern ? String(cluster.recordingPattern.recordS) : '',sleepS: cluster.recordingPattern ? String(cluster.recordingPattern.sleepS) : ''
const confirmDelete = window.confirm(`Are you sure you want to delete "${cluster.name}"? This action cannot be undone.`);
const confirmDelete = window.confirm(`Are you sure you want to delete "${cluster.name}"? This will soft delete the cluster and remove any recording pattern.`);
</div>{/* Recording Pattern Section */}<div className="border-t pt-4"><div className="flex items-center mb-3"><inputtype="checkbox"id="hasRecordingPattern"checked={formData.hasRecordingPattern}onChange={(e) => handleFormChange('hasRecordingPattern', e.target.checked)}className="mr-2"/><label htmlFor="hasRecordingPattern" className="text-sm font-medium text-gray-700">Cyclic Recording Pattern</label></div>{formData.hasRecordingPattern && (<div className="grid grid-cols-2 gap-3 ml-6"><div><label htmlFor="recordS" className="block text-sm font-medium text-gray-700 mb-1">Record (seconds) *</label><inputtype="number"id="recordS"required={formData.hasRecordingPattern}min="1"max="86400"value={formData.recordS}onChange={(e) => handleFormChange('recordS', e.target.value)}className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"placeholder="895"/></div><div><label htmlFor="sleepS" className="block text-sm font-medium text-gray-700 mb-1">Sleep (seconds) *</label><inputtype="number"id="sleepS"required={formData.hasRecordingPattern}min="1"max="86400"value={formData.sleepS}onChange={(e) => handleFormChange('sleepS', e.target.value)}className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"placeholder="5"/></div></div>)}{formData.hasRecordingPattern && (<p className="text-xs text-gray-500 mt-2 ml-6">Some recorders pause while saving data. Enter the record and sleep durations in seconds.</p>)}