supported apps domain + some refactor of data types redundancy

This commit is contained in:
user
2026-03-28 16:19:24 +02:00
parent 6639bcd799
commit 671a712b08
26 changed files with 2052 additions and 169 deletions

View File

@@ -0,0 +1,13 @@
CREATE TABLE "supported_app" (
"id" serial PRIMARY KEY NOT NULL,
"title" text NOT NULL,
"package_name" text NOT NULL,
"created_at" timestamp NOT NULL,
"updated_at" timestamp NOT NULL,
CONSTRAINT "supported_app_package_name_unique" UNIQUE("package_name")
);
--> statement-breakpoint
ALTER TABLE "link" ADD COLUMN "supported_app_id" integer NOT NULL;--> statement-breakpoint
ALTER TABLE "link" ADD CONSTRAINT "link_supported_app_id_supported_app_id_fk" FOREIGN KEY ("supported_app_id") REFERENCES "public"."supported_app"("id") ON DELETE restrict ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "link" DROP COLUMN "app_name";--> statement-breakpoint
ALTER TABLE "link" DROP COLUMN "app_package";

View File

@@ -0,0 +1,997 @@
{
"id": "f9535ed5-4fb8-4f58-b741-712a5c7356a1",
"prevId": "a633b0b6-32a7-4f7f-8b17-4264fe54ca57",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.two_factor": {
"name": "two_factor",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"secret": {
"name": "secret",
"type": "text",
"primaryKey": false,
"notNull": true
},
"backup_codes": {
"name": "backup_codes",
"type": "json",
"primaryKey": false,
"notNull": false
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"two_factor_user_id_user_id_fk": {
"name": "two_factor_user_id_user_id_fk",
"tableFrom": "two_factor",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.twofa_sessions": {
"name": "twofa_sessions",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"session_id": {
"name": "session_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"verification_token": {
"name": "verification_token",
"type": "text",
"primaryKey": false,
"notNull": true
},
"code_used": {
"name": "code_used",
"type": "text",
"primaryKey": false,
"notNull": false
},
"status": {
"name": "status",
"type": "varchar(16)",
"primaryKey": false,
"notNull": true
},
"attempts": {
"name": "attempts",
"type": "integer",
"primaryKey": false,
"notNull": true,
"default": 0
},
"max_attempts": {
"name": "max_attempts",
"type": "integer",
"primaryKey": false,
"notNull": true,
"default": 5
},
"verified_at": {
"name": "verified_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"expires_at": {
"name": "expires_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"ip_address": {
"name": "ip_address",
"type": "text",
"primaryKey": false,
"notNull": false,
"default": "''"
},
"user_agent": {
"name": "user_agent",
"type": "text",
"primaryKey": false,
"notNull": false,
"default": "''"
}
},
"indexes": {},
"foreignKeys": {
"twofa_sessions_user_id_user_id_fk": {
"name": "twofa_sessions_user_id_user_id_fk",
"tableFrom": "twofa_sessions",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"twofa_sessions_verification_token_unique": {
"name": "twofa_sessions_verification_token_unique",
"nullsNotDistinct": false,
"columns": [
"verification_token"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.account": {
"name": "account",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"account_id": {
"name": "account_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"provider_id": {
"name": "provider_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"access_token": {
"name": "access_token",
"type": "text",
"primaryKey": false,
"notNull": false
},
"refresh_token": {
"name": "refresh_token",
"type": "text",
"primaryKey": false,
"notNull": false
},
"id_token": {
"name": "id_token",
"type": "text",
"primaryKey": false,
"notNull": false
},
"access_token_expires_at": {
"name": "access_token_expires_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"refresh_token_expires_at": {
"name": "refresh_token_expires_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"scope": {
"name": "scope",
"type": "text",
"primaryKey": false,
"notNull": false
},
"password": {
"name": "password",
"type": "text",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
}
},
"indexes": {
"account_userId_idx": {
"name": "account_userId_idx",
"columns": [
{
"expression": "user_id",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"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": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.user": {
"name": "user",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"email": {
"name": "email",
"type": "text",
"primaryKey": false,
"notNull": true
},
"email_verified": {
"name": "email_verified",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"image": {
"name": "image",
"type": "text",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"username": {
"name": "username",
"type": "text",
"primaryKey": false,
"notNull": false
},
"display_username": {
"name": "display_username",
"type": "text",
"primaryKey": false,
"notNull": false
},
"role": {
"name": "role",
"type": "text",
"primaryKey": false,
"notNull": false
},
"banned": {
"name": "banned",
"type": "boolean",
"primaryKey": false,
"notNull": false,
"default": false
},
"ban_reason": {
"name": "ban_reason",
"type": "text",
"primaryKey": false,
"notNull": false
},
"ban_expires": {
"name": "ban_expires",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"onboarding_done": {
"name": "onboarding_done",
"type": "boolean",
"primaryKey": false,
"notNull": false,
"default": false
},
"last2_fa_verified_at": {
"name": "last2_fa_verified_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"parent_id": {
"name": "parent_id",
"type": "text",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"user_email_unique": {
"name": "user_email_unique",
"nullsNotDistinct": false,
"columns": [
"email"
]
},
"user_username_unique": {
"name": "user_username_unique",
"nullsNotDistinct": false,
"columns": [
"username"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.verification": {
"name": "verification",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"identifier": {
"name": "identifier",
"type": "text",
"primaryKey": false,
"notNull": true
},
"value": {
"name": "value",
"type": "text",
"primaryKey": false,
"notNull": true
},
"expires_at": {
"name": "expires_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {
"verification_identifier_idx": {
"name": "verification_identifier_idx",
"columns": [
{
"expression": "identifier",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.device": {
"name": "device",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"title": {
"name": "title",
"type": "text",
"primaryKey": false,
"notNull": true
},
"version": {
"name": "version",
"type": "text",
"primaryKey": false,
"notNull": true
},
"status": {
"name": "status",
"type": "varchar(16)",
"primaryKey": false,
"notNull": true,
"default": "'offline'"
},
"is_active": {
"name": "is_active",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"in_use": {
"name": "in_use",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"container_id": {
"name": "container_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"host": {
"name": "host",
"type": "text",
"primaryKey": false,
"notNull": true
},
"ws_port": {
"name": "ws_port",
"type": "text",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.notifications": {
"name": "notifications",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"title": {
"name": "title",
"type": "text",
"primaryKey": false,
"notNull": true
},
"body": {
"name": "body",
"type": "text",
"primaryKey": false,
"notNull": true
},
"priority": {
"name": "priority",
"type": "varchar(12)",
"primaryKey": false,
"notNull": true,
"default": "'normal'"
},
"type": {
"name": "type",
"type": "varchar(12)",
"primaryKey": false,
"notNull": true
},
"category": {
"name": "category",
"type": "varchar(64)",
"primaryKey": false,
"notNull": false
},
"is_read": {
"name": "is_read",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"is_archived": {
"name": "is_archived",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"action_url": {
"name": "action_url",
"type": "text",
"primaryKey": false,
"notNull": false
},
"action_type": {
"name": "action_type",
"type": "varchar(16)",
"primaryKey": false,
"notNull": false
},
"action_data": {
"name": "action_data",
"type": "json",
"primaryKey": false,
"notNull": false
},
"icon": {
"name": "icon",
"type": "varchar(64)",
"primaryKey": false,
"notNull": false
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"sent_at": {
"name": "sent_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"read_at": {
"name": "read_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"expires_at": {
"name": "expires_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"notifications_user_id_user_id_fk": {
"name": "notifications_user_id_user_id_fk",
"tableFrom": "notifications",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.link": {
"name": "link",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"token": {
"name": "token",
"type": "text",
"primaryKey": false,
"notNull": true
},
"status": {
"name": "status",
"type": "varchar(16)",
"primaryKey": false,
"notNull": true,
"default": "'active'"
},
"linked_device_id": {
"name": "linked_device_id",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"supported_app_id": {
"name": "supported_app_id",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"expires_at": {
"name": "expires_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"last_accessed_at": {
"name": "last_accessed_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"link_linked_device_id_device_id_fk": {
"name": "link_linked_device_id_device_id_fk",
"tableFrom": "link",
"tableTo": "device",
"columnsFrom": [
"linked_device_id"
],
"columnsTo": [
"id"
],
"onDelete": "set null",
"onUpdate": "no action"
},
"link_supported_app_id_supported_app_id_fk": {
"name": "link_supported_app_id_supported_app_id_fk",
"tableFrom": "link",
"tableTo": "supported_app",
"columnsFrom": [
"supported_app_id"
],
"columnsTo": [
"id"
],
"onDelete": "restrict",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"link_token_unique": {
"name": "link_token_unique",
"nullsNotDistinct": false,
"columns": [
"token"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.supported_app": {
"name": "supported_app",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"title": {
"name": "title",
"type": "text",
"primaryKey": false,
"notNull": true
},
"package_name": {
"name": "package_name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"supported_app_package_name_unique": {
"name": "supported_app_package_name_unique",
"nullsNotDistinct": false,
"columns": [
"package_name"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.task": {
"name": "task",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"type": {
"name": "type",
"type": "varchar(32)",
"primaryKey": false,
"notNull": true
},
"status": {
"name": "status",
"type": "varchar(16)",
"primaryKey": false,
"notNull": true
},
"progress": {
"name": "progress",
"type": "integer",
"primaryKey": false,
"notNull": true,
"default": 0
},
"payload": {
"name": "payload",
"type": "json",
"primaryKey": false,
"notNull": false
},
"result": {
"name": "result",
"type": "json",
"primaryKey": false,
"notNull": false
},
"error": {
"name": "error",
"type": "json",
"primaryKey": false,
"notNull": false
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"resource_id": {
"name": "resource_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"started_at": {
"name": "started_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"completed_at": {
"name": "completed_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"task_user_id_user_id_fk": {
"name": "task_user_id_user_id_fk",
"tableFrom": "task",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
}
},
"enums": {},
"schemas": {},
"sequences": {},
"roles": {},
"policies": {},
"views": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}

View File

@@ -22,6 +22,13 @@
"when": 1774703478082,
"tag": "0002_remarkable_charles_xavier",
"breakpoints": true
},
{
"idx": 3,
"version": "7",
"when": 1774705734646,
"tag": "0003_workable_runaways",
"breakpoints": true
}
]
}

View File

@@ -3,4 +3,5 @@ export * from "./better.auth.schema";
export * from "./device.schema";
export * from "./general.schema";
export * from "./link.schema";
export * from "./supported-app.schema";
export * from "./task.schema";

View File

@@ -6,6 +6,7 @@ import {
timestamp,
varchar,
} from "drizzle-orm/pg-core";
import { supportedApp } from "./supported-app.schema";
import { device } from "./device.schema";
import { relations } from "drizzle-orm";
@@ -14,12 +15,14 @@ export const link = pgTable("link", {
token: text("token").notNull().unique(),
status: varchar("status", { length: 16 }).notNull().default("active"), // "active" | "inactive" | "expired" | "revoked"
appName: text("app_name").notNull(),
appPackage: text("app_package").notNull(),
linkedDeviceId: integer("linked_device_id").references(() => device.id, {
onDelete: "set null",
}),
supportedAppId: integer("supported_app_id")
.notNull()
.references(() => supportedApp.id, {
onDelete: "restrict",
}),
expiresAt: timestamp("expires_at"),
lastAccessedAt: timestamp("last_accessed_at"),
@@ -32,4 +35,8 @@ export const linkRelations = relations(link, ({ one }) => ({
fields: [link.linkedDeviceId],
references: [device.id],
}),
supportedApp: one(supportedApp, {
fields: [link.supportedAppId],
references: [supportedApp.id],
}),
}));

View File

@@ -0,0 +1,9 @@
import { pgTable, serial, text, timestamp } from "drizzle-orm/pg-core";
export const supportedApp = pgTable("supported_app", {
id: serial("id").primaryKey(),
title: text("title").notNull(),
packageName: text("package_name").notNull().unique(),
createdAt: timestamp("created_at").notNull(),
updatedAt: timestamp("updated_at").notNull(),
});

View File

@@ -3,18 +3,24 @@ import { nanoid } from "nanoid";
import { db } from "@pkg/db";
import { type Err } from "@pkg/result";
import { FlowExecCtx } from "@core/flow.execution.context";
import { CreateLink, Link, LinkStatus, LinkWithDevice, UpdateLink } from "./data";
import {
CreateLink,
Link,
LinkStatus,
LinkWithDevice,
UpdateLink,
} from "./data";
import { LinkRepository } from "./repository";
import { linkErrors } from "./errors";
export class LinkController {
constructor(private repo: LinkRepository) {}
list(fctx: FlowExecCtx): ResultAsync<Link[], Err> {
list(fctx: FlowExecCtx): ResultAsync<LinkWithDevice[], Err> {
return this.repo.list(fctx);
}
getById(fctx: FlowExecCtx, id: number): ResultAsync<Link, Err> {
getById(fctx: FlowExecCtx, id: number): ResultAsync<LinkWithDevice, Err> {
return this.repo.getById(fctx, id);
}

View File

@@ -1,5 +1,6 @@
import * as v from "valibot";
import { deviceSchema } from "@domains/device/data";
import { supportedAppSchema } from "@domains/supported-app/data";
export enum LinkStatus {
ACTIVE = "active",
@@ -15,9 +16,8 @@ export const linkSchema = v.object({
id: v.number(),
token: v.string(),
status: linkStatusSchema,
appName: v.string(),
appPackage: v.string(),
linkedDeviceId: v.nullable(v.number()),
supportedAppId: v.number(),
expiresAt: v.nullable(v.date()),
lastAccessedAt: v.nullable(v.date()),
createdAt: v.date(),
@@ -28,14 +28,14 @@ export type Link = v.InferOutput<typeof linkSchema>;
export const linkWithDeviceSchema = v.object({
...linkSchema.entries,
device: v.nullable(deviceSchema),
supportedApp: v.nullable(supportedAppSchema),
});
export type LinkWithDevice = v.InferOutput<typeof linkWithDeviceSchema>;
export const createLinkSchema = v.object({
token: v.pipe(v.string(), v.minLength(1)),
appName: v.pipe(v.string(), v.minLength(1)),
appPackage: v.pipe(v.string(), v.minLength(1)),
linkedDeviceId: v.number(),
supportedAppId: v.number(),
expiresAt: v.optional(v.nullable(v.date())),
});
export type CreateLink = v.InferOutput<typeof createLinkSchema>;
@@ -43,9 +43,8 @@ export type CreateLink = v.InferOutput<typeof createLinkSchema>;
export const updateLinkSchema = v.partial(
v.object({
status: linkStatusSchema,
appName: v.string(),
appPackage: v.string(),
linkedDeviceId: v.nullable(v.number()),
supportedAppId: v.number(),
expiresAt: v.nullable(v.date()),
lastAccessedAt: v.nullable(v.date()),
}),

View File

@@ -1,6 +1,6 @@
import { ResultAsync, errAsync, okAsync } from "neverthrow";
import { FlowExecCtx } from "@core/flow.execution.context";
import { Database, asc, eq } from "@pkg/db";
import { Database, eq } from "@pkg/db";
import { link } from "@pkg/db/schema";
import { type Err } from "@pkg/result";
import { logger } from "@pkg/logger";
@@ -11,29 +11,41 @@ import { linkErrors } from "./errors";
export class LinkRepository {
constructor(private db: Database) {}
list(fctx: FlowExecCtx): ResultAsync<Link[], Err> {
list(fctx: FlowExecCtx): ResultAsync<LinkWithDevice[], Err> {
return traceResultAsync({
name: "link.list",
fctx,
fn: () =>
ResultAsync.fromPromise(
this.db.select().from(link).orderBy(asc(link.createdAt)),
this.db.query.link.findMany({
orderBy: (link, { asc }) => [asc(link.createdAt)],
with: {
device: true,
supportedApp: true,
},
}),
(e) =>
linkErrors.listFailed(
fctx,
e instanceof Error ? e.message : String(e),
),
).map((rows) => rows as Link[]),
).map((rows) => rows as LinkWithDevice[]),
});
}
getById(fctx: FlowExecCtx, id: number): ResultAsync<Link, Err> {
getById(fctx: FlowExecCtx, id: number): ResultAsync<LinkWithDevice, Err> {
return traceResultAsync({
name: "link.getById",
fctx,
fn: () =>
ResultAsync.fromPromise(
this.db.query.link.findFirst({ where: eq(link.id, id) }),
this.db.query.link.findFirst({
where: eq(link.id, id),
with: {
device: true,
supportedApp: true,
},
}),
(e) =>
linkErrors.dbError(
fctx,
@@ -41,7 +53,7 @@ export class LinkRepository {
),
).andThen((row) => {
if (!row) return errAsync(linkErrors.linkNotFound(fctx, id));
return okAsync(row as Link);
return okAsync(row as LinkWithDevice);
}),
});
}
@@ -54,7 +66,7 @@ export class LinkRepository {
ResultAsync.fromPromise(
this.db.query.link.findFirst({
where: eq(link.token, token),
with: { device: true },
with: { device: true, supportedApp: true },
}),
(e) =>
linkErrors.dbError(
@@ -81,9 +93,8 @@ export class LinkRepository {
.values({
token: data.token,
status: "active",
appName: data.appName,
appPackage: data.appPackage,
linkedDeviceId: data.linkedDeviceId ?? null,
supportedAppId: data.supportedAppId,
expiresAt: data.expiresAt ?? null,
createdAt: new Date(),
updatedAt: new Date(),

View File

@@ -0,0 +1,45 @@
import { db } from "@pkg/db";
import { type Err } from "@pkg/result";
import { FlowExecCtx } from "@core/flow.execution.context";
import { ResultAsync } from "neverthrow";
import {
CreateSupportedApp,
SupportedApp,
UpdateSupportedApp,
} from "./data";
import { SupportedAppRepository } from "./repository";
export class SupportedAppController {
constructor(private repo: SupportedAppRepository) {}
list(fctx: FlowExecCtx): ResultAsync<SupportedApp[], Err> {
return this.repo.list(fctx);
}
getById(fctx: FlowExecCtx, id: number): ResultAsync<SupportedApp, Err> {
return this.repo.getById(fctx, id);
}
create(
fctx: FlowExecCtx,
data: CreateSupportedApp,
): ResultAsync<SupportedApp, Err> {
return this.repo.create(fctx, data);
}
update(
fctx: FlowExecCtx,
id: number,
data: UpdateSupportedApp,
): ResultAsync<SupportedApp, Err> {
return this.repo.update(fctx, id, data);
}
delete(fctx: FlowExecCtx, id: number): ResultAsync<boolean, Err> {
return this.repo.delete(fctx, id);
}
}
export function getSupportedAppController(): SupportedAppController {
return new SupportedAppController(new SupportedAppRepository(db));
}

View File

@@ -0,0 +1,28 @@
import * as v from "valibot";
export const supportedAppSchema = v.object({
id: v.number(),
title: v.string(),
packageName: v.string(),
createdAt: v.date(),
updatedAt: v.date(),
});
export type SupportedApp = v.InferOutput<typeof supportedAppSchema>;
export const createSupportedAppSchema = v.object({
title: v.pipe(v.string(), v.minLength(1)),
packageName: v.pipe(v.string(), v.minLength(1)),
});
export type CreateSupportedApp = v.InferOutput<
typeof createSupportedAppSchema
>;
export const updateSupportedAppSchema = v.partial(
v.object({
title: v.string(),
packageName: v.string(),
}),
);
export type UpdateSupportedApp = v.InferOutput<
typeof updateSupportedAppSchema
>;

View File

@@ -0,0 +1,59 @@
import { FlowExecCtx } from "@/core/flow.execution.context";
import { ERROR_CODES, type Err } from "@pkg/result";
import { getError } from "@pkg/logger";
export const supportedAppErrors = {
dbError: (fctx: FlowExecCtx, detail: string): Err =>
getError({
flowId: fctx.flowId,
code: ERROR_CODES.DATABASE_ERROR,
message: "Database operation failed",
description: "Please try again later",
detail,
}),
supportedAppNotFound: (fctx: FlowExecCtx, id: number): Err =>
getError({
flowId: fctx.flowId,
code: ERROR_CODES.NOT_FOUND,
message: "Supported app not found",
description: "The requested supported app does not exist",
detail: `No supported app found with ID: ${id}`,
}),
listFailed: (fctx: FlowExecCtx, detail: string): Err =>
getError({
flowId: fctx.flowId,
code: ERROR_CODES.DATABASE_ERROR,
message: "Failed to list supported apps",
description: "Try again later",
detail,
}),
createFailed: (fctx: FlowExecCtx, detail: string): Err =>
getError({
flowId: fctx.flowId,
code: ERROR_CODES.DATABASE_ERROR,
message: "Failed to create supported app",
description: "Try again later",
detail,
}),
updateFailed: (fctx: FlowExecCtx, detail: string): Err =>
getError({
flowId: fctx.flowId,
code: ERROR_CODES.DATABASE_ERROR,
message: "Failed to update supported app",
description: "Try again later",
detail,
}),
deleteFailed: (fctx: FlowExecCtx, detail: string): Err =>
getError({
flowId: fctx.flowId,
code: ERROR_CODES.DATABASE_ERROR,
message: "Failed to delete supported app",
description: "Try again later",
detail,
}),
};

View File

@@ -0,0 +1,149 @@
import { ResultAsync, errAsync, okAsync } from "neverthrow";
import { FlowExecCtx } from "@core/flow.execution.context";
import { Database, asc, eq } from "@pkg/db";
import { supportedApp } from "@pkg/db/schema";
import { type Err } from "@pkg/result";
import { logger } from "@pkg/logger";
import { traceResultAsync } from "@core/observability";
import {
CreateSupportedApp,
SupportedApp,
UpdateSupportedApp,
} from "./data";
import { supportedAppErrors } from "./errors";
export class SupportedAppRepository {
constructor(private db: Database) {}
list(fctx: FlowExecCtx): ResultAsync<SupportedApp[], Err> {
return traceResultAsync({
name: "supportedApp.list",
fctx,
fn: () =>
ResultAsync.fromPromise(
this.db
.select()
.from(supportedApp)
.orderBy(asc(supportedApp.createdAt)),
(e) =>
supportedAppErrors.listFailed(
fctx,
e instanceof Error ? e.message : String(e),
),
).map((rows) => rows as SupportedApp[]),
});
}
getById(fctx: FlowExecCtx, id: number): ResultAsync<SupportedApp, Err> {
return traceResultAsync({
name: "supportedApp.getById",
fctx,
fn: () =>
ResultAsync.fromPromise(
this.db.query.supportedApp.findFirst({
where: eq(supportedApp.id, id),
}),
(e) =>
supportedAppErrors.dbError(
fctx,
e instanceof Error ? e.message : String(e),
),
).andThen((row) => {
if (!row) {
return errAsync(
supportedAppErrors.supportedAppNotFound(fctx, id),
);
}
return okAsync(row as SupportedApp);
}),
});
}
create(
fctx: FlowExecCtx,
data: CreateSupportedApp,
): ResultAsync<SupportedApp, Err> {
logger.info("Creating supported app", { ...fctx, title: data.title });
return traceResultAsync({
name: "supportedApp.create",
fctx,
fn: () =>
ResultAsync.fromPromise(
this.db
.insert(supportedApp)
.values({
title: data.title,
packageName: data.packageName,
createdAt: new Date(),
updatedAt: new Date(),
})
.returning()
.execute(),
(e) =>
supportedAppErrors.createFailed(
fctx,
e instanceof Error ? e.message : String(e),
),
).map((rows) => rows[0] as SupportedApp),
});
}
update(
fctx: FlowExecCtx,
id: number,
updates: UpdateSupportedApp,
): ResultAsync<SupportedApp, Err> {
return traceResultAsync({
name: "supportedApp.update",
fctx,
fn: () =>
this.getById(fctx, id).andThen(() =>
ResultAsync.fromPromise(
this.db
.update(supportedApp)
.set({ ...updates, updatedAt: new Date() })
.where(eq(supportedApp.id, id))
.returning()
.execute(),
(e) =>
supportedAppErrors.updateFailed(
fctx,
e instanceof Error ? e.message : String(e),
),
).andThen((rows) => {
if (!rows[0]) {
return errAsync(
supportedAppErrors.supportedAppNotFound(
fctx,
id,
),
);
}
return okAsync(rows[0] as SupportedApp);
}),
),
});
}
delete(fctx: FlowExecCtx, id: number): ResultAsync<boolean, Err> {
return traceResultAsync({
name: "supportedApp.delete",
fctx,
fn: () =>
this.getById(fctx, id).andThen(() =>
ResultAsync.fromPromise(
this.db
.delete(supportedApp)
.where(eq(supportedApp.id, id))
.execute(),
(e) =>
supportedAppErrors.deleteFailed(
fctx,
e instanceof Error ? e.message : String(e),
),
).map(() => true),
),
});
}
}

View File

@@ -19,7 +19,7 @@ export const settingsSchema = v.object({
debugKey: v.string(),
orchestratorApiUrl: v.string(),
wsScrcpySvcUrl: v.string(),
publicWsScrcpySvcUrl: v.string(),
betterAuthUrl: v.string(),
betterAuthSecret: v.string(),
@@ -87,7 +87,7 @@ function loadSettings(): Settings {
"ORCHESTRATOR_API_URL",
"http://localhost:3000",
),
wsScrcpySvcUrl: getEnv("WS_SCRCPY_SVC_URL"),
publicWsScrcpySvcUrl: getEnv("PUBLIC_WS_SCRCPY_SVC_URL"),
betterAuthUrl: getEnv("BETTER_AUTH_URL"),
betterAuthSecret: getEnv("BETTER_AUTH_SECRET"),