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,
|
"when": 1781697895207,
|
||||||
"tag": "0000_stiff_captain_britain",
|
"tag": "0000_stiff_captain_britain",
|
||||||
"breakpoints": true
|
"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],
|
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,
|
user: PublicUser,
|
||||||
});
|
});
|
||||||
export type MeResponse = z.infer<typeof MeResponse>;
|
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