add packages

This commit is contained in:
Fredrik Jensen 2026-02-08 11:43:06 +01:00
parent 708d23df22
commit 36f6657be7
19 changed files with 3456 additions and 3 deletions

View File

@ -11,7 +11,9 @@
"Bash(psql:*)",
"Bash(pnpm test:*)",
"Bash(pnpm vitest run:*)",
"Bash(npx tsc:*)"
"Bash(npx tsc:*)",
"Bash(pnpm db:generate:*)",
"Bash(DATABASE_URL=\"postgresql://fedjens@localhost:5432/eyrun_test\" JWT_SECRET=\"test-secret-that-is-at-least-32-characters-long\" pnpm db:migrate:*)"
]
}
}

View File

@ -0,0 +1,11 @@
CREATE TYPE "public"."membership_role" AS ENUM('owner', 'admin', 'member');--> statement-breakpoint
CREATE TABLE "projects" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"account_id" uuid NOT NULL,
"name" text NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
ALTER TABLE "memberships" ALTER COLUMN "role" SET DATA TYPE "public"."membership_role" USING "role"::"public"."membership_role";--> statement-breakpoint
ALTER TABLE "projects" ADD CONSTRAINT "projects_account_id_accounts_id_fk" FOREIGN KEY ("account_id") REFERENCES "public"."accounts"("id") ON DELETE no action ON UPDATE no action;

View File

@ -0,0 +1,27 @@
CREATE TABLE "packages" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"slug" text NOT NULL,
"name" text NOT NULL,
"vcpu" integer NOT NULL,
"ram" integer NOT NULL,
"disk" integer NOT NULL,
"price_monthly" numeric(10, 2) NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
CONSTRAINT "packages_slug_unique" UNIQUE("slug")
);
--> statement-breakpoint
CREATE TABLE "wms" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"project_id" uuid NOT NULL,
"name" text NOT NULL,
"package_id" uuid,
"vcpu" integer NOT NULL,
"ram" integer NOT NULL,
"disk" integer NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
ALTER TABLE "wms" ADD CONSTRAINT "wms_project_id_projects_id_fk" FOREIGN KEY ("project_id") REFERENCES "public"."projects"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "wms" ADD CONSTRAINT "wms_package_id_packages_id_fk" FOREIGN KEY ("package_id") REFERENCES "public"."packages"("id") ON DELETE no action ON UPDATE no action;

View File

@ -0,0 +1,2 @@
ALTER TABLE "packages" ALTER COLUMN "vcpu" SET DATA TYPE real;--> statement-breakpoint
ALTER TABLE "wms" ALTER COLUMN "vcpu" SET DATA TYPE real;

View File

@ -0,0 +1,7 @@
INSERT INTO "packages" ("slug", "name", "vcpu", "ram", "disk", "price_monthly")
VALUES
('sandbox', 'Sandbox', 0.25, 256, 1, '0.00'),
('nano', 'Nano', 0.5, 512, 5, '3.00'),
('standard', 'Standard', 1, 1024, 10, '6.00'),
('pro', 'Pro', 4, 4096, 20, '18.00')
ON CONFLICT ("slug") DO NOTHING;

View File

@ -0,0 +1,397 @@
{
"id": "68978153-b70a-48ef-bb92-88598f5319cc",
"prevId": "aff5e3b9-ae84-4896-8198-eec50eb41dd9",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.accounts": {
"name": "accounts",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.memberships": {
"name": "memberships",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"user_id": {
"name": "user_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"account_id": {
"name": "account_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"role": {
"name": "role",
"type": "membership_role",
"typeSchema": "public",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"memberships_user_id_users_id_fk": {
"name": "memberships_user_id_users_id_fk",
"tableFrom": "memberships",
"tableTo": "users",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
},
"memberships_account_id_accounts_id_fk": {
"name": "memberships_account_id_accounts_id_fk",
"tableFrom": "memberships",
"tableTo": "accounts",
"columnsFrom": [
"account_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"memberships_user_id_account_id_unique": {
"name": "memberships_user_id_account_id_unique",
"nullsNotDistinct": false,
"columns": [
"user_id",
"account_id"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.otp_codes": {
"name": "otp_codes",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"email": {
"name": "email",
"type": "text",
"primaryKey": false,
"notNull": true
},
"code": {
"name": "code",
"type": "text",
"primaryKey": false,
"notNull": true
},
"expires_at": {
"name": "expires_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true
},
"used": {
"name": "used",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"attempts": {
"name": "attempts",
"type": "integer",
"primaryKey": false,
"notNull": true,
"default": 0
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.projects": {
"name": "projects",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"account_id": {
"name": "account_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"projects_account_id_accounts_id_fk": {
"name": "projects_account_id_accounts_id_fk",
"tableFrom": "projects",
"tableTo": "accounts",
"columnsFrom": [
"account_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.sessions": {
"name": "sessions",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"user_id": {
"name": "user_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"refresh_token": {
"name": "refresh_token",
"type": "text",
"primaryKey": false,
"notNull": true
},
"expires_at": {
"name": "expires_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true
},
"revoked_at": {
"name": "revoked_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"sessions_user_id_users_id_fk": {
"name": "sessions_user_id_users_id_fk",
"tableFrom": "sessions",
"tableTo": "users",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"sessions_refresh_token_unique": {
"name": "sessions_refresh_token_unique",
"nullsNotDistinct": false,
"columns": [
"refresh_token"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.users": {
"name": "users",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"email": {
"name": "email",
"type": "text",
"primaryKey": false,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"users_email_unique": {
"name": "users_email_unique",
"nullsNotDistinct": false,
"columns": [
"email"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
}
},
"enums": {
"public.membership_role": {
"name": "membership_role",
"schema": "public",
"values": [
"owner",
"admin",
"member"
]
}
},
"schemas": {},
"sequences": {},
"roles": {},
"policies": {},
"views": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}

View File

@ -0,0 +1,572 @@
{
"id": "3fe6ec31-b399-41bd-88d8-b1b855d5b3ad",
"prevId": "68978153-b70a-48ef-bb92-88598f5319cc",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.accounts": {
"name": "accounts",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.memberships": {
"name": "memberships",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"user_id": {
"name": "user_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"account_id": {
"name": "account_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"role": {
"name": "role",
"type": "membership_role",
"typeSchema": "public",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"memberships_user_id_users_id_fk": {
"name": "memberships_user_id_users_id_fk",
"tableFrom": "memberships",
"tableTo": "users",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
},
"memberships_account_id_accounts_id_fk": {
"name": "memberships_account_id_accounts_id_fk",
"tableFrom": "memberships",
"tableTo": "accounts",
"columnsFrom": [
"account_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"memberships_user_id_account_id_unique": {
"name": "memberships_user_id_account_id_unique",
"nullsNotDistinct": false,
"columns": [
"user_id",
"account_id"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.otp_codes": {
"name": "otp_codes",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"email": {
"name": "email",
"type": "text",
"primaryKey": false,
"notNull": true
},
"code": {
"name": "code",
"type": "text",
"primaryKey": false,
"notNull": true
},
"expires_at": {
"name": "expires_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true
},
"used": {
"name": "used",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"attempts": {
"name": "attempts",
"type": "integer",
"primaryKey": false,
"notNull": true,
"default": 0
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.packages": {
"name": "packages",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"slug": {
"name": "slug",
"type": "text",
"primaryKey": false,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"vcpu": {
"name": "vcpu",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"ram": {
"name": "ram",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"disk": {
"name": "disk",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"price_monthly": {
"name": "price_monthly",
"type": "numeric(10, 2)",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"packages_slug_unique": {
"name": "packages_slug_unique",
"nullsNotDistinct": false,
"columns": [
"slug"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.projects": {
"name": "projects",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"account_id": {
"name": "account_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"projects_account_id_accounts_id_fk": {
"name": "projects_account_id_accounts_id_fk",
"tableFrom": "projects",
"tableTo": "accounts",
"columnsFrom": [
"account_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.sessions": {
"name": "sessions",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"user_id": {
"name": "user_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"refresh_token": {
"name": "refresh_token",
"type": "text",
"primaryKey": false,
"notNull": true
},
"expires_at": {
"name": "expires_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true
},
"revoked_at": {
"name": "revoked_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"sessions_user_id_users_id_fk": {
"name": "sessions_user_id_users_id_fk",
"tableFrom": "sessions",
"tableTo": "users",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"sessions_refresh_token_unique": {
"name": "sessions_refresh_token_unique",
"nullsNotDistinct": false,
"columns": [
"refresh_token"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.users": {
"name": "users",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"email": {
"name": "email",
"type": "text",
"primaryKey": false,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"users_email_unique": {
"name": "users_email_unique",
"nullsNotDistinct": false,
"columns": [
"email"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.wms": {
"name": "wms",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"project_id": {
"name": "project_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"package_id": {
"name": "package_id",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"vcpu": {
"name": "vcpu",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"ram": {
"name": "ram",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"disk": {
"name": "disk",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"wms_project_id_projects_id_fk": {
"name": "wms_project_id_projects_id_fk",
"tableFrom": "wms",
"tableTo": "projects",
"columnsFrom": [
"project_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
},
"wms_package_id_packages_id_fk": {
"name": "wms_package_id_packages_id_fk",
"tableFrom": "wms",
"tableTo": "packages",
"columnsFrom": [
"package_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
}
},
"enums": {
"public.membership_role": {
"name": "membership_role",
"schema": "public",
"values": [
"owner",
"admin",
"member"
]
}
},
"schemas": {},
"sequences": {},
"roles": {},
"policies": {},
"views": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}

View File

@ -0,0 +1,572 @@
{
"id": "08d293e6-8a0d-4abf-bbe5-600133a8d287",
"prevId": "3fe6ec31-b399-41bd-88d8-b1b855d5b3ad",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.accounts": {
"name": "accounts",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.memberships": {
"name": "memberships",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"user_id": {
"name": "user_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"account_id": {
"name": "account_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"role": {
"name": "role",
"type": "membership_role",
"typeSchema": "public",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"memberships_user_id_users_id_fk": {
"name": "memberships_user_id_users_id_fk",
"tableFrom": "memberships",
"tableTo": "users",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
},
"memberships_account_id_accounts_id_fk": {
"name": "memberships_account_id_accounts_id_fk",
"tableFrom": "memberships",
"tableTo": "accounts",
"columnsFrom": [
"account_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"memberships_user_id_account_id_unique": {
"name": "memberships_user_id_account_id_unique",
"nullsNotDistinct": false,
"columns": [
"user_id",
"account_id"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.otp_codes": {
"name": "otp_codes",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"email": {
"name": "email",
"type": "text",
"primaryKey": false,
"notNull": true
},
"code": {
"name": "code",
"type": "text",
"primaryKey": false,
"notNull": true
},
"expires_at": {
"name": "expires_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true
},
"used": {
"name": "used",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"attempts": {
"name": "attempts",
"type": "integer",
"primaryKey": false,
"notNull": true,
"default": 0
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.packages": {
"name": "packages",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"slug": {
"name": "slug",
"type": "text",
"primaryKey": false,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"vcpu": {
"name": "vcpu",
"type": "real",
"primaryKey": false,
"notNull": true
},
"ram": {
"name": "ram",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"disk": {
"name": "disk",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"price_monthly": {
"name": "price_monthly",
"type": "numeric(10, 2)",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"packages_slug_unique": {
"name": "packages_slug_unique",
"nullsNotDistinct": false,
"columns": [
"slug"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.projects": {
"name": "projects",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"account_id": {
"name": "account_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"projects_account_id_accounts_id_fk": {
"name": "projects_account_id_accounts_id_fk",
"tableFrom": "projects",
"tableTo": "accounts",
"columnsFrom": [
"account_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.sessions": {
"name": "sessions",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"user_id": {
"name": "user_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"refresh_token": {
"name": "refresh_token",
"type": "text",
"primaryKey": false,
"notNull": true
},
"expires_at": {
"name": "expires_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true
},
"revoked_at": {
"name": "revoked_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"sessions_user_id_users_id_fk": {
"name": "sessions_user_id_users_id_fk",
"tableFrom": "sessions",
"tableTo": "users",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"sessions_refresh_token_unique": {
"name": "sessions_refresh_token_unique",
"nullsNotDistinct": false,
"columns": [
"refresh_token"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.users": {
"name": "users",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"email": {
"name": "email",
"type": "text",
"primaryKey": false,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"users_email_unique": {
"name": "users_email_unique",
"nullsNotDistinct": false,
"columns": [
"email"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.wms": {
"name": "wms",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"project_id": {
"name": "project_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"package_id": {
"name": "package_id",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"vcpu": {
"name": "vcpu",
"type": "real",
"primaryKey": false,
"notNull": true
},
"ram": {
"name": "ram",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"disk": {
"name": "disk",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"wms_project_id_projects_id_fk": {
"name": "wms_project_id_projects_id_fk",
"tableFrom": "wms",
"tableTo": "projects",
"columnsFrom": [
"project_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
},
"wms_package_id_packages_id_fk": {
"name": "wms_package_id_packages_id_fk",
"tableFrom": "wms",
"tableTo": "packages",
"columnsFrom": [
"package_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
}
},
"enums": {
"public.membership_role": {
"name": "membership_role",
"schema": "public",
"values": [
"owner",
"admin",
"member"
]
}
},
"schemas": {},
"sequences": {},
"roles": {},
"policies": {},
"views": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}

View File

@ -0,0 +1,572 @@
{
"id": "08d293e6-8a0d-4abf-bbe5-600133a8d287",
"prevId": "3fe6ec31-b399-41bd-88d8-b1b855d5b3ad",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.accounts": {
"name": "accounts",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.memberships": {
"name": "memberships",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"user_id": {
"name": "user_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"account_id": {
"name": "account_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"role": {
"name": "role",
"type": "membership_role",
"typeSchema": "public",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"memberships_user_id_users_id_fk": {
"name": "memberships_user_id_users_id_fk",
"tableFrom": "memberships",
"tableTo": "users",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
},
"memberships_account_id_accounts_id_fk": {
"name": "memberships_account_id_accounts_id_fk",
"tableFrom": "memberships",
"tableTo": "accounts",
"columnsFrom": [
"account_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"memberships_user_id_account_id_unique": {
"name": "memberships_user_id_account_id_unique",
"nullsNotDistinct": false,
"columns": [
"user_id",
"account_id"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.otp_codes": {
"name": "otp_codes",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"email": {
"name": "email",
"type": "text",
"primaryKey": false,
"notNull": true
},
"code": {
"name": "code",
"type": "text",
"primaryKey": false,
"notNull": true
},
"expires_at": {
"name": "expires_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true
},
"used": {
"name": "used",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"attempts": {
"name": "attempts",
"type": "integer",
"primaryKey": false,
"notNull": true,
"default": 0
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.packages": {
"name": "packages",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"slug": {
"name": "slug",
"type": "text",
"primaryKey": false,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"vcpu": {
"name": "vcpu",
"type": "real",
"primaryKey": false,
"notNull": true
},
"ram": {
"name": "ram",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"disk": {
"name": "disk",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"price_monthly": {
"name": "price_monthly",
"type": "numeric(10, 2)",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"packages_slug_unique": {
"name": "packages_slug_unique",
"nullsNotDistinct": false,
"columns": [
"slug"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.projects": {
"name": "projects",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"account_id": {
"name": "account_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"projects_account_id_accounts_id_fk": {
"name": "projects_account_id_accounts_id_fk",
"tableFrom": "projects",
"tableTo": "accounts",
"columnsFrom": [
"account_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.sessions": {
"name": "sessions",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"user_id": {
"name": "user_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"refresh_token": {
"name": "refresh_token",
"type": "text",
"primaryKey": false,
"notNull": true
},
"expires_at": {
"name": "expires_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true
},
"revoked_at": {
"name": "revoked_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"sessions_user_id_users_id_fk": {
"name": "sessions_user_id_users_id_fk",
"tableFrom": "sessions",
"tableTo": "users",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"sessions_refresh_token_unique": {
"name": "sessions_refresh_token_unique",
"nullsNotDistinct": false,
"columns": [
"refresh_token"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.users": {
"name": "users",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"email": {
"name": "email",
"type": "text",
"primaryKey": false,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"users_email_unique": {
"name": "users_email_unique",
"nullsNotDistinct": false,
"columns": [
"email"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.wms": {
"name": "wms",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"project_id": {
"name": "project_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"package_id": {
"name": "package_id",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"vcpu": {
"name": "vcpu",
"type": "real",
"primaryKey": false,
"notNull": true
},
"ram": {
"name": "ram",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"disk": {
"name": "disk",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"wms_project_id_projects_id_fk": {
"name": "wms_project_id_projects_id_fk",
"tableFrom": "wms",
"tableTo": "projects",
"columnsFrom": [
"project_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
},
"wms_package_id_packages_id_fk": {
"name": "wms_package_id_packages_id_fk",
"tableFrom": "wms",
"tableTo": "packages",
"columnsFrom": [
"package_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
}
},
"enums": {
"public.membership_role": {
"name": "membership_role",
"schema": "public",
"values": [
"owner",
"admin",
"member"
]
}
},
"schemas": {},
"sequences": {},
"roles": {},
"policies": {},
"views": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}

View File

@ -22,6 +22,34 @@
"when": 1770481466355,
"tag": "0002_fast_tarot",
"breakpoints": true
},
{
"idx": 3,
"version": "7",
"when": 1770544024156,
"tag": "0003_sparkling_ares",
"breakpoints": true
},
{
"idx": 4,
"version": "7",
"when": 1770544905186,
"tag": "0004_dear_zuras",
"breakpoints": true
},
{
"idx": 5,
"version": "7",
"when": 1770547190763,
"tag": "0005_amazing_supernaut",
"breakpoints": true
},
{
"idx": 6,
"version": "7",
"when": 1770547300000,
"tag": "0006_seed_packages",
"breakpoints": true
}
]
}

View File

@ -1,4 +1,4 @@
import { boolean, integer, pgEnum, pgTable, text, timestamp, unique, uuid } from "drizzle-orm/pg-core";
import { boolean, integer, numeric, pgEnum, pgTable, real, text, timestamp, unique, uuid } from "drizzle-orm/pg-core";
export const membershipRoleEnum = pgEnum("membership_role", ["owner", "admin", "member"]);
@ -43,6 +43,42 @@ export const memberships = pgTable(
(t) => [unique().on(t.userId, t.accountId)],
);
export const projects = pgTable("projects", {
id: uuid().defaultRandom().primaryKey(),
accountId: uuid("account_id")
.notNull()
.references(() => accounts.id),
name: text().notNull(),
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(),
});
export const packages = pgTable("packages", {
id: uuid().defaultRandom().primaryKey(),
slug: text().notNull().unique(),
name: text().notNull(),
vcpu: real().notNull(),
ram: integer().notNull(),
disk: integer().notNull(),
priceMonthly: numeric("price_monthly", { precision: 10, scale: 2 }).notNull(),
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(),
});
export const wms = pgTable("wms", {
id: uuid().defaultRandom().primaryKey(),
projectId: uuid("project_id")
.notNull()
.references(() => projects.id),
name: text().notNull(),
packageId: uuid("package_id").references(() => packages.id),
vcpu: real().notNull(),
ram: integer().notNull(),
disk: integer().notNull(),
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(),
});
export const sessions = pgTable("sessions", {
id: uuid().defaultRandom().primaryKey(),
userId: uuid("user_id")

View File

@ -5,6 +5,9 @@ import { signupRoutes } from "./signup.js";
import { sessionRoutes } from "./sessions.js";
import { meRoutes } from "./me.js";
import { accountRoutes } from "./accounts.js";
import { projectRoutes } from "./projects.js";
import { packageRoutes } from "./packages.js";
import { wmRoutes } from "./wms.js";
export async function registerRoutes(app: FastifyInstance) {
app.register(healthRoutes);
@ -13,4 +16,7 @@ export async function registerRoutes(app: FastifyInstance) {
app.register(sessionRoutes, { prefix: "/sessions" });
app.register(meRoutes);
app.register(accountRoutes);
app.register(projectRoutes);
app.register(packageRoutes);
app.register(wmRoutes);
}

37
src/routes/packages.ts Normal file
View File

@ -0,0 +1,37 @@
import type { FastifyInstance } from "fastify";
import { db } from "../db/index.js";
import { packages } from "../db/schema.js";
export async function packageRoutes(app: FastifyInstance) {
// GET /packages — list available packages
app.get(
"/packages",
{
schema: {
description: "List available WM packages",
tags: ["Packages"],
response: {
200: {
description: "List of packages",
type: "array",
items: {
type: "object",
properties: {
id: { type: "string" },
slug: { type: "string" },
name: { type: "string" },
vcpu: { type: "number" },
ram: { type: "integer" },
disk: { type: "integer" },
priceMonthly: { type: "string" },
},
},
},
},
},
},
async () => {
return db.select().from(packages);
},
);
}

187
src/routes/projects.ts Normal file
View File

@ -0,0 +1,187 @@
import type { FastifyInstance } from "fastify";
import { eq, and } from "drizzle-orm";
import { db } from "../db/index.js";
import { projects } from "../db/schema.js";
export async function projectRoutes(app: FastifyInstance) {
// GET /projects — list projects for the current account
app.get(
"/projects",
{
preHandler: [app.authenticate, app.requireAccount],
schema: {
description: "List all projects for the current account",
tags: ["Projects"],
security: [{ bearerAuth: [] }],
response: {
200: {
description: "List of projects",
type: "array",
items: {
type: "object",
properties: {
id: { type: "string" },
name: { type: "string" },
createdAt: { type: "string", format: "date-time" },
updatedAt: { type: "string", format: "date-time" },
},
},
},
},
},
},
async (request) => {
return db
.select()
.from(projects)
.where(eq(projects.accountId, request.accountId));
},
);
// POST /projects — create a new project
app.post<{ Body: { name: string } }>(
"/projects",
{
preHandler: [app.authenticate, app.requireAccount],
schema: {
description: "Create a new project in the current account",
tags: ["Projects"],
security: [{ bearerAuth: [] }],
body: {
type: "object",
required: ["name"],
properties: {
name: { type: "string", minLength: 1 },
},
},
response: {
201: {
description: "Project created successfully",
type: "object",
properties: {
id: { type: "string" },
name: { type: "string" },
accountId: { type: "string" },
createdAt: { type: "string", format: "date-time" },
updatedAt: { type: "string", format: "date-time" },
},
},
},
},
},
async (request, reply) => {
const [project] = await db
.insert(projects)
.values({ name: request.body.name, accountId: request.accountId })
.returning();
return reply.status(201).send(project);
},
);
// PATCH /projects/:id — update a project
app.patch<{ Params: { id: string }; Body: { name: string } }>(
"/projects/:id",
{
preHandler: [app.authenticate, app.requireAccount],
schema: {
description: "Update a project",
tags: ["Projects"],
security: [{ bearerAuth: [] }],
params: {
type: "object",
properties: {
id: { type: "string", format: "uuid" },
},
},
body: {
type: "object",
required: ["name"],
properties: {
name: { type: "string", minLength: 1 },
},
},
response: {
200: {
description: "Project updated successfully",
type: "object",
properties: {
id: { type: "string" },
name: { type: "string" },
accountId: { type: "string" },
createdAt: { type: "string", format: "date-time" },
updatedAt: { type: "string", format: "date-time" },
},
},
404: {
description: "Project not found",
type: "object",
properties: {
error: { type: "string" },
},
},
},
},
},
async (request, reply) => {
const [project] = await db
.update(projects)
.set({ name: request.body.name, updatedAt: new Date() })
.where(
and(eq(projects.id, request.params.id), eq(projects.accountId, request.accountId)),
)
.returning();
if (!project) {
return reply.status(404).send({ error: "Project not found" });
}
return project;
},
);
// DELETE /projects/:id — delete a project
app.delete<{ Params: { id: string } }>(
"/projects/:id",
{
preHandler: [app.authenticate, app.requireAccount],
schema: {
description: "Delete a project",
tags: ["Projects"],
security: [{ bearerAuth: [] }],
params: {
type: "object",
properties: {
id: { type: "string", format: "uuid" },
},
},
response: {
204: {
description: "Project deleted successfully",
},
404: {
description: "Project not found",
type: "object",
properties: {
error: { type: "string" },
},
},
},
},
},
async (request, reply) => {
const [deleted] = await db
.delete(projects)
.where(
and(eq(projects.id, request.params.id), eq(projects.accountId, request.accountId)),
)
.returning();
if (!deleted) {
return reply.status(404).send({ error: "Project not found" });
}
return reply.status(204).send();
},
);
}

308
src/routes/wms.ts Normal file
View File

@ -0,0 +1,308 @@
import type { FastifyInstance } from "fastify";
import { eq, and } from "drizzle-orm";
import { db } from "../db/index.js";
import { wms, packages, projects } from "../db/schema.js";
export async function wmRoutes(app: FastifyInstance) {
// GET /projects/:projectId/wms — list WMs for a project
app.get<{ Params: { projectId: string } }>(
"/projects/:projectId/wms",
{
preHandler: [app.authenticate, app.requireAccount],
schema: {
description: "List all WMs for a project",
tags: ["WMs"],
security: [{ bearerAuth: [] }],
params: {
type: "object",
properties: {
projectId: { type: "string", format: "uuid" },
},
},
response: {
200: {
description: "List of WMs",
type: "array",
items: {
type: "object",
properties: {
id: { type: "string" },
projectId: { type: "string" },
name: { type: "string" },
packageId: { type: "string", nullable: true },
vcpu: { type: "number" },
ram: { type: "integer" },
disk: { type: "integer" },
createdAt: { type: "string", format: "date-time" },
updatedAt: { type: "string", format: "date-time" },
},
},
},
404: {
description: "Project not found",
type: "object",
properties: { error: { type: "string" } },
},
},
},
},
async (request, reply) => {
// Verify project belongs to the account
const [project] = await db
.select()
.from(projects)
.where(
and(eq(projects.id, request.params.projectId), eq(projects.accountId, request.accountId)),
);
if (!project) {
return reply.status(404).send({ error: "Project not found" });
}
return db.select().from(wms).where(eq(wms.projectId, request.params.projectId));
},
);
// POST /projects/:projectId/wms — create a WM
app.post<{
Params: { projectId: string };
Body: { name: string; packageId?: string; vcpu?: number; ram?: number; disk?: number };
}>(
"/projects/:projectId/wms",
{
preHandler: [app.authenticate, app.requireAccount],
schema: {
description:
"Create a WM from a package or with custom settings. Provide packageId to use a package, or vcpu/ram/disk for custom settings.",
tags: ["WMs"],
security: [{ bearerAuth: [] }],
params: {
type: "object",
properties: {
projectId: { type: "string", format: "uuid" },
},
},
body: {
type: "object",
required: ["name"],
properties: {
name: { type: "string", minLength: 1 },
packageId: { type: "string", format: "uuid" },
vcpu: { type: "number", minimum: 0.25 },
ram: { type: "integer", minimum: 1 },
disk: { type: "integer", minimum: 1 },
},
},
response: {
201: {
description: "WM created successfully",
type: "object",
properties: {
id: { type: "string" },
projectId: { type: "string" },
name: { type: "string" },
packageId: { type: "string", nullable: true },
vcpu: { type: "number" },
ram: { type: "integer" },
disk: { type: "integer" },
createdAt: { type: "string", format: "date-time" },
updatedAt: { type: "string", format: "date-time" },
},
},
400: {
description: "Invalid input",
type: "object",
properties: { error: { type: "string" } },
},
404: {
description: "Project or package not found",
type: "object",
properties: { error: { type: "string" } },
},
},
},
},
async (request, reply) => {
const { name, packageId, vcpu, ram, disk } = request.body;
// Verify project belongs to the account
const [project] = await db
.select()
.from(projects)
.where(
and(eq(projects.id, request.params.projectId), eq(projects.accountId, request.accountId)),
);
if (!project) {
return reply.status(404).send({ error: "Project not found" });
}
let settings: { vcpu: number; ram: number; disk: number; packageId: string | null };
if (packageId) {
const [pkg] = await db.select().from(packages).where(eq(packages.id, packageId));
if (!pkg) {
return reply.status(404).send({ error: "Package not found" });
}
settings = { vcpu: pkg.vcpu, ram: pkg.ram, disk: pkg.disk, packageId: pkg.id };
} else if (vcpu !== undefined && ram !== undefined && disk !== undefined) {
settings = { vcpu, ram, disk, packageId: null };
} else {
return reply
.status(400)
.send({ error: "Provide either packageId or all of vcpu, ram, and disk" });
}
const [wm] = await db
.insert(wms)
.values({
projectId: request.params.projectId,
name,
...settings,
})
.returning();
return reply.status(201).send(wm);
},
);
// PATCH /projects/:projectId/wms/:id — update a WM
app.patch<{
Params: { projectId: string; id: string };
Body: { name?: string; vcpu?: number; ram?: number; disk?: number };
}>(
"/projects/:projectId/wms/:id",
{
preHandler: [app.authenticate, app.requireAccount],
schema: {
description: "Update a WM's name or settings",
tags: ["WMs"],
security: [{ bearerAuth: [] }],
params: {
type: "object",
properties: {
projectId: { type: "string", format: "uuid" },
id: { type: "string", format: "uuid" },
},
},
body: {
type: "object",
properties: {
name: { type: "string", minLength: 1 },
vcpu: { type: "number", minimum: 0.25 },
ram: { type: "integer", minimum: 1 },
disk: { type: "integer", minimum: 1 },
},
},
response: {
200: {
description: "WM updated successfully",
type: "object",
properties: {
id: { type: "string" },
projectId: { type: "string" },
name: { type: "string" },
packageId: { type: "string", nullable: true },
vcpu: { type: "number" },
ram: { type: "integer" },
disk: { type: "integer" },
createdAt: { type: "string", format: "date-time" },
updatedAt: { type: "string", format: "date-time" },
},
},
404: {
description: "WM not found",
type: "object",
properties: { error: { type: "string" } },
},
},
},
},
async (request, reply) => {
// Verify project belongs to the account
const [project] = await db
.select()
.from(projects)
.where(
and(eq(projects.id, request.params.projectId), eq(projects.accountId, request.accountId)),
);
if (!project) {
return reply.status(404).send({ error: "Project not found" });
}
const { name, vcpu, ram, disk } = request.body;
const updates: Record<string, unknown> = { updatedAt: new Date() };
if (name !== undefined) updates.name = name;
if (vcpu !== undefined) updates.vcpu = vcpu;
if (ram !== undefined) updates.ram = ram;
if (disk !== undefined) updates.disk = disk;
const [wm] = await db
.update(wms)
.set(updates)
.where(and(eq(wms.id, request.params.id), eq(wms.projectId, request.params.projectId)))
.returning();
if (!wm) {
return reply.status(404).send({ error: "WM not found" });
}
return wm;
},
);
// DELETE /projects/:projectId/wms/:id — delete a WM
app.delete<{ Params: { projectId: string; id: string } }>(
"/projects/:projectId/wms/:id",
{
preHandler: [app.authenticate, app.requireAccount],
schema: {
description: "Delete a WM",
tags: ["WMs"],
security: [{ bearerAuth: [] }],
params: {
type: "object",
properties: {
projectId: { type: "string", format: "uuid" },
id: { type: "string", format: "uuid" },
},
},
response: {
204: {
description: "WM deleted successfully",
},
404: {
description: "WM not found",
type: "object",
properties: { error: { type: "string" } },
},
},
},
},
async (request, reply) => {
// Verify project belongs to the account
const [project] = await db
.select()
.from(projects)
.where(
and(eq(projects.id, request.params.projectId), eq(projects.accountId, request.accountId)),
);
if (!project) {
return reply.status(404).send({ error: "Project not found" });
}
const [deleted] = await db
.delete(wms)
.where(and(eq(wms.id, request.params.id), eq(wms.projectId, request.params.projectId)))
.returning();
if (!deleted) {
return reply.status(404).send({ error: "WM not found" });
}
return reply.status(204).send();
},
);
}

View File

@ -1,7 +1,7 @@
import { type FastifyInstance } from "fastify";
import { buildApp } from "../src/app.js";
import { db } from "../src/db/index.js";
import { users, otpCodes, sessions, accounts, memberships } from "../src/db/schema.js";
import { users, otpCodes, sessions, accounts, memberships, projects, wms, packages } from "../src/db/schema.js";
import { sql } from "drizzle-orm";
export async function createTestApp(): Promise<FastifyInstance> {
@ -11,6 +11,9 @@ export async function createTestApp(): Promise<FastifyInstance> {
}
export async function cleanDb() {
await db.delete(wms);
await db.delete(packages);
await db.delete(projects);
await db.delete(sessions);
await db.delete(memberships);
await db.delete(accounts);

47
tests/packages.test.ts Normal file
View File

@ -0,0 +1,47 @@
import { describe, it, expect, beforeAll, afterAll, beforeEach } from "vitest";
import type { FastifyInstance } from "fastify";
import { createTestApp, cleanDb } from "./helpers.js";
import { db } from "../src/db/index.js";
import { packages } from "../src/db/schema.js";
let app: FastifyInstance;
beforeAll(async () => {
app = await createTestApp();
});
afterAll(async () => {
await app.close();
});
beforeEach(async () => {
await cleanDb();
});
describe("GET /packages", () => {
it("returns empty list when no packages exist", async () => {
const res = await app.inject({ method: "GET", url: "/packages" });
expect(res.statusCode).toBe(200);
expect(res.json()).toEqual([]);
});
it("returns seeded packages", async () => {
await db.insert(packages).values([
{ slug: "small", name: "Small", vcpu: 1, ram: 1024, disk: 20, priceMonthly: "5.00" },
{ slug: "medium", name: "Medium", vcpu: 2, ram: 2048, disk: 40, priceMonthly: "10.00" },
]);
const res = await app.inject({ method: "GET", url: "/packages" });
expect(res.statusCode).toBe(200);
const body = res.json();
expect(body).toHaveLength(2);
expect(body.map((p: { slug: string }) => p.slug).sort()).toEqual(["medium", "small"]);
});
it("does not require authentication", async () => {
const res = await app.inject({ method: "GET", url: "/packages" });
expect(res.statusCode).toBe(200);
});
});

279
tests/projects.test.ts Normal file
View File

@ -0,0 +1,279 @@
import { describe, it, expect, beforeAll, afterAll, beforeEach } from "vitest";
import type { FastifyInstance } from "fastify";
import { createTestApp, cleanDb, signupUser } from "./helpers.js";
let app: FastifyInstance;
beforeAll(async () => {
app = await createTestApp();
});
afterAll(async () => {
await app.close();
});
beforeEach(async () => {
await cleanDb();
});
async function setupUser() {
const signup = await signupUser(app, "user@example.com", "Org");
const { accessToken, accounts } = signup.json();
return { accessToken, accountId: accounts[0].id as string };
}
function headers(accessToken: string, accountId: string) {
return { authorization: `Bearer ${accessToken}`, "x-account-id": accountId };
}
describe("GET /projects", () => {
it("returns empty list when no projects exist", async () => {
const { accessToken, accountId } = await setupUser();
const res = await app.inject({
method: "GET",
url: "/projects",
headers: headers(accessToken, accountId),
});
expect(res.statusCode).toBe(200);
expect(res.json()).toEqual([]);
});
it("returns projects for the account", async () => {
const { accessToken, accountId } = await setupUser();
await app.inject({
method: "POST",
url: "/projects",
headers: headers(accessToken, accountId),
payload: { name: "Project A" },
});
await app.inject({
method: "POST",
url: "/projects",
headers: headers(accessToken, accountId),
payload: { name: "Project B" },
});
const res = await app.inject({
method: "GET",
url: "/projects",
headers: headers(accessToken, accountId),
});
expect(res.statusCode).toBe(200);
const body = res.json();
expect(body).toHaveLength(2);
expect(body.map((p: { name: string }) => p.name).sort()).toEqual(["Project A", "Project B"]);
});
it("does not return projects from another account", async () => {
const { accessToken, accountId } = await setupUser();
// Create a project in the first account
await app.inject({
method: "POST",
url: "/projects",
headers: headers(accessToken, accountId),
payload: { name: "Project A" },
});
// Create a second account
const accountRes = await app.inject({
method: "POST",
url: "/accounts",
headers: { authorization: `Bearer ${accessToken}` },
payload: { name: "Org 2" },
});
const secondAccountId = accountRes.json().id;
// List projects in the second account — should be empty
const res = await app.inject({
method: "GET",
url: "/projects",
headers: headers(accessToken, secondAccountId),
});
expect(res.statusCode).toBe(200);
expect(res.json()).toEqual([]);
});
it("returns 401 without auth", async () => {
const res = await app.inject({ method: "GET", url: "/projects" });
expect(res.statusCode).toBe(401);
});
});
describe("POST /projects", () => {
it("creates a project", async () => {
const { accessToken, accountId } = await setupUser();
const res = await app.inject({
method: "POST",
url: "/projects",
headers: headers(accessToken, accountId),
payload: { name: "New Project" },
});
expect(res.statusCode).toBe(201);
const body = res.json();
expect(body.name).toBe("New Project");
expect(body.accountId).toBe(accountId);
expect(body.id).toBeDefined();
});
it("returns 400 if name is missing", async () => {
const { accessToken, accountId } = await setupUser();
const res = await app.inject({
method: "POST",
url: "/projects",
headers: headers(accessToken, accountId),
payload: {},
});
expect(res.statusCode).toBe(400);
});
});
describe("PATCH /projects/:id", () => {
it("updates a project name", async () => {
const { accessToken, accountId } = await setupUser();
const create = await app.inject({
method: "POST",
url: "/projects",
headers: headers(accessToken, accountId),
payload: { name: "Old Name" },
});
const projectId = create.json().id;
const res = await app.inject({
method: "PATCH",
url: `/projects/${projectId}`,
headers: headers(accessToken, accountId),
payload: { name: "New Name" },
});
expect(res.statusCode).toBe(200);
expect(res.json().name).toBe("New Name");
expect(res.json().id).toBe(projectId);
});
it("returns 404 for non-existent project", async () => {
const { accessToken, accountId } = await setupUser();
const res = await app.inject({
method: "PATCH",
url: "/projects/00000000-0000-0000-0000-000000000000",
headers: headers(accessToken, accountId),
payload: { name: "Nope" },
});
expect(res.statusCode).toBe(404);
});
it("returns 404 when updating a project from another account", async () => {
const { accessToken, accountId } = await setupUser();
const create = await app.inject({
method: "POST",
url: "/projects",
headers: headers(accessToken, accountId),
payload: { name: "Project" },
});
const projectId = create.json().id;
// Create second account
const accountRes = await app.inject({
method: "POST",
url: "/accounts",
headers: { authorization: `Bearer ${accessToken}` },
payload: { name: "Org 2" },
});
const secondAccountId = accountRes.json().id;
// Try to update from second account
const res = await app.inject({
method: "PATCH",
url: `/projects/${projectId}`,
headers: headers(accessToken, secondAccountId),
payload: { name: "Hacked" },
});
expect(res.statusCode).toBe(404);
});
});
describe("DELETE /projects/:id", () => {
it("deletes a project", async () => {
const { accessToken, accountId } = await setupUser();
const create = await app.inject({
method: "POST",
url: "/projects",
headers: headers(accessToken, accountId),
payload: { name: "To Delete" },
});
const projectId = create.json().id;
const res = await app.inject({
method: "DELETE",
url: `/projects/${projectId}`,
headers: headers(accessToken, accountId),
});
expect(res.statusCode).toBe(204);
// Verify it's gone
const list = await app.inject({
method: "GET",
url: "/projects",
headers: headers(accessToken, accountId),
});
expect(list.json()).toEqual([]);
});
it("returns 404 for non-existent project", async () => {
const { accessToken, accountId } = await setupUser();
const res = await app.inject({
method: "DELETE",
url: "/projects/00000000-0000-0000-0000-000000000000",
headers: headers(accessToken, accountId),
});
expect(res.statusCode).toBe(404);
});
it("returns 404 when deleting a project from another account", async () => {
const { accessToken, accountId } = await setupUser();
const create = await app.inject({
method: "POST",
url: "/projects",
headers: headers(accessToken, accountId),
payload: { name: "Project" },
});
const projectId = create.json().id;
// Create second account
const accountRes = await app.inject({
method: "POST",
url: "/accounts",
headers: { authorization: `Bearer ${accessToken}` },
payload: { name: "Org 2" },
});
const secondAccountId = accountRes.json().id;
// Try to delete from second account
const res = await app.inject({
method: "DELETE",
url: `/projects/${projectId}`,
headers: headers(accessToken, secondAccountId),
});
expect(res.statusCode).toBe(404);
});
});

360
tests/wms.test.ts Normal file
View File

@ -0,0 +1,360 @@
import { describe, it, expect, beforeAll, afterAll, beforeEach } from "vitest";
import type { FastifyInstance } from "fastify";
import { createTestApp, cleanDb, signupUser } from "./helpers.js";
import { db } from "../src/db/index.js";
import { packages } from "../src/db/schema.js";
let app: FastifyInstance;
beforeAll(async () => {
app = await createTestApp();
});
afterAll(async () => {
await app.close();
});
beforeEach(async () => {
await cleanDb();
});
async function setupUserWithProject() {
const signup = await signupUser(app, "user@example.com", "Org");
const { accessToken, accounts } = signup.json();
const accountId = accounts[0].id as string;
const projectRes = await app.inject({
method: "POST",
url: "/projects",
headers: { authorization: `Bearer ${accessToken}`, "x-account-id": accountId },
payload: { name: "My Project" },
});
const projectId = projectRes.json().id as string;
return { accessToken, accountId, projectId };
}
function headers(accessToken: string, accountId: string) {
return { authorization: `Bearer ${accessToken}`, "x-account-id": accountId };
}
async function seedSmallPackage() {
const [pkg] = await db
.insert(packages)
.values({ slug: "small", name: "Small", vcpu: 1, ram: 1024, disk: 20, priceMonthly: "5.00" })
.returning();
return pkg;
}
describe("GET /projects/:projectId/wms", () => {
it("returns empty list when no WMs exist", async () => {
const { accessToken, accountId, projectId } = await setupUserWithProject();
const res = await app.inject({
method: "GET",
url: `/projects/${projectId}/wms`,
headers: headers(accessToken, accountId),
});
expect(res.statusCode).toBe(200);
expect(res.json()).toEqual([]);
});
it("returns WMs for the project", async () => {
const { accessToken, accountId, projectId } = await setupUserWithProject();
await app.inject({
method: "POST",
url: `/projects/${projectId}/wms`,
headers: headers(accessToken, accountId),
payload: { name: "WM 1", vcpu: 1, ram: 512, disk: 10 },
});
await app.inject({
method: "POST",
url: `/projects/${projectId}/wms`,
headers: headers(accessToken, accountId),
payload: { name: "WM 2", vcpu: 2, ram: 1024, disk: 20 },
});
const res = await app.inject({
method: "GET",
url: `/projects/${projectId}/wms`,
headers: headers(accessToken, accountId),
});
expect(res.statusCode).toBe(200);
expect(res.json()).toHaveLength(2);
});
it("returns 404 for a project in another account", async () => {
const { accessToken, accountId, projectId } = await setupUserWithProject();
// Create second account
const accountRes = await app.inject({
method: "POST",
url: "/accounts",
headers: { authorization: `Bearer ${accessToken}` },
payload: { name: "Org 2" },
});
const secondAccountId = accountRes.json().id;
const res = await app.inject({
method: "GET",
url: `/projects/${projectId}/wms`,
headers: headers(accessToken, secondAccountId),
});
expect(res.statusCode).toBe(404);
});
});
describe("POST /projects/:projectId/wms", () => {
it("creates a WM from a package", async () => {
const { accessToken, accountId, projectId } = await setupUserWithProject();
const pkg = await seedSmallPackage();
const res = await app.inject({
method: "POST",
url: `/projects/${projectId}/wms`,
headers: headers(accessToken, accountId),
payload: { name: "My WM", packageId: pkg.id },
});
expect(res.statusCode).toBe(201);
const body = res.json();
expect(body.name).toBe("My WM");
expect(body.packageId).toBe(pkg.id);
expect(body.vcpu).toBe(1);
expect(body.ram).toBe(1024);
expect(body.disk).toBe(20);
});
it("creates a WM with custom settings", async () => {
const { accessToken, accountId, projectId } = await setupUserWithProject();
const res = await app.inject({
method: "POST",
url: `/projects/${projectId}/wms`,
headers: headers(accessToken, accountId),
payload: { name: "Custom WM", vcpu: 4, ram: 8192, disk: 100 },
});
expect(res.statusCode).toBe(201);
const body = res.json();
expect(body.packageId).toBeNull();
expect(body.vcpu).toBe(4);
expect(body.ram).toBe(8192);
expect(body.disk).toBe(100);
});
it("returns 400 if neither packageId nor full custom settings provided", async () => {
const { accessToken, accountId, projectId } = await setupUserWithProject();
const res = await app.inject({
method: "POST",
url: `/projects/${projectId}/wms`,
headers: headers(accessToken, accountId),
payload: { name: "Bad WM", vcpu: 2 },
});
expect(res.statusCode).toBe(400);
expect(res.json().error).toMatch(/packageId/);
});
it("returns 404 for non-existent package", async () => {
const { accessToken, accountId, projectId } = await setupUserWithProject();
const res = await app.inject({
method: "POST",
url: `/projects/${projectId}/wms`,
headers: headers(accessToken, accountId),
payload: { name: "WM", packageId: "00000000-0000-0000-0000-000000000000" },
});
expect(res.statusCode).toBe(404);
expect(res.json().error).toMatch(/Package/);
});
it("returns 404 for a project in another account", async () => {
const { accessToken, accountId, projectId } = await setupUserWithProject();
const accountRes = await app.inject({
method: "POST",
url: "/accounts",
headers: { authorization: `Bearer ${accessToken}` },
payload: { name: "Org 2" },
});
const secondAccountId = accountRes.json().id;
const res = await app.inject({
method: "POST",
url: `/projects/${projectId}/wms`,
headers: headers(accessToken, secondAccountId),
payload: { name: "WM", vcpu: 1, ram: 512, disk: 10 },
});
expect(res.statusCode).toBe(404);
});
});
describe("PATCH /projects/:projectId/wms/:id", () => {
it("updates WM name", async () => {
const { accessToken, accountId, projectId } = await setupUserWithProject();
const create = await app.inject({
method: "POST",
url: `/projects/${projectId}/wms`,
headers: headers(accessToken, accountId),
payload: { name: "Old Name", vcpu: 1, ram: 512, disk: 10 },
});
const wmId = create.json().id;
const res = await app.inject({
method: "PATCH",
url: `/projects/${projectId}/wms/${wmId}`,
headers: headers(accessToken, accountId),
payload: { name: "New Name" },
});
expect(res.statusCode).toBe(200);
expect(res.json().name).toBe("New Name");
expect(res.json().vcpu).toBe(1);
});
it("updates individual settings (e.g. only disk)", async () => {
const { accessToken, accountId, projectId } = await setupUserWithProject();
const pkg = await seedSmallPackage();
const create = await app.inject({
method: "POST",
url: `/projects/${projectId}/wms`,
headers: headers(accessToken, accountId),
payload: { name: "WM", packageId: pkg.id },
});
const wmId = create.json().id;
const res = await app.inject({
method: "PATCH",
url: `/projects/${projectId}/wms/${wmId}`,
headers: headers(accessToken, accountId),
payload: { disk: 40 },
});
expect(res.statusCode).toBe(200);
expect(res.json().disk).toBe(40);
expect(res.json().vcpu).toBe(1);
expect(res.json().ram).toBe(1024);
});
it("returns 404 for non-existent WM", async () => {
const { accessToken, accountId, projectId } = await setupUserWithProject();
const res = await app.inject({
method: "PATCH",
url: `/projects/${projectId}/wms/00000000-0000-0000-0000-000000000000`,
headers: headers(accessToken, accountId),
payload: { name: "Nope" },
});
expect(res.statusCode).toBe(404);
});
it("returns 404 for project in another account", async () => {
const { accessToken, accountId, projectId } = await setupUserWithProject();
const create = await app.inject({
method: "POST",
url: `/projects/${projectId}/wms`,
headers: headers(accessToken, accountId),
payload: { name: "WM", vcpu: 1, ram: 512, disk: 10 },
});
const wmId = create.json().id;
const accountRes = await app.inject({
method: "POST",
url: "/accounts",
headers: { authorization: `Bearer ${accessToken}` },
payload: { name: "Org 2" },
});
const secondAccountId = accountRes.json().id;
const res = await app.inject({
method: "PATCH",
url: `/projects/${projectId}/wms/${wmId}`,
headers: headers(accessToken, secondAccountId),
payload: { name: "Hacked" },
});
expect(res.statusCode).toBe(404);
});
});
describe("DELETE /projects/:projectId/wms/:id", () => {
it("deletes a WM", async () => {
const { accessToken, accountId, projectId } = await setupUserWithProject();
const create = await app.inject({
method: "POST",
url: `/projects/${projectId}/wms`,
headers: headers(accessToken, accountId),
payload: { name: "To Delete", vcpu: 1, ram: 512, disk: 10 },
});
const wmId = create.json().id;
const res = await app.inject({
method: "DELETE",
url: `/projects/${projectId}/wms/${wmId}`,
headers: headers(accessToken, accountId),
});
expect(res.statusCode).toBe(204);
// Verify it's gone
const list = await app.inject({
method: "GET",
url: `/projects/${projectId}/wms`,
headers: headers(accessToken, accountId),
});
expect(list.json()).toEqual([]);
});
it("returns 404 for non-existent WM", async () => {
const { accessToken, accountId, projectId } = await setupUserWithProject();
const res = await app.inject({
method: "DELETE",
url: `/projects/${projectId}/wms/00000000-0000-0000-0000-000000000000`,
headers: headers(accessToken, accountId),
});
expect(res.statusCode).toBe(404);
});
it("returns 404 for project in another account", async () => {
const { accessToken, accountId, projectId } = await setupUserWithProject();
const create = await app.inject({
method: "POST",
url: `/projects/${projectId}/wms`,
headers: headers(accessToken, accountId),
payload: { name: "WM", vcpu: 1, ram: 512, disk: 10 },
});
const wmId = create.json().id;
const accountRes = await app.inject({
method: "POST",
url: "/accounts",
headers: { authorization: `Bearer ${accessToken}` },
payload: { name: "Org 2" },
});
const secondAccountId = accountRes.json().id;
const res = await app.inject({
method: "DELETE",
url: `/projects/${projectId}/wms/${wmId}`,
headers: headers(accessToken, secondAccountId),
});
expect(res.statusCode).toBe(404);
});
});