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 provided
if (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 object
const 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 yet
sampleRate: sampleRate,
createdBy: userId,
createdAt: now,
lastModified: now,
modifiedBy: userId,
active: true,
};
// Create cyclic recording pattern if provided
if (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 object
const 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 cluster
const [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 response
const recordingPatternData = recordingPattern ? {
recordS: recordingPattern.recordS,
sleepS: recordingPattern.sleepS
} : null;
// Insert the cluster
const 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 provided
if (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 cluster
const 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 updates
if (recordingPattern !== undefined) {
if (recordingPattern === null) {
// Remove recording pattern
if (currentPatternId) {
await tx.delete(cyclicRecordingPattern).where(eq(cyclicRecordingPattern.id, currentPatternId));
updateData.cyclicRecordingPatternId = null;
}
} else {
// Add or update recording pattern
if (currentPatternId) {
// Update existing pattern
await tx
.update(cyclicRecordingPattern)
.set({
recordS: recordingPattern.recordS,
sleepS: recordingPattern.sleepS,
lastModified: new Date(),
modifiedBy: userId,
})
.where(eq(cyclicRecordingPattern.id, currentPatternId));
} else {
// Create new pattern
const 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 cluster
const [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 exists
let recordingPatternData = null;
if (updatedCluster.cyclicRecordingPatternId) {
if (recordingPattern !== undefined && recordingPattern !== null) {
// Use the provided pattern data
recordingPatternData = {
recordS: recordingPattern.recordS,
sleepS: recordingPattern.sleepS
};
} else {
// Fetch existing pattern data
const 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 JWT
const jwtPayload = (c as unknown as { jwtPayload: JWTPayload }).jwtPayload;
const userId = jwtPayload.sub;
// Get cluster ID from URL parameters
const clusterId = c.req.param("id");
// Connect to database
const sql = neon(c.env.DATABASE_URL);
const db = drizzle(sql);
// Check if cluster exists and get its dataset ID and recording pattern
const 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 dataset
const 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 transaction
await 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 enabled
if (formData.hasRecordingPattern && formData.recordS && formData.sleepS) {
updatePayload.recordingPattern = {
recordS: parseInt(formData.recordS),
sleepS: parseInt(formData.sleepS)
};
} else if (!formData.hasRecordingPattern) {
// Explicitly remove recording pattern
updatePayload.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 enabled
if (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">
<input
type="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>
<input
type="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>
<input
type="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>
)}