mirror of
https://github.com/soconnor0919/hristudio.git
synced 2025-12-12 23:24:44 -05:00
- 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
304 lines
7.7 KiB
Markdown
304 lines
7.7 KiB
Markdown
# 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
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
const study = await db.query.studies.findFirst({
|
|
where: eq(studies.id, studyId),
|
|
with: {
|
|
creator: true,
|
|
members: {
|
|
with: {
|
|
user: true,
|
|
},
|
|
},
|
|
participants: true,
|
|
},
|
|
});
|
|
```
|
|
|
|
### Mutations
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
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 |