feat(api): add activities + work_sessions domain schema and shared contracts
This commit is contained in:
26
apps/api/drizzle/0001_common_leopardon.sql
Normal file
26
apps/api/drizzle/0001_common_leopardon.sql
Normal file
@@ -0,0 +1,26 @@
|
||||
CREATE TABLE `activities` (
|
||||
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
`name` text NOT NULL,
|
||||
`insole_types` text DEFAULT '["Kurk","Berk","3D"]' NOT NULL,
|
||||
`created_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `work_sessions` (
|
||||
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
`user_id` text NOT NULL,
|
||||
`activity_id` integer NOT NULL,
|
||||
`insole_type` text,
|
||||
`pair_count` integer DEFAULT 2 NOT NULL,
|
||||
`start_time` integer NOT NULL,
|
||||
`end_time` integer,
|
||||
`duration_seconds` integer,
|
||||
`status` text DEFAULT 'active' NOT NULL,
|
||||
`source` text DEFAULT 'app' NOT NULL,
|
||||
`notes` text,
|
||||
`created_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL,
|
||||
FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade,
|
||||
FOREIGN KEY (`activity_id`) REFERENCES `activities`(`id`) ON UPDATE no action ON DELETE no action
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE INDEX `work_sessions_userId_idx` ON `work_sessions` (`user_id`);--> statement-breakpoint
|
||||
CREATE INDEX `work_sessions_startTime_idx` ON `work_sessions` (`start_time`);
|
||||
550
apps/api/drizzle/meta/0001_snapshot.json
Normal file
550
apps/api/drizzle/meta/0001_snapshot.json
Normal file
@@ -0,0 +1,550 @@
|
||||
{
|
||||
"version": "6",
|
||||
"dialect": "sqlite",
|
||||
"id": "a878be91-939d-44a7-8aa7-546bb6ff8b6f",
|
||||
"prevId": "e8545186-09fa-4515-bca1-891ed8364a07",
|
||||
"tables": {
|
||||
"account": {
|
||||
"name": "account",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"account_id": {
|
||||
"name": "account_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"provider_id": {
|
||||
"name": "provider_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"access_token": {
|
||||
"name": "access_token",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"refresh_token": {
|
||||
"name": "refresh_token",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"id_token": {
|
||||
"name": "id_token",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"access_token_expires_at": {
|
||||
"name": "access_token_expires_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"refresh_token_expires_at": {
|
||||
"name": "refresh_token_expires_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"scope": {
|
||||
"name": "scope",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"password": {
|
||||
"name": "password",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(cast(unixepoch('subsecond') * 1000 as integer))"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"account_userId_idx": {
|
||||
"name": "account_userId_idx",
|
||||
"columns": [
|
||||
"user_id"
|
||||
],
|
||||
"isUnique": false
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"account_user_id_user_id_fk": {
|
||||
"name": "account_user_id_user_id_fk",
|
||||
"tableFrom": "account",
|
||||
"tableTo": "user",
|
||||
"columnsFrom": [
|
||||
"user_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"activities": {
|
||||
"name": "activities",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"insole_types": {
|
||||
"name": "insole_types",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "'[\"Kurk\",\"Berk\",\"3D\"]'"
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(cast(unixepoch('subsecond') * 1000 as integer))"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"session": {
|
||||
"name": "session",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"expires_at": {
|
||||
"name": "expires_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"token": {
|
||||
"name": "token",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(cast(unixepoch('subsecond') * 1000 as integer))"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"ip_address": {
|
||||
"name": "ip_address",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"user_agent": {
|
||||
"name": "user_agent",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"session_token_unique": {
|
||||
"name": "session_token_unique",
|
||||
"columns": [
|
||||
"token"
|
||||
],
|
||||
"isUnique": true
|
||||
},
|
||||
"session_userId_idx": {
|
||||
"name": "session_userId_idx",
|
||||
"columns": [
|
||||
"user_id"
|
||||
],
|
||||
"isUnique": false
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"session_user_id_user_id_fk": {
|
||||
"name": "session_user_id_user_id_fk",
|
||||
"tableFrom": "session",
|
||||
"tableTo": "user",
|
||||
"columnsFrom": [
|
||||
"user_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"user": {
|
||||
"name": "user",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"email": {
|
||||
"name": "email",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"email_verified": {
|
||||
"name": "email_verified",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": false
|
||||
},
|
||||
"image": {
|
||||
"name": "image",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(cast(unixepoch('subsecond') * 1000 as integer))"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(cast(unixepoch('subsecond') * 1000 as integer))"
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"user_email_unique": {
|
||||
"name": "user_email_unique",
|
||||
"columns": [
|
||||
"email"
|
||||
],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"verification": {
|
||||
"name": "verification",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"identifier": {
|
||||
"name": "identifier",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"value": {
|
||||
"name": "value",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"expires_at": {
|
||||
"name": "expires_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(cast(unixepoch('subsecond') * 1000 as integer))"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(cast(unixepoch('subsecond') * 1000 as integer))"
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"verification_identifier_idx": {
|
||||
"name": "verification_identifier_idx",
|
||||
"columns": [
|
||||
"identifier"
|
||||
],
|
||||
"isUnique": false
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"work_sessions": {
|
||||
"name": "work_sessions",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"activity_id": {
|
||||
"name": "activity_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"insole_type": {
|
||||
"name": "insole_type",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"pair_count": {
|
||||
"name": "pair_count",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": 2
|
||||
},
|
||||
"start_time": {
|
||||
"name": "start_time",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"end_time": {
|
||||
"name": "end_time",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"duration_seconds": {
|
||||
"name": "duration_seconds",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"status": {
|
||||
"name": "status",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "'active'"
|
||||
},
|
||||
"source": {
|
||||
"name": "source",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "'app'"
|
||||
},
|
||||
"notes": {
|
||||
"name": "notes",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(cast(unixepoch('subsecond') * 1000 as integer))"
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"work_sessions_userId_idx": {
|
||||
"name": "work_sessions_userId_idx",
|
||||
"columns": [
|
||||
"user_id"
|
||||
],
|
||||
"isUnique": false
|
||||
},
|
||||
"work_sessions_startTime_idx": {
|
||||
"name": "work_sessions_startTime_idx",
|
||||
"columns": [
|
||||
"start_time"
|
||||
],
|
||||
"isUnique": false
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"work_sessions_user_id_user_id_fk": {
|
||||
"name": "work_sessions_user_id_user_id_fk",
|
||||
"tableFrom": "work_sessions",
|
||||
"tableTo": "user",
|
||||
"columnsFrom": [
|
||||
"user_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
"work_sessions_activity_id_activities_id_fk": {
|
||||
"name": "work_sessions_activity_id_activities_id_fk",
|
||||
"tableFrom": "work_sessions",
|
||||
"tableTo": "activities",
|
||||
"columnsFrom": [
|
||||
"activity_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
}
|
||||
},
|
||||
"views": {},
|
||||
"enums": {},
|
||||
"_meta": {
|
||||
"schemas": {},
|
||||
"tables": {},
|
||||
"columns": {}
|
||||
},
|
||||
"internal": {
|
||||
"indexes": {}
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,13 @@
|
||||
"when": 1781697895207,
|
||||
"tag": "0000_stiff_captain_britain",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 1,
|
||||
"version": "6",
|
||||
"when": 1781702878767,
|
||||
"tag": "0001_common_leopardon",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -109,3 +109,45 @@ export const accountRelations = relations(account, ({ one }) => ({
|
||||
references: [user.id],
|
||||
}),
|
||||
}));
|
||||
|
||||
// ---- SoleLog domain tables (Phase 1) ----
|
||||
export const activities = sqliteTable('activities', {
|
||||
id: integer('id').primaryKey({ autoIncrement: true }),
|
||||
name: text('name').notNull(),
|
||||
// subset of 'Kurk' | 'Berk' | '3D' — stored as a JSON string by libsql.
|
||||
insoleTypes: text('insole_types', { mode: 'json' })
|
||||
.$type<string[]>()
|
||||
.notNull()
|
||||
.default(['Kurk', 'Berk', '3D']),
|
||||
createdAt: integer('created_at', { mode: 'timestamp_ms' })
|
||||
.default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`)
|
||||
.notNull(),
|
||||
});
|
||||
|
||||
export const workSessions = sqliteTable(
|
||||
'work_sessions',
|
||||
{
|
||||
id: integer('id').primaryKey({ autoIncrement: true }),
|
||||
userId: text('user_id')
|
||||
.notNull()
|
||||
.references(() => user.id, { onDelete: 'cascade' }),
|
||||
activityId: integer('activity_id')
|
||||
.notNull()
|
||||
.references(() => activities.id),
|
||||
insoleType: text('insole_type'),
|
||||
pairCount: integer('pair_count').notNull().default(2),
|
||||
startTime: integer('start_time', { mode: 'timestamp_ms' }).notNull(),
|
||||
endTime: integer('end_time', { mode: 'timestamp_ms' }), // null = active
|
||||
durationSeconds: integer('duration_seconds'),
|
||||
status: text('status').notNull().default('active'), // 'active' | 'completed' | 'discarded'
|
||||
source: text('source').notNull().default('app'), // 'app' | 'manual'
|
||||
notes: text('notes'),
|
||||
createdAt: integer('created_at', { mode: 'timestamp_ms' })
|
||||
.default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`)
|
||||
.notNull(),
|
||||
},
|
||||
(table) => ({
|
||||
workSessionsUserIdIdx: index('work_sessions_userId_idx').on(table.userId),
|
||||
workSessionsStartTimeIdx: index('work_sessions_startTime_idx').on(table.startTime),
|
||||
})
|
||||
);
|
||||
|
||||
60
apps/api/test/schema.test.ts
Normal file
60
apps/api/test/schema.test.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { db } from '../src/db/client';
|
||||
import { activities, workSessions, user } from '../src/db/schema';
|
||||
import { createApp } from '../src/app';
|
||||
|
||||
const json = { 'content-type': 'application/json' };
|
||||
|
||||
describe('domain schema', () => {
|
||||
it('creates and reads back an activity with a json insole_types array', async () => {
|
||||
const [inserted] = await db
|
||||
.insert(activities)
|
||||
.values({ name: 'Frezen', insoleTypes: ['Kurk', 'Berk'] })
|
||||
.returning();
|
||||
|
||||
const [row] = await db.select().from(activities).where(eq(activities.id, inserted.id));
|
||||
|
||||
expect(row.name).toBe('Frezen');
|
||||
expect(row.insoleTypes).toEqual(['Kurk', 'Berk']);
|
||||
});
|
||||
|
||||
it('defaults a work_sessions row to status=active, source=app, pair_count=2, null end_time', async () => {
|
||||
const app = createApp();
|
||||
const creds = {
|
||||
email: 'schema-user@example.com',
|
||||
password: 'sterk-wachtwoord-123',
|
||||
name: 'Schema User',
|
||||
};
|
||||
await app.request('/api/auth/sign-up/email', {
|
||||
method: 'POST',
|
||||
headers: json,
|
||||
body: JSON.stringify(creds),
|
||||
});
|
||||
|
||||
const [createdUser] = await db.select().from(user).where(eq(user.email, creds.email));
|
||||
expect(createdUser).toBeTruthy();
|
||||
|
||||
const [activity] = await db
|
||||
.insert(activities)
|
||||
.values({ name: 'Slijpen', insoleTypes: ['Kurk', 'Berk', '3D'] })
|
||||
.returning();
|
||||
|
||||
const [inserted] = await db
|
||||
.insert(workSessions)
|
||||
.values({
|
||||
userId: createdUser.id,
|
||||
activityId: activity.id,
|
||||
startTime: new Date(),
|
||||
})
|
||||
.returning();
|
||||
|
||||
const [row] = await db.select().from(workSessions).where(eq(workSessions.id, inserted.id));
|
||||
|
||||
expect(row.status).toBe('active');
|
||||
expect(row.source).toBe('app');
|
||||
expect(row.pairCount).toBe(2);
|
||||
expect(row.endTime).toBeNull();
|
||||
expect(row.durationSeconds).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -16,3 +16,50 @@ export const MeResponse = z.object({
|
||||
user: PublicUser,
|
||||
});
|
||||
export type MeResponse = z.infer<typeof MeResponse>;
|
||||
|
||||
export const InsoleType = z.enum(['Kurk', 'Berk', '3D']);
|
||||
export type InsoleType = z.infer<typeof InsoleType>;
|
||||
|
||||
export const Activity = z.object({
|
||||
id: z.number().int(),
|
||||
name: z.string(),
|
||||
insole_types: z.array(InsoleType),
|
||||
created_at: z.string(), // ISO-8601
|
||||
});
|
||||
export type Activity = z.infer<typeof Activity>;
|
||||
|
||||
export const CreateActivityInput = z.object({
|
||||
name: z.string().trim().min(1),
|
||||
insole_types: z.array(InsoleType).default(['Kurk', 'Berk', '3D']),
|
||||
});
|
||||
export type CreateActivityInput = z.infer<typeof CreateActivityInput>;
|
||||
|
||||
export const UpdateActivityInput = CreateActivityInput;
|
||||
export type UpdateActivityInput = z.infer<typeof UpdateActivityInput>;
|
||||
|
||||
export const SessionStatus = z.enum(['active', 'completed', 'discarded']);
|
||||
export type SessionStatus = z.infer<typeof SessionStatus>;
|
||||
|
||||
export const WorkSession = z.object({
|
||||
id: z.number().int(),
|
||||
user_id: z.string(),
|
||||
activity_id: z.number().int(),
|
||||
activity_name: z.string().optional(), // present on history/active joins
|
||||
insole_type: InsoleType.nullable(),
|
||||
pair_count: z.number().int(),
|
||||
start_time: z.string(), // ISO-8601
|
||||
end_time: z.string().nullable(),
|
||||
duration_seconds: z.number().int().nullable(),
|
||||
status: SessionStatus,
|
||||
source: z.enum(['app', 'manual']),
|
||||
notes: z.string().nullable(),
|
||||
created_at: z.string(),
|
||||
});
|
||||
export type WorkSession = z.infer<typeof WorkSession>;
|
||||
|
||||
export const StartSessionInput = z.object({
|
||||
activity_id: z.number().int(),
|
||||
insole_type: InsoleType,
|
||||
pair_count: z.number().int().min(1).default(2),
|
||||
});
|
||||
export type StartSessionInput = z.infer<typeof StartSessionInput>;
|
||||
|
||||
Reference in New Issue
Block a user