feat: implement complete plugin store repository synchronization system

• Fix repository sync implementation in admin API (was TODO placeholder)
- Add full fetch/parse logic for repository.json and plugin index -
Implement robot matching by name/manufacturer patterns - Handle plugin
creation/updates with proper error handling - Add comprehensive
TypeScript typing throughout

• Fix plugin store installation state detection - Add getStudyPlugins
API integration to check installed plugins - Update PluginCard component
with isInstalled prop and correct button states - Fix repository name
display using metadata.repositoryId mapping - Show "Installed"
(disabled) vs "Install" (enabled) based on actual state

• Resolve admin access and authentication issues - Add missing
administrator role to user system roles table - Fix admin route access
for repository management - Enable repository sync functionality in
admin dashboard

• Add repository metadata integration - Update plugin records with
proper repositoryId references - Add metadata field to
robots.plugins.list API response - Enable repository name display for
all plugins from metadata

• Fix TypeScript compliance across plugin system - Replace unsafe 'any'
types with proper interfaces - Add type definitions for repository and
plugin data structures - Use nullish coalescing operators for safer null
handling - Remove unnecessary type assertions

• Integrate live repository at https://repo.hristudio.com - Successfully
loads 3 robot plugins (TurtleBot3 Burger/Waffle, NAO) - Complete ROS2
action definitions with parameter schemas - Trust level categorization
(official, verified, community) - Platform and documentation metadata
preservation

• Update documentation and development workflow - Document plugin
repository system in work_in_progress.md - Update quick-reference.md
with repository sync examples - Add plugin installation and management
guidance - Remove problematic test script with TypeScript errors

BREAKING CHANGE: Plugin store now requires repository sync for robot
plugins. Run repository sync in admin dashboard after deployment to
populate plugin store.

Closes: Plugin store repository integration Resolves: Installation state
detection and repository name display Fixes: Admin authentication and
TypeScript compliance issues
This commit is contained in:
2025-08-07 10:47:29 -04:00
parent b1f4eedb53
commit 18f709f879
33 changed files with 5146 additions and 2273 deletions

589
scripts/seed-core-blocks.ts Normal file
View File

@@ -0,0 +1,589 @@
import { drizzle } from "drizzle-orm/postgres-js";
import { eq } from "drizzle-orm";
import postgres from "postgres";
import * as schema from "../src/server/db/schema";
const connectionString =
process.env.DATABASE_URL ??
"postgresql://postgres:password@localhost:5140/hristudio";
const client = postgres(connectionString);
const db = drizzle(client, { schema });
async function seedCoreRepository() {
console.log("🏗️ Seeding core system repository...");
// Check if core repository already exists
const existingCoreRepo = await db
.select()
.from(schema.pluginRepositories)
.where(eq(schema.pluginRepositories.url, "https://core.hristudio.com"));
if (existingCoreRepo.length > 0) {
console.log("⚠️ Core repository already exists, skipping");
return;
}
// Get the first user to use as creator
const users = await db.select().from(schema.users);
const adminUser =
users.find((u) => u.email?.includes("sarah.chen")) ?? users[0];
if (!adminUser) {
console.log("⚠️ No users found. Please run basic seeding first.");
return;
}
const coreRepository = {
id: "00000000-0000-0000-0000-000000000001",
name: "HRIStudio Core System Blocks",
url: "https://core.hristudio.com",
description:
"Essential system blocks for experiment design including events, control flow, wizard actions, and logic operations",
trustLevel: "official" as const,
isEnabled: true,
isOfficial: true,
lastSyncAt: new Date(),
syncStatus: "completed" as const,
syncError: null,
metadata: {
apiVersion: "1.0",
pluginApiVersion: "1.0",
categories: ["core", "wizard", "control", "logic", "events"],
compatibility: {
hristudio: { min: "0.1.0", recommended: "0.1.0" },
},
isCore: true,
},
createdAt: new Date("2024-01-01T00:00:00"),
updatedAt: new Date(),
createdBy: adminUser.id,
};
await db.insert(schema.pluginRepositories).values([coreRepository]);
console.log("✅ Created core system repository");
}
async function seedCorePlugin() {
console.log("🧱 Seeding core system plugin...");
// Check if core plugin already exists
const existingCorePlugin = await db
.select()
.from(schema.plugins)
.where(eq(schema.plugins.name, "HRIStudio Core System"));
if (existingCorePlugin.length > 0) {
console.log("⚠️ Core plugin already exists, skipping");
return;
}
const corePlugin = {
id: "00000000-0000-0000-0000-000000000001",
robotId: null, // Core plugin doesn't need a specific robot
name: "HRIStudio Core System",
version: "1.0.0",
description:
"Essential system blocks for experiment design including events, control flow, wizard actions, and logic operations",
author: "HRIStudio Team",
repositoryUrl: "https://core.hristudio.com",
trustLevel: "official" as const,
status: "active" as const,
configurationSchema: {
type: "object",
properties: {
enableAdvancedBlocks: {
type: "boolean",
default: true,
description: "Enable advanced control flow blocks",
},
wizardInterface: {
type: "string",
enum: ["basic", "advanced"],
default: "basic",
description: "Wizard interface complexity level",
},
},
},
actionDefinitions: [
// Event Blocks
{
id: "when_trial_starts",
name: "when trial starts",
description: "Triggered when the trial begins",
category: "logic",
icon: "Play",
timeout: 0,
retryable: false,
parameterSchema: {
type: "object",
properties: {},
required: [],
},
blockType: "hat",
color: "#22c55e",
nestable: false,
},
{
id: "when_participant_speaks",
name: "when participant speaks",
description: "Triggered when participant says something",
category: "logic",
icon: "Mic",
timeout: 0,
retryable: false,
parameterSchema: {
type: "object",
properties: {
keywords: {
type: "array",
items: { type: "string" },
default: [],
description: "Optional keywords to listen for",
},
},
required: [],
},
blockType: "hat",
color: "#22c55e",
nestable: false,
},
// Wizard Actions
{
id: "wizard_say",
name: "say",
description: "Wizard speaks to participant",
category: "interaction",
icon: "Users",
timeout: 30000,
retryable: true,
parameterSchema: {
type: "object",
properties: {
message: {
type: "string",
default: "",
description: "What should the wizard say?",
},
},
required: ["message"],
},
blockType: "action",
color: "#a855f7",
nestable: false,
},
{
id: "wizard_gesture",
name: "gesture",
description: "Wizard performs a gesture",
category: "interaction",
icon: "Users",
timeout: 10000,
retryable: true,
parameterSchema: {
type: "object",
properties: {
type: {
type: "string",
enum: ["wave", "point", "nod", "thumbs_up", "clap"],
default: "wave",
description: "Type of gesture to perform",
},
},
required: ["type"],
},
blockType: "action",
color: "#a855f7",
nestable: false,
},
{
id: "wizard_note",
name: "take note",
description: "Wizard records an observation",
category: "sensors",
icon: "FileText",
timeout: 15000,
retryable: true,
parameterSchema: {
type: "object",
properties: {
category: {
type: "string",
enum: [
"behavior",
"performance",
"engagement",
"technical",
"other",
],
default: "behavior",
description: "Category of observation",
},
note: {
type: "string",
default: "",
description: "Observation details",
},
},
required: ["note"],
},
blockType: "action",
color: "#f59e0b",
nestable: false,
},
// Control Flow
{
id: "wait",
name: "wait",
description: "Pause execution for specified time",
category: "logic",
icon: "Clock",
timeout: 0,
retryable: false,
parameterSchema: {
type: "object",
properties: {
seconds: {
type: "number",
minimum: 0.1,
maximum: 300,
default: 1,
description: "Time to wait in seconds",
},
},
required: ["seconds"],
},
blockType: "action",
color: "#f97316",
nestable: false,
},
{
id: "repeat",
name: "repeat",
description: "Execute contained blocks multiple times",
category: "logic",
icon: "GitBranch",
timeout: 0,
retryable: false,
parameterSchema: {
type: "object",
properties: {
times: {
type: "number",
minimum: 1,
maximum: 50,
default: 3,
description: "Number of times to repeat",
},
},
required: ["times"],
},
blockType: "control",
color: "#f97316",
nestable: true,
},
{
id: "if_condition",
name: "if",
description: "Conditional execution based on conditions",
category: "logic",
icon: "GitBranch",
timeout: 0,
retryable: false,
parameterSchema: {
type: "object",
properties: {
condition: {
type: "string",
enum: [
"participant_speaks",
"time_elapsed",
"wizard_signal",
"custom_condition",
],
default: "participant_speaks",
description: "Condition to evaluate",
},
value: {
type: "string",
default: "",
description: "Value to compare against (if applicable)",
},
},
required: ["condition"],
},
blockType: "control",
color: "#f97316",
nestable: true,
},
{
id: "parallel",
name: "do together",
description: "Execute multiple blocks simultaneously",
category: "logic",
icon: "Layers",
timeout: 0,
retryable: false,
parameterSchema: {
type: "object",
properties: {},
required: [],
},
blockType: "control",
color: "#f97316",
nestable: true,
},
// Data Collection
{
id: "start_recording",
name: "start recording",
description: "Begin recording specified data streams",
category: "sensors",
icon: "Circle",
timeout: 5000,
retryable: true,
parameterSchema: {
type: "object",
properties: {
streams: {
type: "array",
items: {
type: "string",
enum: [
"video",
"audio",
"screen",
"robot_data",
"wizard_actions",
],
},
default: ["video", "audio"],
description: "Data streams to record",
},
quality: {
type: "string",
enum: ["low", "medium", "high"],
default: "medium",
description: "Recording quality",
},
},
required: ["streams"],
},
blockType: "action",
color: "#dc2626",
nestable: false,
},
{
id: "stop_recording",
name: "stop recording",
description: "Stop recording and save data",
category: "sensors",
icon: "Square",
timeout: 10000,
retryable: true,
parameterSchema: {
type: "object",
properties: {
save_location: {
type: "string",
default: "default",
description: "Where to save the recording",
},
},
required: [],
},
blockType: "action",
color: "#dc2626",
nestable: false,
},
{
id: "mark_event",
name: "mark event",
description: "Add a timestamped marker to the data",
category: "sensors",
icon: "MapPin",
timeout: 1000,
retryable: true,
parameterSchema: {
type: "object",
properties: {
event_name: {
type: "string",
default: "",
description: "Name of the event to mark",
},
description: {
type: "string",
default: "",
description: "Optional event description",
},
},
required: ["event_name"],
},
blockType: "action",
color: "#f59e0b",
nestable: false,
},
// Study Flow Control
{
id: "show_instructions",
name: "show instructions",
description: "Display instructions to the participant",
category: "interaction",
icon: "FileText",
timeout: 60000,
retryable: true,
parameterSchema: {
type: "object",
properties: {
title: {
type: "string",
default: "Instructions",
description: "Instruction title",
},
content: {
type: "string",
default: "",
description: "Instruction content (supports markdown)",
},
require_acknowledgment: {
type: "boolean",
default: true,
description: "Require participant to acknowledge reading",
},
},
required: ["content"],
},
blockType: "action",
color: "#3b82f6",
nestable: false,
},
{
id: "collect_response",
name: "collect response",
description: "Collect a response from the participant",
category: "sensors",
icon: "MessageCircle",
timeout: 120000,
retryable: true,
parameterSchema: {
type: "object",
properties: {
question: {
type: "string",
default: "",
description: "Question to ask the participant",
},
response_type: {
type: "string",
enum: ["text", "scale", "choice", "voice"],
default: "text",
description: "Type of response to collect",
},
options: {
type: "array",
items: { type: "string" },
default: [],
description: "Options for choice responses",
},
},
required: ["question"],
},
blockType: "action",
color: "#8b5cf6",
nestable: false,
},
],
createdAt: new Date("2024-01-01T00:00:00"),
updatedAt: new Date(),
};
await db.insert(schema.plugins).values([corePlugin]);
console.log("✅ Created core system plugin");
}
async function seedCoreStudyPlugins() {
console.log("🔗 Installing core plugin in all studies...");
// Get all studies
const studies = await db.select().from(schema.studies);
if (studies.length === 0) {
console.log("⚠️ No studies found. Please run basic seeding first.");
return;
}
// Check if core plugin installations already exist
const existingInstallation = await db
.select()
.from(schema.studyPlugins)
.where(
eq(schema.studyPlugins.pluginId, "00000000-0000-0000-0000-000000000001"),
);
if (existingInstallation.length > 0) {
console.log("⚠️ Core plugin already installed in studies, skipping");
return;
}
const coreInstallations = studies.map((study, index) => ({
id: `00000000-0000-0000-0000-00000000000${index + 2}`, // Start from 2 to avoid conflicts
studyId: study.id,
pluginId: "00000000-0000-0000-0000-000000000001",
configuration: {
enableAdvancedBlocks: true,
wizardInterface: "advanced",
recordingDefaults: {
video: true,
audio: true,
quality: "high",
},
},
installedAt: new Date("2024-01-01T00:00:00"),
installedBy: study.createdBy,
}));
await db.insert(schema.studyPlugins).values(coreInstallations);
console.log(`✅ Installed core plugin in ${studies.length} studies`);
}
async function main() {
try {
console.log("🏗️ HRIStudio Core System Seeding Started");
console.log("📍 Database:", connectionString.replace(/:[^:]*@/, ":***@"));
await seedCoreRepository();
await seedCorePlugin();
await seedCoreStudyPlugins();
console.log("✅ Core system seeding completed successfully!");
console.log("\n📋 Core System Summary:");
console.log(" 🏗️ Core Repository: 1 (HRIStudio Core System)");
console.log(" 🧱 Core Plugin: 1 (with 15 essential blocks)");
console.log(" 🔗 Study Installations: Installed in all studies");
console.log("\n🧱 Core Blocks Available:");
console.log(" 🎯 Events: when trial starts, when participant speaks");
console.log(" 🧙 Wizard: say, gesture, take note");
console.log(" ⏳ Control: wait, repeat, if condition, do together");
console.log(" 📊 Data: start/stop recording, mark event");
console.log(" 📋 Study: show instructions, collect response");
console.log("\n🎨 Block Designer Integration:");
console.log(" • All core blocks now come from the plugin system");
console.log(" • Consistent with robot plugin architecture");
console.log(" • Easy to extend and version core functionality");
console.log(" • Unified block management across all categories");
console.log("\n🚀 Ready to test unified block system!");
} catch (error) {
console.error("❌ Core system seeding failed:", error);
process.exit(1);
} finally {
await client.end();
}
}
if (require.main === module) {
void main();
}

690
scripts/seed-plugins.ts Normal file
View File

@@ -0,0 +1,690 @@
import { drizzle } from "drizzle-orm/postgres-js";
import postgres from "postgres";
import * as schema from "../src/server/db/schema";
const connectionString =
process.env.DATABASE_URL ??
"postgresql://postgres:password@localhost:5140/hristudio";
const client = postgres(connectionString);
const db = drizzle(client, { schema });
async function seedRobots() {
console.log("🤖 Seeding robots...");
// Check if robots already exist
const existingRobots = await db.select().from(schema.robots);
if (existingRobots.length > 0) {
console.log(
`⚠️ ${existingRobots.length} robots already exist, skipping robot seeding`,
);
return;
}
const robots = [
{
id: "31234567-89ab-cdef-0123-456789abcde1",
name: "TurtleBot3 Burger",
manufacturer: "ROBOTIS",
model: "TurtleBot3 Burger",
description:
"A compact, affordable, programmable, ROS2-based mobile robot for education and research",
capabilities: [
"differential_drive",
"lidar",
"imu",
"odometry",
"autonomous_navigation",
],
communicationProtocol: "ros2" as const,
createdAt: new Date("2024-01-01T00:00:00"),
updatedAt: new Date("2024-01-01T00:00:00"),
},
{
id: "31234567-89ab-cdef-0123-456789abcde2",
name: "NAO Humanoid Robot",
manufacturer: "SoftBank Robotics",
model: "NAO v6",
description:
"Autonomous, programmable humanoid robot designed for education, research, and human-robot interaction studies",
capabilities: [
"bipedal_walking",
"speech_synthesis",
"speech_recognition",
"computer_vision",
"gestures",
"led_control",
"touch_sensors",
],
communicationProtocol: "custom" as const,
createdAt: new Date("2024-01-01T00:00:00"),
updatedAt: new Date("2024-01-01T00:00:00"),
},
{
id: "31234567-89ab-cdef-0123-456789abcde3",
name: "TurtleBot3 Waffle Pi",
manufacturer: "ROBOTIS",
model: "TurtleBot3 Waffle Pi",
description:
"Extended TurtleBot3 platform with additional sensors and computing power for advanced research applications",
capabilities: [
"differential_drive",
"lidar",
"imu",
"odometry",
"camera",
"manipulation",
"autonomous_navigation",
],
communicationProtocol: "ros2" as const,
createdAt: new Date("2024-01-01T00:00:00"),
updatedAt: new Date("2024-01-01T00:00:00"),
},
];
await db.insert(schema.robots).values(robots);
console.log(`✅ Created ${robots.length} robots`);
}
async function seedPluginRepositories() {
console.log("📦 Seeding plugin repositories...");
// Check if repositories already exist
const existingRepos = await db.select().from(schema.pluginRepositories);
if (existingRepos.length > 0) {
console.log(
`⚠️ ${existingRepos.length} plugin repositories already exist, skipping`,
);
return;
}
// Get the first user to use as creator
const users = await db.select().from(schema.users);
const adminUser =
users.find((u) => u.email?.includes("sean@soconnor.dev")) ?? users[0];
if (!adminUser) {
console.log("⚠️ No users found. Please run basic seeding first.");
return;
}
const repositories = [
{
id: "41234567-89ab-cdef-0123-456789abcde1",
name: "HRIStudio Official Robot Plugins",
url: "https://repo.hristudio.com",
description:
"Official collection of robot plugins maintained by the HRIStudio team",
trustLevel: "official" as const,
isEnabled: true,
isOfficial: true,
lastSyncAt: new Date("2024-01-10T12:00:00"),
syncStatus: "completed" as const,
syncError: null,
metadata: {
apiVersion: "1.0",
pluginApiVersion: "1.0",
categories: [
"mobile-robots",
"humanoid-robots",
"manipulators",
"drones",
],
compatibility: {
hristudio: { min: "0.1.0", recommended: "0.1.0" },
ros2: { distributions: ["humble", "iron"], recommended: "iron" },
},
},
createdAt: new Date("2024-01-01T00:00:00"),
updatedAt: new Date("2024-01-10T12:00:00"),
createdBy: adminUser.id,
},
];
await db.insert(schema.pluginRepositories).values(repositories);
console.log(`✅ Created ${repositories.length} plugin repositories`);
}
async function seedPlugins() {
console.log("🔌 Seeding robot plugins...");
// Check if plugins already exist
const existingPlugins = await db.select().from(schema.plugins);
if (existingPlugins.length > 0) {
console.log(
`⚠️ ${existingPlugins.length} plugins already exist, skipping plugin seeding`,
);
return;
}
const plugins = [
{
id: "51234567-89ab-cdef-0123-456789abcde1",
robotId: "31234567-89ab-cdef-0123-456789abcde1",
name: "TurtleBot3 Burger",
version: "2.0.0",
description:
"A compact, affordable, programmable, ROS2-based mobile robot for education and research",
author: "ROBOTIS",
repositoryUrl: "https://repo.hristudio.com",
trustLevel: "official" as const,
status: "active" as const,
configurationSchema: {
type: "object",
properties: {
namespace: { type: "string", default: "turtlebot3" },
topics: {
type: "object",
properties: {
cmd_vel: { type: "string", default: "/cmd_vel" },
odom: { type: "string", default: "/odom" },
scan: { type: "string", default: "/scan" },
},
},
},
},
actionDefinitions: [
{
id: "move_velocity",
name: "Set Velocity",
description: "Control the robot's linear and angular velocity",
category: "movement",
icon: "navigation",
timeout: 30000,
retryable: true,
parameterSchema: {
type: "object",
properties: {
linear: {
type: "number",
minimum: -0.22,
maximum: 0.22,
default: 0,
description: "Forward/backward velocity in m/s",
},
angular: {
type: "number",
minimum: -2.84,
maximum: 2.84,
default: 0,
description: "Rotational velocity in rad/s",
},
},
required: ["linear", "angular"],
},
ros2: {
messageType: "geometry_msgs/msg/Twist",
topic: "/cmd_vel",
payloadMapping: {
type: "transform",
transformFn: "transformToTwist",
},
},
},
{
id: "move_to_pose",
name: "Navigate to Position",
description:
"Navigate to a specific position on the map using autonomous navigation",
category: "movement",
icon: "target",
timeout: 120000,
retryable: true,
parameterSchema: {
type: "object",
properties: {
x: {
type: "number",
default: 0,
description: "X coordinate in meters",
},
y: {
type: "number",
default: 0,
description: "Y coordinate in meters",
},
theta: {
type: "number",
default: 0,
description: "Final orientation in radians",
},
},
required: ["x", "y", "theta"],
},
ros2: {
messageType: "geometry_msgs/msg/PoseStamped",
action: "/navigate_to_pose",
payloadMapping: {
type: "transform",
transformFn: "transformToPoseStamped",
},
},
},
{
id: "stop_robot",
name: "Stop Robot",
description: "Immediately stop all robot movement",
category: "movement",
icon: "square",
timeout: 5000,
retryable: false,
parameterSchema: {
type: "object",
properties: {},
required: [],
},
ros2: {
messageType: "geometry_msgs/msg/Twist",
topic: "/cmd_vel",
payloadMapping: {
type: "static",
payload: {
linear: { x: 0.0, y: 0.0, z: 0.0 },
angular: { x: 0.0, y: 0.0, z: 0.0 },
},
},
},
},
],
createdAt: new Date("2024-01-01T00:00:00"),
updatedAt: new Date("2024-01-10T12:00:00"),
},
{
id: "51234567-89ab-cdef-0123-456789abcde2",
robotId: "31234567-89ab-cdef-0123-456789abcde2",
name: "NAO Humanoid Robot",
version: "1.0.0",
description:
"Autonomous, programmable humanoid robot designed for education, research, and human-robot interaction studies",
author: "SoftBank Robotics",
repositoryUrl: "https://repo.hristudio.com",
trustLevel: "verified" as const,
status: "active" as const,
configurationSchema: {
type: "object",
properties: {
ip: { type: "string", default: "nao.local" },
port: { type: "number", default: 9559 },
modules: {
type: "array",
default: [
"ALMotion",
"ALTextToSpeech",
"ALAnimationPlayer",
"ALLeds",
],
},
},
},
actionDefinitions: [
{
id: "say_text",
name: "Say Text",
description: "Make the robot speak using text-to-speech",
category: "interaction",
icon: "volume-2",
timeout: 15000,
retryable: true,
parameterSchema: {
type: "object",
properties: {
text: {
type: "string",
default: "Hello, I am NAO!",
description: "Text to speak",
},
volume: {
type: "number",
minimum: 0.1,
maximum: 1.0,
default: 0.7,
description: "Speech volume (0.1 to 1.0)",
},
},
required: ["text"],
},
naoqi: {
module: "ALTextToSpeech",
method: "say",
parameters: ["text"],
},
},
{
id: "play_animation",
name: "Play Animation",
description: "Play a predefined animation or gesture",
category: "interaction",
icon: "zap",
timeout: 20000,
retryable: true,
parameterSchema: {
type: "object",
properties: {
animation: {
type: "string",
enum: ["Hello", "Goodbye", "Excited", "Thinking"],
default: "Hello",
description: "Animation to play",
},
},
required: ["animation"],
},
naoqi: {
module: "ALAnimationPlayer",
method: "run",
parameters: ["animations/Stand/Gestures/{animation}"],
},
},
{
id: "walk_to_position",
name: "Walk to Position",
description:
"Walk to a specific position relative to current location",
category: "movement",
icon: "footprints",
timeout: 30000,
retryable: true,
parameterSchema: {
type: "object",
properties: {
x: {
type: "number",
minimum: -2.0,
maximum: 2.0,
default: 0.5,
description: "Forward distance in meters",
},
y: {
type: "number",
minimum: -1.0,
maximum: 1.0,
default: 0.0,
description: "Sideways distance in meters (left is positive)",
},
theta: {
type: "number",
minimum: -3.14159,
maximum: 3.14159,
default: 0.0,
description: "Turn angle in radians",
},
},
required: ["x", "y", "theta"],
},
naoqi: {
module: "ALMotion",
method: "walkTo",
parameters: ["x", "y", "theta"],
},
},
],
createdAt: new Date("2024-01-01T00:00:00"),
updatedAt: new Date("2024-01-10T12:00:00"),
},
{
id: "51234567-89ab-cdef-0123-456789abcde3",
robotId: "31234567-89ab-cdef-0123-456789abcde3",
name: "TurtleBot3 Waffle Pi",
version: "2.0.0",
description:
"Extended TurtleBot3 platform with additional sensors and computing power for advanced research applications",
author: "ROBOTIS",
repositoryUrl: "https://repo.hristudio.com",
trustLevel: "official" as const,
status: "active" as const,
configurationSchema: {
type: "object",
properties: {
namespace: { type: "string", default: "turtlebot3" },
topics: {
type: "object",
properties: {
cmd_vel: { type: "string", default: "/cmd_vel" },
odom: { type: "string", default: "/odom" },
scan: { type: "string", default: "/scan" },
camera: { type: "string", default: "/camera/image_raw" },
},
},
},
},
actionDefinitions: [
{
id: "move_velocity",
name: "Set Velocity",
description: "Control the robot's linear and angular velocity",
category: "movement",
icon: "navigation",
timeout: 30000,
retryable: true,
parameterSchema: {
type: "object",
properties: {
linear: {
type: "number",
minimum: -0.26,
maximum: 0.26,
default: 0,
description: "Forward/backward velocity in m/s",
},
angular: {
type: "number",
minimum: -1.82,
maximum: 1.82,
default: 0,
description: "Rotational velocity in rad/s",
},
},
required: ["linear", "angular"],
},
ros2: {
messageType: "geometry_msgs/msg/Twist",
topic: "/cmd_vel",
payloadMapping: {
type: "transform",
transformFn: "transformToTwist",
},
},
},
{
id: "capture_image",
name: "Capture Image",
description: "Capture an image from the robot's camera",
category: "sensors",
icon: "camera",
timeout: 10000,
retryable: true,
parameterSchema: {
type: "object",
properties: {
filename: {
type: "string",
default: "image_{timestamp}.jpg",
description: "Filename for the captured image",
},
quality: {
type: "integer",
minimum: 1,
maximum: 100,
default: 85,
description: "JPEG quality (1-100)",
},
},
required: ["filename"],
},
ros2: {
messageType: "sensor_msgs/msg/Image",
topic: "/camera/image_raw",
payloadMapping: {
type: "transform",
transformFn: "captureAndSaveImage",
},
},
},
{
id: "scan_environment",
name: "Scan Environment",
description:
"Perform a 360-degree scan of the environment using LIDAR",
category: "sensors",
icon: "radar",
timeout: 15000,
retryable: true,
parameterSchema: {
type: "object",
properties: {
duration: {
type: "number",
minimum: 1.0,
maximum: 10.0,
default: 3.0,
description: "Scan duration in seconds",
},
save_data: {
type: "boolean",
default: true,
description: "Save scan data to file",
},
},
required: ["duration"],
},
ros2: {
messageType: "sensor_msgs/msg/LaserScan",
topic: "/scan",
payloadMapping: {
type: "transform",
transformFn: "collectLaserScan",
},
},
},
],
createdAt: new Date("2024-01-01T00:00:00"),
updatedAt: new Date("2024-01-10T12:00:00"),
},
];
await db.insert(schema.plugins).values(plugins);
console.log(`✅ Created ${plugins.length} robot plugins`);
}
async function seedStudyPlugins() {
console.log("🔌 Seeding study plugin installations...");
// Check if study plugins already exist
const existingStudyPlugins = await db.select().from(schema.studyPlugins);
if (existingStudyPlugins.length > 0) {
console.log(
`⚠️ ${existingStudyPlugins.length} study plugin installations already exist, skipping`,
);
return;
}
// Get study IDs from the existing studies
const studies = await db.select().from(schema.studies);
if (studies.length === 0) {
console.log("⚠️ No studies found. Please run basic seeding first.");
return;
}
const studyPlugins = [
{
id: "61234567-89ab-cdef-0123-456789abcde1",
studyId: studies[0]!.id, // First study (navigation)
pluginId: "51234567-89ab-cdef-0123-456789abcde1", // TurtleBot3 Burger
configuration: {
namespace: "navigation_study",
topics: {
cmd_vel: "/navigation_study/cmd_vel",
odom: "/navigation_study/odom",
scan: "/navigation_study/scan",
},
max_speed: 0.15,
safety_distance: 0.3,
},
installedAt: new Date("2024-01-05T10:00:00"),
installedBy: studies[0]!.createdBy,
},
{
id: "61234567-89ab-cdef-0123-456789abcde2",
studyId: studies[1]?.id ?? studies[0]!.id, // Second study (social robots) or fallback
pluginId: "51234567-89ab-cdef-0123-456789abcde2", // NAO Humanoid
configuration: {
ip: "192.168.1.100",
port: 9559,
modules: [
"ALMotion",
"ALTextToSpeech",
"ALAnimationPlayer",
"ALLeds",
"ALSpeechRecognition",
],
language: "English",
speech_speed: 100,
volume: 0.8,
},
installedAt: new Date("2024-01-05T11:00:00"),
installedBy: studies[1]?.createdBy ?? studies[0]!.createdBy,
},
{
id: "61234567-89ab-cdef-0123-456789abcde3",
studyId: studies[0]!.id, // First study also gets Waffle for advanced tasks
pluginId: "51234567-89ab-cdef-0123-456789abcde3", // TurtleBot3 Waffle
configuration: {
namespace: "advanced_navigation",
topics: {
cmd_vel: "/advanced_navigation/cmd_vel",
odom: "/advanced_navigation/odom",
scan: "/advanced_navigation/scan",
camera: "/advanced_navigation/camera/image_raw",
},
max_speed: 0.2,
camera_enabled: true,
lidar_enabled: true,
},
installedAt: new Date("2024-01-05T12:00:00"),
installedBy: studies[0]!.createdBy,
},
];
await db.insert(schema.studyPlugins).values(studyPlugins);
console.log(`✅ Created ${studyPlugins.length} study plugin installations`);
}
async function main() {
try {
console.log("🔌 HRIStudio Plugin System Seeding Started");
console.log("📍 Database:", connectionString.replace(/:[^:]*@/, ":***@"));
await seedRobots();
await seedPluginRepositories();
await seedPlugins();
await seedStudyPlugins();
console.log("✅ Plugin system seeding completed successfully!");
console.log("\n📋 Plugin System Summary:");
console.log(" 🤖 Robots: 3 (TurtleBot3 Burger, NAO, TurtleBot3 Waffle)");
console.log(" 📦 Plugin Repositories: 1 (official HRIStudio repo)");
console.log(" 🔌 Robot Plugins: 3 (with complete action definitions)");
console.log(" 📱 Study Plugin Installations: 3 (active configurations)");
console.log("\n🎯 Plugin Actions Available:");
console.log(
" 📍 TurtleBot3 Burger: 3 actions (movement, navigation, stop)",
);
console.log(" 🤖 NAO Humanoid: 3 actions (speech, animations, walking)");
console.log(" 📊 TurtleBot3 Waffle: 3 actions (movement, camera, LIDAR)");
console.log("\n🧪 Test Plugin Integration:");
console.log(" 1. Navigate to any experiment designer");
console.log(" 2. Check 'Robot' category in block library");
console.log(" 3. Plugin actions should appear alongside core blocks");
console.log(" 4. Actions are configured per study installation");
console.log("\n🚀 Ready to test robot plugin integration!");
} catch (error) {
console.error("❌ Plugin seeding failed:", error);
process.exit(1);
} finally {
await client.end();
}
}
if (require.main === module) {
void main();
}

View File

@@ -8,6 +8,7 @@
*/
import { drizzle } from "drizzle-orm/postgres-js";
import { sql } from "drizzle-orm";
import postgres from "postgres";
import * as schema from "../src/server/db/schema";
@@ -23,17 +24,21 @@ console.log("🌱 Starting HRIStudio database seeding...");
async function clearDatabase() {
console.log("🧹 Clearing existing data...");
// Delete in reverse dependency order
await db.delete(schema.trialEvents);
await db.delete(schema.actions);
await db.delete(schema.steps);
await db.delete(schema.trials);
await db.delete(schema.participants);
await db.delete(schema.experiments);
await db.delete(schema.studyMembers);
await db.delete(schema.studies);
await db.delete(schema.userSystemRoles);
await db.delete(schema.users);
// Delete in reverse dependency order using TRUNCATE for safety
await db.execute(sql`TRUNCATE TABLE hs_trial_event CASCADE`);
await db.execute(sql`TRUNCATE TABLE hs_action CASCADE`);
await db.execute(sql`TRUNCATE TABLE hs_step CASCADE`);
await db.execute(sql`TRUNCATE TABLE hs_trial CASCADE`);
await db.execute(sql`TRUNCATE TABLE hs_participant CASCADE`);
await db.execute(sql`TRUNCATE TABLE hs_experiment CASCADE`);
await db.execute(sql`TRUNCATE TABLE hs_study_member CASCADE`);
await db.execute(sql`TRUNCATE TABLE hs_study CASCADE`);
await db.execute(sql`TRUNCATE TABLE hs_user_system_role CASCADE`);
await db.execute(sql`TRUNCATE TABLE hs_user CASCADE`);
await db.execute(sql`TRUNCATE TABLE hs_robot CASCADE`);
await db.execute(sql`TRUNCATE TABLE hs_plugin_repository CASCADE`);
await db.execute(sql`TRUNCATE TABLE hs_plugin CASCADE`);
await db.execute(sql`TRUNCATE TABLE hs_study_plugin CASCADE`);
console.log("✅ Database cleared");
}
@@ -43,7 +48,15 @@ async function seedUsers() {
const users = [
{
id: "user-admin-1",
id: "01234567-89ab-cdef-0123-456789abcde0",
name: "Sean O'Connor",
email: "sean@soconnor.dev",
emailVerified: new Date(),
institution: "HRIStudio",
activeStudyId: null,
},
{
id: "01234567-89ab-cdef-0123-456789abcde1",
name: "Dr. Sarah Chen",
email: "sarah.chen@university.edu",
emailVerified: new Date(),
@@ -51,7 +64,7 @@ async function seedUsers() {
activeStudyId: null,
},
{
id: "user-researcher-1",
id: "01234567-89ab-cdef-0123-456789abcde2",
name: "Dr. Michael Rodriguez",
email: "m.rodriguez@research.org",
emailVerified: new Date(),
@@ -59,7 +72,7 @@ async function seedUsers() {
activeStudyId: null,
},
{
id: "user-wizard-1",
id: "01234567-89ab-cdef-0123-456789abcde3",
name: "Emma Thompson",
email: "emma.thompson@university.edu",
emailVerified: new Date(),
@@ -67,7 +80,7 @@ async function seedUsers() {
activeStudyId: null,
},
{
id: "user-observer-1",
id: "01234567-89ab-cdef-0123-456789abcde4",
name: "Dr. James Wilson",
email: "james.wilson@university.edu",
emailVerified: new Date(),
@@ -81,28 +94,34 @@ async function seedUsers() {
// Add user roles
const userRoles = [
{
userId: "user-admin-1",
userId: "01234567-89ab-cdef-0123-456789abcde0",
role: "administrator" as const,
assignedAt: new Date(),
assignedBy: "user-admin-1", // Self-assigned for bootstrap
assignedBy: "01234567-89ab-cdef-0123-456789abcde0", // Sean as admin
},
{
userId: "user-researcher-1",
userId: "01234567-89ab-cdef-0123-456789abcde1",
role: "researcher" as const,
assignedAt: new Date(),
assignedBy: "user-admin-1",
assignedBy: "01234567-89ab-cdef-0123-456789abcde0",
},
{
userId: "user-wizard-1",
userId: "01234567-89ab-cdef-0123-456789abcde2",
role: "researcher" as const,
assignedAt: new Date(),
assignedBy: "01234567-89ab-cdef-0123-456789abcde0",
},
{
userId: "01234567-89ab-cdef-0123-456789abcde3",
role: "wizard" as const,
assignedAt: new Date(),
assignedBy: "user-admin-1",
assignedBy: "01234567-89ab-cdef-0123-456789abcde0",
},
{
userId: "user-observer-1",
userId: "01234567-89ab-cdef-0123-456789abcde4",
role: "observer" as const,
assignedAt: new Date(),
assignedBy: "user-admin-1",
assignedBy: "01234567-89ab-cdef-0123-456789abcde0",
},
];
@@ -116,14 +135,14 @@ async function seedStudies() {
const studies = [
{
id: "study-hri-navigation",
id: "11234567-89ab-cdef-0123-456789abcde1",
name: "Robot Navigation Assistance Study",
description:
"Investigating how robots can effectively assist humans with indoor navigation tasks using multimodal interaction.",
institution: "MIT Computer Science",
irbProtocolNumber: "IRB-2024-001",
status: "active" as const,
createdBy: "user-researcher-1",
createdBy: "01234567-89ab-cdef-0123-456789abcde0",
metadata: {
duration: "6 months",
targetParticipants: 50,
@@ -132,14 +151,14 @@ async function seedStudies() {
},
},
{
id: "study-social-robots",
id: "11234567-89ab-cdef-0123-456789abcde2",
name: "Social Robot Interaction Patterns",
description:
"Exploring how different personality traits in robots affect human-robot collaboration in workplace settings.",
institution: "Stanford HCI Lab",
irbProtocolNumber: "IRB-2024-002",
status: "draft" as const,
createdBy: "user-researcher-1",
createdBy: "01234567-89ab-cdef-0123-456789abcde0",
metadata: {
duration: "4 months",
targetParticipants: 30,
@@ -148,18 +167,18 @@ async function seedStudies() {
},
},
{
id: "study-elderly-assistance",
name: "Elderly Care Assistant Robot Study",
id: "11234567-89ab-cdef-0123-456789abcde3",
name: "Assistive Robotics for Elderly Care",
description:
"Evaluating the effectiveness of companion robots in assisted living facilities for elderly residents.",
institution: "MIT Computer Science",
"Evaluating the effectiveness of companion robots in assisted living facilities for improving quality of life.",
institution: "University of Washington",
irbProtocolNumber: "IRB-2024-003",
status: "completed" as const,
createdBy: "user-admin-1",
createdBy: "01234567-89ab-cdef-0123-456789abcde0",
metadata: {
duration: "8 months",
targetParticipants: 25,
robotPlatform: "NAO",
duration: "12 months",
targetParticipants: 40,
robotPlatform: "Companion Robot",
environment: "Assisted living facility",
},
},
@@ -169,53 +188,77 @@ async function seedStudies() {
// Add study members
const studyMembers = [
// Sean as admin/owner of all studies
{
studyId: "11234567-89ab-cdef-0123-456789abcde1",
userId: "01234567-89ab-cdef-0123-456789abcde0",
role: "owner" as const,
joinedAt: new Date(),
invitedBy: null,
},
// Navigation Study Team
{
studyId: "study-hri-navigation",
userId: "user-researcher-1",
role: "owner" as const,
joinedAt: new Date(),
invitedBy: null,
},
{
studyId: "study-hri-navigation",
userId: "user-wizard-1",
role: "wizard" as const,
joinedAt: new Date(),
invitedBy: "user-researcher-1",
},
{
studyId: "study-hri-navigation",
userId: "user-observer-1",
role: "observer" as const,
joinedAt: new Date(),
invitedBy: "user-researcher-1",
},
// Social Robots Study Team
{
studyId: "study-social-robots",
userId: "user-researcher-1",
role: "owner" as const,
joinedAt: new Date(),
invitedBy: null,
},
{
studyId: "study-social-robots",
userId: "user-admin-1",
studyId: "11234567-89ab-cdef-0123-456789abcde1",
userId: "01234567-89ab-cdef-0123-456789abcde2",
role: "researcher" as const,
joinedAt: new Date(),
invitedBy: "user-researcher-1",
invitedBy: "01234567-89ab-cdef-0123-456789abcde0",
},
{
studyId: "11234567-89ab-cdef-0123-456789abcde1",
userId: "01234567-89ab-cdef-0123-456789abcde3",
role: "wizard" as const,
joinedAt: new Date(),
invitedBy: "01234567-89ab-cdef-0123-456789abcde0",
},
{
studyId: "11234567-89ab-cdef-0123-456789abcde1",
userId: "01234567-89ab-cdef-0123-456789abcde4",
role: "observer" as const,
joinedAt: new Date(),
invitedBy: "01234567-89ab-cdef-0123-456789abcde0",
},
// Elderly Care Study Team
// Sean as admin/owner of Social Robots Study
{
studyId: "study-elderly-assistance",
userId: "user-admin-1",
studyId: "11234567-89ab-cdef-0123-456789abcde2",
userId: "01234567-89ab-cdef-0123-456789abcde0",
role: "owner" as const,
joinedAt: new Date(),
invitedBy: null,
},
// Social Robots Study Team
{
studyId: "11234567-89ab-cdef-0123-456789abcde2",
userId: "01234567-89ab-cdef-0123-456789abcde2",
role: "researcher" as const,
joinedAt: new Date(),
invitedBy: "01234567-89ab-cdef-0123-456789abcde0",
},
{
studyId: "11234567-89ab-cdef-0123-456789abcde2",
userId: "01234567-89ab-cdef-0123-456789abcde1",
role: "researcher" as const,
joinedAt: new Date(),
invitedBy: "01234567-89ab-cdef-0123-456789abcde0",
},
// Sean as admin/owner of Elderly Care Study
{
studyId: "11234567-89ab-cdef-0123-456789abcde3",
userId: "01234567-89ab-cdef-0123-456789abcde0",
role: "owner" as const,
joinedAt: new Date(),
invitedBy: null,
},
// Elderly Care Study Team
{
studyId: "11234567-89ab-cdef-0123-456789abcde3",
userId: "01234567-89ab-cdef-0123-456789abcde1",
role: "researcher" as const,
joinedAt: new Date(),
invitedBy: "01234567-89ab-cdef-0123-456789abcde0",
},
];
await db.insert(schema.studyMembers).values(studyMembers);
@@ -938,6 +981,54 @@ async function seedTrialEvents() {
console.log(`✅ Created ${trialEvents.length} trial events`);
}
async function seedRobots() {
console.log("🤖 Seeding robots...");
const robots = [
{
id: "31234567-89ab-cdef-0123-456789abcde1",
name: "TurtleBot3 Burger",
manufacturer: "ROBOTIS",
model: "TurtleBot3 Burger",
description:
"A compact, affordable, programmable, ROS2-based mobile robot for education and research",
capabilities: [
"differential_drive",
"lidar",
"imu",
"odometry",
"autonomous_navigation",
],
communicationProtocol: "ros2" as const,
createdAt: new Date("2024-01-01T00:00:00"),
updatedAt: new Date("2024-01-01T00:00:00"),
},
{
id: "31234567-89ab-cdef-0123-456789abcde2",
name: "NAO Humanoid Robot",
manufacturer: "SoftBank Robotics",
model: "NAO v6",
description:
"Autonomous, programmable humanoid robot designed for education, research, and human-robot interaction studies",
capabilities: [
"bipedal_walking",
"speech_synthesis",
"speech_recognition",
"computer_vision",
"gestures",
"led_control",
"touch_sensors",
],
communicationProtocol: "custom" as const,
createdAt: new Date("2024-01-01T00:00:00"),
updatedAt: new Date("2024-01-01T00:00:00"),
},
];
await db.insert(schema.robots).values(robots);
console.log(`✅ Created ${robots.length} robots`);
}
async function main() {
try {
console.log("🚀 HRIStudio Database Seeding Started");
@@ -946,6 +1037,7 @@ async function main() {
await clearDatabase();
await seedUsers();
await seedStudies();
await seedRobots();
await seedExperiments();
await seedStepsAndActions();
await seedParticipants();
@@ -956,6 +1048,7 @@ async function main() {
console.log("\n📋 Summary:");
console.log(" 👥 Users: 4 (admin, researcher, wizard, observer)");
console.log(" 📚 Studies: 3 (navigation, social robots, elderly care)");
console.log(" 🤖 Robots: 2 (TurtleBot3, NAO)");
console.log(" 🧪 Experiments: 4 (with comprehensive test scenarios)");
console.log(" 📋 Steps: 10 (covering all experiment types)");
console.log(" ⚡ Actions: 12 (detailed robot and wizard actions)");

323
scripts/test-seed-data.ts Normal file
View File

@@ -0,0 +1,323 @@
#!/usr/bin/env tsx
/**
* Test script to validate seed data structure
* Ensures all user relationships and study memberships are correct
*/
interface User {
id: string;
name: string;
email: string;
institution: string;
}
interface UserRole {
userId: string;
role: "administrator" | "researcher" | "wizard" | "observer";
assignedBy: string;
}
interface Study {
id: string;
name: string;
createdBy: string;
}
interface StudyMember {
studyId: string;
userId: string;
role: "owner" | "researcher" | "wizard" | "observer";
invitedBy: string | null;
}
function validateSeedData() {
console.log("🧪 Testing seed data structure...\n");
// Users data
const users: User[] = [
{
id: "01234567-89ab-cdef-0123-456789abcde0",
name: "Sean O'Connor",
email: "sean@soconnor.dev",
institution: "HRIStudio",
},
{
id: "01234567-89ab-cdef-0123-456789abcde1",
name: "Dr. Sarah Chen",
email: "sarah.chen@university.edu",
institution: "MIT Computer Science",
},
{
id: "01234567-89ab-cdef-0123-456789abcde2",
name: "Dr. Michael Rodriguez",
email: "m.rodriguez@research.org",
institution: "Stanford HCI Lab",
},
{
id: "01234567-89ab-cdef-0123-456789abcde3",
name: "Emma Thompson",
email: "emma.thompson@university.edu",
institution: "MIT Computer Science",
},
{
id: "01234567-89ab-cdef-0123-456789abcde4",
name: "Dr. James Wilson",
email: "james.wilson@university.edu",
institution: "MIT Computer Science",
},
];
// User roles
const userRoles: UserRole[] = [
{
userId: "01234567-89ab-cdef-0123-456789abcde0",
role: "administrator",
assignedBy: "01234567-89ab-cdef-0123-456789abcde0",
},
{
userId: "01234567-89ab-cdef-0123-456789abcde1",
role: "researcher",
assignedBy: "01234567-89ab-cdef-0123-456789abcde0",
},
{
userId: "01234567-89ab-cdef-0123-456789abcde2",
role: "researcher",
assignedBy: "01234567-89ab-cdef-0123-456789abcde0",
},
{
userId: "01234567-89ab-cdef-0123-456789abcde3",
role: "wizard",
assignedBy: "01234567-89ab-cdef-0123-456789abcde0",
},
{
userId: "01234567-89ab-cdef-0123-456789abcde4",
role: "observer",
assignedBy: "01234567-89ab-cdef-0123-456789abcde0",
},
];
// Studies
const studies: Study[] = [
{
id: "11234567-89ab-cdef-0123-456789abcde1",
name: "Robot Navigation Assistance Study",
createdBy: "01234567-89ab-cdef-0123-456789abcde0",
},
{
id: "11234567-89ab-cdef-0123-456789abcde2",
name: "Social Robots in Healthcare Study",
createdBy: "01234567-89ab-cdef-0123-456789abcde0",
},
{
id: "11234567-89ab-cdef-0123-456789abcde3",
name: "Elderly Care Robot Interaction Study",
createdBy: "01234567-89ab-cdef-0123-456789abcde0",
},
];
// Study members
const studyMembers: StudyMember[] = [
// Sean as owner of all studies
{
studyId: "11234567-89ab-cdef-0123-456789abcde1",
userId: "01234567-89ab-cdef-0123-456789abcde0",
role: "owner",
invitedBy: null,
},
{
studyId: "11234567-89ab-cdef-0123-456789abcde2",
userId: "01234567-89ab-cdef-0123-456789abcde0",
role: "owner",
invitedBy: null,
},
{
studyId: "11234567-89ab-cdef-0123-456789abcde3",
userId: "01234567-89ab-cdef-0123-456789abcde0",
role: "owner",
invitedBy: null,
},
// Other team members
{
studyId: "11234567-89ab-cdef-0123-456789abcde1",
userId: "01234567-89ab-cdef-0123-456789abcde2",
role: "researcher",
invitedBy: "01234567-89ab-cdef-0123-456789abcde0",
},
{
studyId: "11234567-89ab-cdef-0123-456789abcde1",
userId: "01234567-89ab-cdef-0123-456789abcde3",
role: "wizard",
invitedBy: "01234567-89ab-cdef-0123-456789abcde0",
},
{
studyId: "11234567-89ab-cdef-0123-456789abcde2",
userId: "01234567-89ab-cdef-0123-456789abcde2",
role: "researcher",
invitedBy: "01234567-89ab-cdef-0123-456789abcde0",
},
{
studyId: "11234567-89ab-cdef-0123-456789abcde3",
userId: "01234567-89ab-cdef-0123-456789abcde1",
role: "researcher",
invitedBy: "01234567-89ab-cdef-0123-456789abcde0",
},
];
let errors = 0;
console.log("👥 Validating users...");
console.log(` Users: ${users.length}`);
// Check for Sean as admin
const seanUser = users.find((u) => u.email === "sean@soconnor.dev");
if (seanUser) {
console.log(` ✅ Sean found: ${seanUser.name} (${seanUser.email})`);
} else {
console.error(` ❌ Sean not found as user`);
errors++;
}
console.log("\n🔐 Validating user roles...");
console.log(` User roles: ${userRoles.length}`);
// Check Sean's admin role
const seanRole = userRoles.find(
(r) => r.userId === "01234567-89ab-cdef-0123-456789abcde0",
);
if (seanRole && seanRole.role === "administrator") {
console.log(` ✅ Sean has administrator role`);
} else {
console.error(` ❌ Sean missing administrator role`);
errors++;
}
// Check all roles are assigned by Sean
const rolesAssignedBySean = userRoles.filter(
(r) => r.assignedBy === "01234567-89ab-cdef-0123-456789abcde0",
);
console.log(
`${rolesAssignedBySean.length}/${userRoles.length} roles assigned by Sean`,
);
console.log("\n📚 Validating studies...");
console.log(` Studies: ${studies.length}`);
// Check all studies created by Sean
const studiesCreatedBySean = studies.filter(
(s) => s.createdBy === "01234567-89ab-cdef-0123-456789abcde0",
);
if (studiesCreatedBySean.length === studies.length) {
console.log(` ✅ All ${studies.length} studies created by Sean`);
} else {
console.error(
` ❌ Only ${studiesCreatedBySean.length}/${studies.length} studies created by Sean`,
);
errors++;
}
console.log("\n👨💼 Validating study memberships...");
console.log(` Study memberships: ${studyMembers.length}`);
// Check Sean is owner of all studies
const seanOwnerships = studyMembers.filter(
(m) =>
m.userId === "01234567-89ab-cdef-0123-456789abcde0" && m.role === "owner",
);
if (seanOwnerships.length === studies.length) {
console.log(` ✅ Sean is owner of all ${studies.length} studies`);
} else {
console.error(
` ❌ Sean only owns ${seanOwnerships.length}/${studies.length} studies`,
);
errors++;
}
// Check invitation chain
const membersInvitedBySean = studyMembers.filter(
(m) => m.invitedBy === "01234567-89ab-cdef-0123-456789abcde0",
);
console.log(`${membersInvitedBySean.length} members invited by Sean`);
// Validate all user references exist
console.log("\n🔗 Validating references...");
const userIds = new Set(users.map((u) => u.id));
for (const role of userRoles) {
if (!userIds.has(role.userId)) {
console.error(` ❌ Invalid user reference in role: ${role.userId}`);
errors++;
}
if (!userIds.has(role.assignedBy)) {
console.error(
` ❌ Invalid assignedBy reference in role: ${role.assignedBy}`,
);
errors++;
}
}
for (const study of studies) {
if (!userIds.has(study.createdBy)) {
console.error(
` ❌ Invalid createdBy reference in study: ${study.createdBy}`,
);
errors++;
}
}
const studyIds = new Set(studies.map((s) => s.id));
for (const member of studyMembers) {
if (!studyIds.has(member.studyId)) {
console.error(
` ❌ Invalid study reference in membership: ${member.studyId}`,
);
errors++;
}
if (!userIds.has(member.userId)) {
console.error(
` ❌ Invalid user reference in membership: ${member.userId}`,
);
errors++;
}
if (member.invitedBy && !userIds.has(member.invitedBy)) {
console.error(
` ❌ Invalid invitedBy reference in membership: ${member.invitedBy}`,
);
errors++;
}
}
if (errors === 0) {
console.log(" ✅ All references are valid");
}
// Summary
console.log(`\n📊 Validation Summary:`);
console.log(` Users: ${users.length}`);
console.log(` User roles: ${userRoles.length}`);
console.log(` Studies: ${studies.length}`);
console.log(` Study memberships: ${studyMembers.length}`);
console.log(` Errors: ${errors}`);
if (errors === 0) {
console.log(`\n🎉 All validations passed! Seed data structure is correct.`);
console.log(` Sean (sean@soconnor.dev) is admin of everything:`);
console.log(` • System administrator role`);
console.log(` • Owner of all ${studies.length} studies`);
console.log(` • Assigned all user roles`);
console.log(` • Invited all study members`);
process.exit(0);
} else {
console.log(`\n❌ Validation failed with ${errors} error(s).`);
process.exit(1);
}
}
// Run the validation
if (import.meta.url === `file://${process.argv[1]}`) {
validateSeedData();
}
export { validateSeedData };