Files
hristudio/docs/data-layer.md
Sean O'Connor ab08c1b724 feat: Enhance plugin store and experiment design infrastructure
- Add plugin store system with dynamic loading of robot actions
- Implement plugin store API routes and database schema
- Update experiment designer to support plugin-based actions
- Refactor environment configuration and sidebar navigation
- Improve authentication session handling with additional user details
- Update Tailwind CSS configuration and global styles
- Remove deprecated files and consolidate project structure
2025-02-28 11:10:56 -05:00

7.7 KiB

Data Layer

Overview

HRIStudio's data layer is built on PostgreSQL using Drizzle ORM for type-safe database operations. The system implements a comprehensive schema design that supports studies, experiments, participants, and plugin management while maintaining data integrity and proper relationships.

Database Schema

Core Tables

Studies

export const studies = createTable("study", {
  id: integer("id").primaryKey().notNull().generatedAlwaysAsIdentity(),
  title: varchar("title", { length: 256 }).notNull(),
  description: text("description"),
  createdById: varchar("created_by", { length: 255 })
    .notNull()
    .references(() => users.id),
  createdAt: timestamp("created_at").defaultNow().notNull(),
  updatedAt: timestamp("updated_at").defaultNow().notNull(),
});

export const studyMembers = createTable("study_member", {
  id: integer("id").primaryKey().notNull().generatedAlwaysAsIdentity(),
  studyId: integer("study_id")
    .notNull()
    .references(() => studies.id, { onDelete: "cascade" }),
  userId: varchar("user_id", { length: 255 })
    .notNull()
    .references(() => users.id, { onDelete: "cascade" }),
  role: studyRoleEnum("role").notNull(),
  createdAt: timestamp("created_at").defaultNow().notNull(),
});

Experiments

export const experiments = createTable("experiment", {
  id: integer("id").primaryKey().notNull().generatedAlwaysAsIdentity(),
  studyId: integer("study_id")
    .notNull()
    .references(() => studies.id, { onDelete: "cascade" }),
  title: varchar("title", { length: 256 }).notNull(),
  description: text("description"),
  version: integer("version").notNull().default(1),
  status: experimentStatusEnum("status").notNull().default("draft"),
  steps: jsonb("steps").$type<Step[]>().default([]),
  createdById: varchar("created_by", { length: 255 })
    .notNull()
    .references(() => users.id),
  createdAt: timestamp("created_at").defaultNow().notNull(),
  updatedAt: timestamp("updated_at").defaultNow().notNull(),
});

Participants

export const participants = createTable("participant", {
  id: integer("id").primaryKey().notNull().generatedAlwaysAsIdentity(),
  studyId: integer("study_id")
    .notNull()
    .references(() => studies.id, { onDelete: "cascade" }),
  identifier: varchar("identifier", { length: 256 }),
  email: varchar("email", { length: 256 }),
  firstName: varchar("first_name", { length: 256 }),
  lastName: varchar("last_name", { length: 256 }),
  notes: text("notes"),
  status: participantStatusEnum("status").notNull().default("active"),
  createdAt: timestamp("created_at").defaultNow().notNull(),
  updatedAt: timestamp("updated_at").defaultNow().notNull(),
});

Plugin Store Tables

export const pluginRepositories = createTable("plugin_repository", {
  id: varchar("id", { length: 255 }).primaryKey(),
  name: varchar("name", { length: 255 }).notNull(),
  description: text("description"),
  url: varchar("url", { length: 255 }).notNull(),
  official: boolean("official").default(false).notNull(),
  author: jsonb("author").notNull().$type<{
    name: string;
    email?: string;
    url?: string;
    organization?: string;
  }>(),
  maintainers: jsonb("maintainers").$type<Array<{
    name: string;
    email?: string;
    url?: string;
  }>>(),
  compatibility: jsonb("compatibility").notNull().$type<{
    hristudio: {
      min: string;
      recommended?: string;
    };
    ros2?: {
      distributions: string[];
      recommended?: string;
    };
  }>(),
  stats: jsonb("stats").$type<{
    downloads: number;
    stars: number;
    plugins: number;
  }>(),
  addedById: varchar("added_by", { length: 255 })
    .references(() => users.id),
  createdAt: timestamp("created_at").defaultNow().notNull(),
  updatedAt: timestamp("updated_at").defaultNow().notNull(),
});

Relations

Study Relations

export const studiesRelations = relations(studies, ({ one, many }) => ({
  creator: one(users, {
    fields: [studies.createdById],
    references: [users.id],
  }),
  members: many(studyMembers),
  participants: many(participants),
  experiments: many(experiments),
}));

export const studyMembersRelations = relations(studyMembers, ({ one }) => ({
  study: one(studies, {
    fields: [studyMembers.studyId],
    references: [studies.id],
  }),
  user: one(users, {
    fields: [studyMembers.userId],
    references: [users.id],
  }),
}));

Data Validation

Zod Schemas

const studySchema = z.object({
  title: z.string().min(1, "Title is required").max(256, "Title is too long"),
  description: z.string().optional(),
});

const participantSchema = z.object({
  identifier: z.string().optional(),
  email: z.string().email().optional(),
  firstName: z.string().optional(),
  lastName: z.string().optional(),
  notes: z.string().optional(),
  status: z.enum(["active", "inactive", "completed", "withdrawn"]),
});

const experimentSchema = z.object({
  title: z.string().min(1, "Title is required").max(256, "Title is too long"),
  description: z.string().optional(),
  steps: z.array(stepSchema),
  status: z.enum(["draft", "active", "archived"]),
});

Query Building

Type-Safe Queries

const study = await db.query.studies.findFirst({
  where: eq(studies.id, studyId),
  with: {
    creator: true,
    members: {
      with: {
        user: true,
      },
    },
    participants: true,
  },
});

Mutations

const newStudy = await db.transaction(async (tx) => {
  const [study] = await tx.insert(studies).values({
    title,
    description,
    createdById: userId,
  }).returning();

  await tx.insert(studyMembers).values({
    studyId: study.id,
    userId,
    role: "OWNER",
  });

  return study;
});

Error Handling

Database Errors

try {
  await db.insert(participants).values({
    studyId,
    identifier,
    email,
  });
} catch (error) {
  if (error instanceof PostgresError) {
    if (error.code === "23505") { // Unique violation
      throw new TRPCError({
        code: "CONFLICT",
        message: "A participant with this identifier already exists",
      });
    }
  }
  throw error;
}

Migrations

Migration Structure

import { sql } from "drizzle-orm";
import { createTable, integer, text, timestamp, varchar } from "drizzle-orm/pg-core";

export async function up(db: Database) {
  await db.schema.createTable("study")
    .addColumn("id", "serial", (col) => col.primaryKey())
    .addColumn("title", "varchar(256)", (col) => col.notNull())
    .addColumn("description", "text")
    .addColumn("created_by", "varchar(255)", (col) => 
      col.notNull().references("user.id"))
    .addColumn("created_at", "timestamp", (col) => 
      col.notNull().defaultTo(sql`CURRENT_TIMESTAMP`))
    .addColumn("updated_at", "timestamp");
}

export async function down(db: Database) {
  await db.schema.dropTable("study");
}

Best Practices

  1. Type Safety:

    • Use Drizzle's type inference
    • Define explicit types for complex queries
    • Validate input with Zod schemas
  2. Performance:

    • Use appropriate indexes
    • Optimize complex queries
    • Implement caching where needed
  3. Data Integrity:

    • Use transactions for related operations
    • Implement proper cascading
    • Validate data before insertion
  4. Security:

    • Sanitize user input
    • Implement proper access control
    • Use parameterized queries

Future Enhancements

  1. Query Optimization:

    • Query caching
    • Materialized views
    • Query analysis tools
  2. Data Management:

    • Data archiving
    • Backup strategies
    • Data export tools
  3. Schema Evolution:

    • Zero-downtime migrations
    • Schema versioning
    • Backward compatibility
  4. Monitoring:

    • Query performance metrics
    • Error tracking
    • Usage analytics