diff --git a/.env.example b/.env.example index 12e7a7a..1388cbb 100644 --- a/.env.example +++ b/.env.example @@ -17,4 +17,5 @@ STRIPE_PRODUCT_PREMIUM_YEARLY_PRICE_ID=price_premium_yearly_price_id_here # Development/Testing Flags, for convenience SKIP_OTP=true EMAIL_USE_CONSOLE=true -GCP_USE_FAKE_GCS_SERVER=true \ No newline at end of file +USE_MOCK_GCP_STORAGE=true +LABELER_SKIP_CLOUD_TASK=true \ No newline at end of file diff --git a/deno.json b/deno.json index 3db69e5..b17e10c 100644 --- a/deno.json +++ b/deno.json @@ -1,20 +1,26 @@ { "workspace": [ "./packages/app", - "./packages/core" + "./packages/core", + "./packages/db", + "./packages/settings", + "./packages/storage", + "./packages/labeler", + "./packages/manifests", + "./packages/coreApiTypes" ], "nodeModulesDir": "auto", "lock": false, "tasks": { "dev:app": "deno task --config ./packages/app/deno.json dev", "dev:core": "deno task --config ./packages/core/deno.json dev", - "dev:runner": "deno task --config ./packages/runner/deno.json dev", + "dev:labeler": "deno task --config ./packages/labeler/deno.json dev", "build:app": "deno task --config ./packages/app/deno.json build", "build:core": "deno task --config ./packages/core/deno.json build", - "test": "STRIPE_USE_MOCK=true GCP_USE_FAKE_GCS_SERVER=true deno test -A", + "build:labeler": "deno task --config ./packages/labeler/deno.json build", + "test": "STRIPE_USE_MOCK=true USE_MOCK_GCP_STORAGE=true deno test -A", "db:docker": "docker container rm stackcore-pg --force || true && docker run --name stackcore-pg -e POSTGRES_PASSWORD=password -e POSTGRES_USER=user -e POSTGRES_DB=core -p 5432:5432 -d postgres:17", - "db:migrate": "deno task --config ./packages/core/deno.json migrate -A", - "stripe:create-products": "deno run -A --env-file=.env ./packages/core/src/stripe/scripts/createProducts.ts", + "db:migrate": "deno run -A ./packages/db/src/scripts/migrate.ts", "stripe:cli": "docker run --rm -it --env-file=.env stripe/stripe-cli:latest", "stripe:forward": "deno task stripe:cli listen --forward-to http://host.docker.internal:4000/billing/webhook", "stripe:mock": "docker run --rm -it -p 12111-12112:12111-12112 stripe/stripe-mock:latest", diff --git a/packages/app/deno.json b/packages/app/deno.json index 9c1fc20..dfefdbd 100644 --- a/packages/app/deno.json +++ b/packages/app/deno.json @@ -15,8 +15,8 @@ "@radix-ui/react-slot": "npm:@radix-ui/react-slot@^1.2.3", "@radix-ui/react-tabs": "npm:@radix-ui/react-tabs@^1.1.12", "@radix-ui/react-tooltip": "npm:@radix-ui/react-tooltip@^1.2.7", - "@stackcore/core/responses": "../core/src/api/responseType.ts", - "@stackcore/core/manifest": "../core/src/manifest/types.ts", + "@stackcore/coreApiTypes": "../coreApiTypes/src/index.ts", + "@stackcore/manifests": "../manifests/src/index.ts", "@deno/vite-plugin": "npm:@deno/vite-plugin@^1.0.4", "@tailwindcss/vite": "npm:@tailwindcss/vite@^4.1.8", "@tanstack/react-table": "npm:@tanstack/react-table@^8.21.3", diff --git a/packages/app/src/components/DependencyVisualizer/DependencyVisualizer.tsx b/packages/app/src/components/DependencyVisualizer/DependencyVisualizer.tsx index 1122036..dc269f4 100644 --- a/packages/app/src/components/DependencyVisualizer/DependencyVisualizer.tsx +++ b/packages/app/src/components/DependencyVisualizer/DependencyVisualizer.tsx @@ -1,9 +1,6 @@ import { useState } from "react"; import { useSearchParams } from "react-router"; -import type { - AuditManifest, - DependencyManifest, -} from "@stackcore/core/manifest"; +import type { AuditManifest, DependencyManifest } from "@stackcore/manifests"; import { SidebarProvider, SidebarTrigger } from "../shadcn/Sidebar.tsx"; import { FileExplorerSidebar } from "./components/FileExplorerSidebar.tsx"; import BreadcrumbNav from "./components/BreadcrumNav.tsx"; diff --git a/packages/app/src/components/DependencyVisualizer/components/FileExplorerSidebar.tsx b/packages/app/src/components/DependencyVisualizer/components/FileExplorerSidebar.tsx index 45e2ced..a981733 100644 --- a/packages/app/src/components/DependencyVisualizer/components/FileExplorerSidebar.tsx +++ b/packages/app/src/components/DependencyVisualizer/components/FileExplorerSidebar.tsx @@ -1,9 +1,6 @@ import { useEffect, useState } from "react"; import { Link } from "react-router"; -import type { - AuditManifest, - DependencyManifest, -} from "@stackcore/core/manifest"; +import type { AuditManifest, DependencyManifest } from "@stackcore/manifests"; import { Sidebar, SidebarContent, diff --git a/packages/app/src/components/DependencyVisualizer/components/contextMenu/FileContextMenu.tsx b/packages/app/src/components/DependencyVisualizer/components/contextMenu/FileContextMenu.tsx index c6f9cbb..0886fea 100644 --- a/packages/app/src/components/DependencyVisualizer/components/contextMenu/FileContextMenu.tsx +++ b/packages/app/src/components/DependencyVisualizer/components/contextMenu/FileContextMenu.tsx @@ -1,5 +1,5 @@ import { Link, useSearchParams } from "react-router"; -import type { DependencyManifest } from "@stackcore/core/manifest"; +import type { DependencyManifest } from "@stackcore/manifests"; import { DropdownMenu, DropdownMenuContent, diff --git a/packages/app/src/components/DependencyVisualizer/components/contextMenu/SymbolContextMenu.tsx b/packages/app/src/components/DependencyVisualizer/components/contextMenu/SymbolContextMenu.tsx index f90f822..1d36ebd 100644 --- a/packages/app/src/components/DependencyVisualizer/components/contextMenu/SymbolContextMenu.tsx +++ b/packages/app/src/components/DependencyVisualizer/components/contextMenu/SymbolContextMenu.tsx @@ -1,5 +1,5 @@ import { Link, useSearchParams } from "react-router"; -import type { DependencyManifest } from "@stackcore/core/manifest"; +import type { DependencyManifest } from "@stackcore/manifests"; import { DropdownMenu, DropdownMenuContent, diff --git a/packages/app/src/components/DependencyVisualizer/components/controls/ControlExtensions/MetricsExtension.tsx b/packages/app/src/components/DependencyVisualizer/components/controls/ControlExtensions/MetricsExtension.tsx index bdbbae3..c222f97 100644 --- a/packages/app/src/components/DependencyVisualizer/components/controls/ControlExtensions/MetricsExtension.tsx +++ b/packages/app/src/components/DependencyVisualizer/components/controls/ControlExtensions/MetricsExtension.tsx @@ -7,7 +7,7 @@ import { metricDependencyCount, metricDependentCount, metricLinesCount, -} from "@stackcore/core/manifest"; +} from "@stackcore/manifests"; import { Tooltip, TooltipContent, diff --git a/packages/app/src/components/DependencyVisualizer/components/detailsPanes/FileDetailsPane.tsx b/packages/app/src/components/DependencyVisualizer/components/detailsPanes/FileDetailsPane.tsx index b0f3e3c..c230466 100644 --- a/packages/app/src/components/DependencyVisualizer/components/detailsPanes/FileDetailsPane.tsx +++ b/packages/app/src/components/DependencyVisualizer/components/detailsPanes/FileDetailsPane.tsx @@ -1,7 +1,4 @@ -import type { - AuditManifest, - DependencyManifest, -} from "@stackcore/core/manifest"; +import type { AuditManifest, DependencyManifest } from "@stackcore/manifests"; import { Sheet, SheetContent, diff --git a/packages/app/src/components/DependencyVisualizer/components/detailsPanes/Metrics.tsx b/packages/app/src/components/DependencyVisualizer/components/detailsPanes/Metrics.tsx index c13d10c..e89d65a 100644 --- a/packages/app/src/components/DependencyVisualizer/components/detailsPanes/Metrics.tsx +++ b/packages/app/src/components/DependencyVisualizer/components/detailsPanes/Metrics.tsx @@ -8,7 +8,7 @@ import { metricDependencyCount, metricDependentCount, metricLinesCount, -} from "@stackcore/core/manifest"; +} from "@stackcore/manifests"; import { Alert, AlertDescription } from "../../../shadcn/Alert.tsx"; export default function Metrics(props: { diff --git a/packages/app/src/components/DependencyVisualizer/components/detailsPanes/SymbolDetailsPane.tsx b/packages/app/src/components/DependencyVisualizer/components/detailsPanes/SymbolDetailsPane.tsx index 5ee1b1a..5907f2f 100644 --- a/packages/app/src/components/DependencyVisualizer/components/detailsPanes/SymbolDetailsPane.tsx +++ b/packages/app/src/components/DependencyVisualizer/components/detailsPanes/SymbolDetailsPane.tsx @@ -1,7 +1,4 @@ -import type { - AuditManifest, - DependencyManifest, -} from "@stackcore/core/manifest"; +import type { AuditManifest, DependencyManifest } from "@stackcore/manifests"; import { Sheet, SheetContent, diff --git a/packages/app/src/components/DependencyVisualizer/cytoscape/elements/file.ts b/packages/app/src/components/DependencyVisualizer/cytoscape/elements/file.ts index 901bea6..c6853d7 100644 --- a/packages/app/src/components/DependencyVisualizer/cytoscape/elements/file.ts +++ b/packages/app/src/components/DependencyVisualizer/cytoscape/elements/file.ts @@ -9,7 +9,7 @@ import type { metricDependentCount, metricLinesCount, SymbolType, -} from "@stackcore/core/manifest"; +} from "@stackcore/manifests"; import { getCollapsedSymbolNodeLabel, getExpandedSymbolNodeLabel, diff --git a/packages/app/src/components/DependencyVisualizer/cytoscape/elements/project.ts b/packages/app/src/components/DependencyVisualizer/cytoscape/elements/project.ts index 6cd7497..f000e31 100644 --- a/packages/app/src/components/DependencyVisualizer/cytoscape/elements/project.ts +++ b/packages/app/src/components/DependencyVisualizer/cytoscape/elements/project.ts @@ -3,10 +3,7 @@ import type { ElementDefinition, NodeDefinition, } from "cytoscape"; -import type { - AuditManifest, - DependencyManifest, -} from "@stackcore/core/manifest"; +import type { AuditManifest, DependencyManifest } from "@stackcore/manifests"; import { getCollapsedFileNodeLabel, getExpandedFileNodeLabel, diff --git a/packages/app/src/components/DependencyVisualizer/cytoscape/elements/symbol.ts b/packages/app/src/components/DependencyVisualizer/cytoscape/elements/symbol.ts index 6f27f0b..736b9e3 100644 --- a/packages/app/src/components/DependencyVisualizer/cytoscape/elements/symbol.ts +++ b/packages/app/src/components/DependencyVisualizer/cytoscape/elements/symbol.ts @@ -9,7 +9,7 @@ import type { metricDependentCount, metricLinesCount, SymbolType, -} from "@stackcore/core/manifest"; +} from "@stackcore/manifests"; import { getCollapsedSymbolNodeLabel, getExpandedSymbolNodeLabel, diff --git a/packages/app/src/components/DependencyVisualizer/cytoscape/elements/types.ts b/packages/app/src/components/DependencyVisualizer/cytoscape/elements/types.ts index e51ccda..e928ae5 100644 --- a/packages/app/src/components/DependencyVisualizer/cytoscape/elements/types.ts +++ b/packages/app/src/components/DependencyVisualizer/cytoscape/elements/types.ts @@ -6,7 +6,7 @@ import type { metricDependencyCount, metricDependentCount, metricLinesCount, -} from "@stackcore/core/manifest"; +} from "@stackcore/manifests"; export interface NapiNodeData { id: string; diff --git a/packages/app/src/components/DependencyVisualizer/cytoscape/fileDependencyVisualizer/index.ts b/packages/app/src/components/DependencyVisualizer/cytoscape/fileDependencyVisualizer/index.ts index 88b6748..c412c17 100644 --- a/packages/app/src/components/DependencyVisualizer/cytoscape/fileDependencyVisualizer/index.ts +++ b/packages/app/src/components/DependencyVisualizer/cytoscape/fileDependencyVisualizer/index.ts @@ -10,7 +10,7 @@ import { symbolTypeRecord, symbolTypeStruct, symbolTypeVariable, -} from "@stackcore/core/manifest"; +} from "@stackcore/manifests"; import type { Collection, Core, diff --git a/packages/app/src/components/DependencyVisualizer/cytoscape/label/index.ts b/packages/app/src/components/DependencyVisualizer/cytoscape/label/index.ts index 83a8041..42a5d4a 100644 --- a/packages/app/src/components/DependencyVisualizer/cytoscape/label/index.ts +++ b/packages/app/src/components/DependencyVisualizer/cytoscape/label/index.ts @@ -1,4 +1,4 @@ -import type { AuditManifest } from "@stackcore/core/manifest"; +import type { AuditManifest } from "@stackcore/manifests"; /** * Calculates the optimal width and height for a node based on its label text. diff --git a/packages/app/src/components/DependencyVisualizer/cytoscape/metrics/index.ts b/packages/app/src/components/DependencyVisualizer/cytoscape/metrics/index.ts index f63c1ab..f80ca5f 100644 --- a/packages/app/src/components/DependencyVisualizer/cytoscape/metrics/index.ts +++ b/packages/app/src/components/DependencyVisualizer/cytoscape/metrics/index.ts @@ -8,7 +8,7 @@ import { metricDependencyCount, metricDependentCount, metricLinesCount, -} from "@stackcore/core/manifest"; +} from "@stackcore/manifests"; /** * Extracts metric severity levels from an audit manifest for visualization. diff --git a/packages/app/src/components/DependencyVisualizer/cytoscape/projectDependencyVisualizer/index.ts b/packages/app/src/components/DependencyVisualizer/cytoscape/projectDependencyVisualizer/index.ts index 0adf624..ec0c072 100644 --- a/packages/app/src/components/DependencyVisualizer/cytoscape/projectDependencyVisualizer/index.ts +++ b/packages/app/src/components/DependencyVisualizer/cytoscape/projectDependencyVisualizer/index.ts @@ -9,7 +9,7 @@ import type { AuditManifest, DependencyManifest, Metric, -} from "@stackcore/core/manifest"; +} from "@stackcore/manifests"; import type { NapiNodeData } from "../elements/types.ts"; import { mainLayout } from "../layout/index.ts"; import { getCytoscapeStylesheet } from "../styles/index.ts"; diff --git a/packages/app/src/components/DependencyVisualizer/cytoscape/styles/index.ts b/packages/app/src/components/DependencyVisualizer/cytoscape/styles/index.ts index 23a60f6..992eb14 100644 --- a/packages/app/src/components/DependencyVisualizer/cytoscape/styles/index.ts +++ b/packages/app/src/components/DependencyVisualizer/cytoscape/styles/index.ts @@ -9,7 +9,7 @@ import { symbolTypeRecord, symbolTypeStruct, symbolTypeVariable, -} from "@stackcore/core/manifest"; +} from "@stackcore/manifests"; import type { NapiNodeData, SymbolNapiNodeData } from "../elements/types.ts"; interface CytoscapeStyles { diff --git a/packages/app/src/components/DependencyVisualizer/cytoscape/symbolDependencyVisualizer/index.ts b/packages/app/src/components/DependencyVisualizer/cytoscape/symbolDependencyVisualizer/index.ts index cb0ce65..61a9f0d 100644 --- a/packages/app/src/components/DependencyVisualizer/cytoscape/symbolDependencyVisualizer/index.ts +++ b/packages/app/src/components/DependencyVisualizer/cytoscape/symbolDependencyVisualizer/index.ts @@ -7,7 +7,7 @@ import { symbolTypeFunction, symbolTypeStruct, symbolTypeVariable, -} from "@stackcore/core/manifest"; +} from "@stackcore/manifests"; import type { Collection, Core, diff --git a/packages/app/src/components/DependencyVisualizer/visualizers/FileVisualizer.tsx b/packages/app/src/components/DependencyVisualizer/visualizers/FileVisualizer.tsx index 5579855..1bad577 100644 --- a/packages/app/src/components/DependencyVisualizer/visualizers/FileVisualizer.tsx +++ b/packages/app/src/components/DependencyVisualizer/visualizers/FileVisualizer.tsx @@ -7,7 +7,7 @@ import type { AuditManifest, DependencyManifest, Metric, -} from "@stackcore/core/manifest"; +} from "@stackcore/manifests"; import MetricsExtension from "../components/controls/ControlExtensions/MetricsExtension.tsx"; import { useTheme } from "../../../contexts/ThemeProvider.tsx"; import FiltersExtension from "../components/controls/ControlExtensions/FiltersExtension.tsx"; diff --git a/packages/app/src/components/DependencyVisualizer/visualizers/ProjectVisualizer.tsx b/packages/app/src/components/DependencyVisualizer/visualizers/ProjectVisualizer.tsx index 5c8ffe6..f4c7e6c 100644 --- a/packages/app/src/components/DependencyVisualizer/visualizers/ProjectVisualizer.tsx +++ b/packages/app/src/components/DependencyVisualizer/visualizers/ProjectVisualizer.tsx @@ -10,7 +10,7 @@ import type { AuditManifest, DependencyManifest, Metric, -} from "@stackcore/core/manifest"; +} from "@stackcore/manifests"; import { useTheme } from "../../../contexts/ThemeProvider.tsx"; export default function ProjectVisualizer(props: VisualizerContext) { diff --git a/packages/app/src/components/DependencyVisualizer/visualizers/SymbolVisualizer.tsx b/packages/app/src/components/DependencyVisualizer/visualizers/SymbolVisualizer.tsx index 325a000..79753ad 100644 --- a/packages/app/src/components/DependencyVisualizer/visualizers/SymbolVisualizer.tsx +++ b/packages/app/src/components/DependencyVisualizer/visualizers/SymbolVisualizer.tsx @@ -6,10 +6,7 @@ import { useNavigate, useSearchParams } from "react-router"; import type { VisualizerContext } from "../DependencyVisualizer.tsx"; import SymbolDetailsPane from "../components/detailsPanes/SymbolDetailsPane.tsx"; import { useTheme } from "../../../contexts/ThemeProvider.tsx"; -import type { - AuditManifest, - DependencyManifest, -} from "@stackcore/core/manifest"; +import type { AuditManifest, DependencyManifest } from "@stackcore/manifests"; import { SymbolDependencyVisualizer } from "../cytoscape/symbolDependencyVisualizer/index.ts"; export default function SymbolVisualizer( diff --git a/packages/app/src/contexts/Workspace.tsx b/packages/app/src/contexts/Workspace.tsx index d14641b..934bde8 100644 --- a/packages/app/src/contexts/Workspace.tsx +++ b/packages/app/src/contexts/Workspace.tsx @@ -1,7 +1,7 @@ import { createContext, useContext, useEffect, useState } from "react"; import { toast } from "sonner"; import { useCoreApi } from "./CoreApi.tsx"; -import { WorkspaceApiTypes } from "@stackcore/core/responses"; +import { WorkspaceApiTypes } from "@stackcore/coreApiTypes"; export type Workspace = WorkspaceApiTypes.GetWorkspacesResponse["results"][number]; diff --git a/packages/app/src/pages/index.tsx b/packages/app/src/pages/index.tsx index a208cb4..1a8ab77 100644 --- a/packages/app/src/pages/index.tsx +++ b/packages/app/src/pages/index.tsx @@ -19,7 +19,7 @@ import { Settings, Users, } from "lucide-react"; -import { ProjectApiTypes } from "@stackcore/core/responses"; +import { ProjectApiTypes } from "@stackcore/coreApiTypes"; type Project = ProjectApiTypes.GetProjectsResponse["results"][number]; diff --git a/packages/app/src/pages/invitations/claim.tsx b/packages/app/src/pages/invitations/claim.tsx index b7a05db..b186f17 100644 --- a/packages/app/src/pages/invitations/claim.tsx +++ b/packages/app/src/pages/invitations/claim.tsx @@ -1,5 +1,5 @@ import { Link, useSearchParams } from "react-router"; -import { InvitationApiTypes } from "@stackcore/core/responses"; +import { InvitationApiTypes } from "@stackcore/coreApiTypes"; import { useCoreApi } from "../../contexts/CoreApi.tsx"; import { toast } from "sonner"; import { useEffect, useState } from "react"; diff --git a/packages/app/src/pages/login.tsx b/packages/app/src/pages/login.tsx index 19b4df1..1010526 100644 --- a/packages/app/src/pages/login.tsx +++ b/packages/app/src/pages/login.tsx @@ -29,7 +29,7 @@ import { FormLabel, FormMessage, } from "../components/shadcn/Form.tsx"; -import { AuthApiTypes } from "@stackcore/core/responses"; +import { AuthApiTypes } from "@stackcore/coreApiTypes"; export default function LoginPage() { const navigate = useNavigate(); diff --git a/packages/app/src/pages/profile.tsx b/packages/app/src/pages/profile.tsx index 9bbbddf..dfde61d 100644 --- a/packages/app/src/pages/profile.tsx +++ b/packages/app/src/pages/profile.tsx @@ -50,7 +50,7 @@ import { Copy, Key, Loader, Plus, Trash, User } from "lucide-react"; import { z } from "zod"; import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; -import { TokenApiTypes } from "@stackcore/core/responses"; +import { TokenApiTypes } from "@stackcore/coreApiTypes"; type Token = { id: number; diff --git a/packages/app/src/pages/projects/add.tsx b/packages/app/src/pages/projects/add.tsx index e1f0819..ecce651 100644 --- a/packages/app/src/pages/projects/add.tsx +++ b/packages/app/src/pages/projects/add.tsx @@ -25,7 +25,7 @@ import { FormLabel, FormMessage, } from "../../components/shadcn/Form.tsx"; -import { ProjectApiTypes } from "@stackcore/core/responses"; +import { ProjectApiTypes } from "@stackcore/coreApiTypes"; import { Tabs, TabsContent, diff --git a/packages/app/src/pages/projects/index.tsx b/packages/app/src/pages/projects/index.tsx index 1fd32a8..1c8c220 100644 --- a/packages/app/src/pages/projects/index.tsx +++ b/packages/app/src/pages/projects/index.tsx @@ -29,7 +29,7 @@ import { import { DataTablePagination } from "../../components/shadcn/Datatablepagination.tsx"; import { toast } from "sonner"; import { PencilRuler, Plus } from "lucide-react"; -import { ProjectApiTypes } from "@stackcore/core/responses"; +import { ProjectApiTypes } from "@stackcore/coreApiTypes"; import { Separator } from "../../components/shadcn/Separator.tsx"; type Project = { diff --git a/packages/app/src/pages/projects/project/base.tsx b/packages/app/src/pages/projects/project/base.tsx index 35924b2..961bf29 100644 --- a/packages/app/src/pages/projects/project/base.tsx +++ b/packages/app/src/pages/projects/project/base.tsx @@ -1,7 +1,7 @@ import { Outlet, useNavigate, useParams } from "react-router"; import { useEffect, useState } from "react"; import { Skeleton } from "../../../components/shadcn/Skeleton.tsx"; -import { ProjectApiTypes } from "@stackcore/core/responses"; +import { ProjectApiTypes } from "@stackcore/coreApiTypes"; import { toast } from "sonner"; import { useCoreApi } from "../../../contexts/CoreApi.tsx"; diff --git a/packages/app/src/pages/projects/project/manifests/index.tsx b/packages/app/src/pages/projects/project/manifests/index.tsx index a4c9932..5f9e3cc 100644 --- a/packages/app/src/pages/projects/project/manifests/index.tsx +++ b/packages/app/src/pages/projects/project/manifests/index.tsx @@ -27,7 +27,7 @@ import { import { DataTablePagination } from "../../../../components/shadcn/Datatablepagination.tsx"; import { toast } from "sonner"; import { Eye, Plus, ScrollText } from "lucide-react"; -import { ManifestApiTypes } from "@stackcore/core/responses"; +import { ManifestApiTypes } from "@stackcore/coreApiTypes"; import { Separator } from "../../../../components/shadcn/Separator.tsx"; import { useCoreApi } from "../../../../contexts/CoreApi.tsx"; import type { ProjectPageContext } from "../base.tsx"; diff --git a/packages/app/src/pages/projects/project/manifests/manifest.tsx b/packages/app/src/pages/projects/project/manifests/manifest.tsx index e66f1fa..340f738 100644 --- a/packages/app/src/pages/projects/project/manifests/manifest.tsx +++ b/packages/app/src/pages/projects/project/manifests/manifest.tsx @@ -1,11 +1,8 @@ import { useEffect, useState } from "react"; import { useParams } from "react-router"; import { useCoreApi } from "../../../../contexts/CoreApi.tsx"; -import { ManifestApiTypes } from "@stackcore/core/responses"; -import type { - AuditManifest, - DependencyManifest, -} from "@stackcore/core/manifest"; +import { ManifestApiTypes } from "@stackcore/coreApiTypes"; +import type { AuditManifest, DependencyManifest } from "@stackcore/manifests"; import { Loader } from "lucide-react"; import DependencyVisualizer from "../../../../components/DependencyVisualizer/DependencyVisualizer.tsx"; diff --git a/packages/app/src/pages/projects/project/settings.tsx b/packages/app/src/pages/projects/project/settings.tsx index d666d6a..894daf1 100644 --- a/packages/app/src/pages/projects/project/settings.tsx +++ b/packages/app/src/pages/projects/project/settings.tsx @@ -24,7 +24,7 @@ import { FormLabel, FormMessage, } from "../../../components/shadcn/Form.tsx"; -import { ProjectApiTypes } from "@stackcore/core/responses"; +import { ProjectApiTypes } from "@stackcore/coreApiTypes"; import { Tabs, TabsContent, diff --git a/packages/app/src/pages/workspaces/add.tsx b/packages/app/src/pages/workspaces/add.tsx index 745d108..cd140b2 100644 --- a/packages/app/src/pages/workspaces/add.tsx +++ b/packages/app/src/pages/workspaces/add.tsx @@ -24,7 +24,7 @@ import { FormLabel, FormMessage, } from "../../components/shadcn/Form.tsx"; -import { WorkspaceApiTypes } from "@stackcore/core/responses"; +import { WorkspaceApiTypes } from "@stackcore/coreApiTypes"; export default function AddWorkspacePage() { const navigate = useNavigate(); diff --git a/packages/app/src/pages/workspaces/workspace/index.tsx b/packages/app/src/pages/workspaces/workspace/index.tsx index 7b51f40..3239a18 100644 --- a/packages/app/src/pages/workspaces/workspace/index.tsx +++ b/packages/app/src/pages/workspaces/workspace/index.tsx @@ -14,7 +14,7 @@ import { } from "../../../components/shadcn/Card.tsx"; import { Badge } from "../../../components/shadcn/Badge.tsx"; import { Skeleton } from "../../../components/shadcn/Skeleton.tsx"; -import { MemberApiTypes, WorkspaceApiTypes } from "@stackcore/core/responses"; +import { MemberApiTypes, WorkspaceApiTypes } from "@stackcore/coreApiTypes"; import { zodResolver } from "@hookform/resolvers/zod"; import { Loader, Trash } from "lucide-react"; import { Button } from "../../../components/shadcn/Button.tsx"; diff --git a/packages/app/src/pages/workspaces/workspace/members.tsx b/packages/app/src/pages/workspaces/workspace/members.tsx index 0d51b8c..d247217 100644 --- a/packages/app/src/pages/workspaces/workspace/members.tsx +++ b/packages/app/src/pages/workspaces/workspace/members.tsx @@ -49,7 +49,7 @@ import { SelectTrigger, SelectValue, } from "../../../components/shadcn/Select.tsx"; -import { InvitationApiTypes, MemberApiTypes } from "@stackcore/core/responses"; +import { InvitationApiTypes, MemberApiTypes } from "@stackcore/coreApiTypes"; import { useOutletContext } from "react-router"; import type { WorkspacePageContext } from "./index.tsx"; import { Separator } from "../../../components/shadcn/Separator.tsx"; diff --git a/packages/app/src/pages/workspaces/workspace/subscription.tsx b/packages/app/src/pages/workspaces/workspace/subscription.tsx index d26c198..39059db 100644 --- a/packages/app/src/pages/workspaces/workspace/subscription.tsx +++ b/packages/app/src/pages/workspaces/workspace/subscription.tsx @@ -9,11 +9,7 @@ import { CardTitle, } from "../../../components/shadcn/Card.tsx"; import { Button } from "../../../components/shadcn/Button.tsx"; -import { - BillingApiTypes, - MemberApiTypes, - WorkspaceApiTypes, -} from "@stackcore/core/responses"; +import { BillingApiTypes, MemberApiTypes } from "@stackcore/coreApiTypes"; import { Check, CreditCard, Loader } from "lucide-react"; import { Dialog, @@ -52,9 +48,9 @@ export default function WorkspaceSubscription() { BillingApiTypes.SubscriptionDetails | undefined >(undefined); const [billingCycle, setBillingCycle] = useState< - WorkspaceApiTypes.StripeBillingCycle + BillingApiTypes.StripeBillingCycle >( - WorkspaceApiTypes.YEARLY_BILLING_CYCLE, + BillingApiTypes.STRIPE_YEARLY_BILLING_CYCLE, ); useEffect(() => { @@ -145,14 +141,14 @@ export default function WorkspaceSubscription() { } function getChangeType( - currentProduct: WorkspaceApiTypes.StripeProduct, - currentBillingCycle: WorkspaceApiTypes.StripeBillingCycle | null, - newProduct: WorkspaceApiTypes.StripeProduct, - newBillingCycle: WorkspaceApiTypes.StripeBillingCycle, + currentProduct: BillingApiTypes.StripeProduct, + currentBillingCycle: BillingApiTypes.StripeBillingCycle | null, + newProduct: BillingApiTypes.StripeProduct, + newBillingCycle: BillingApiTypes.StripeBillingCycle, ): "upgrade" | "downgrade" | "same" | "custom" { if ( - currentProduct === WorkspaceApiTypes.CUSTOM_PRODUCT || - newProduct === WorkspaceApiTypes.CUSTOM_PRODUCT || + currentProduct === BillingApiTypes.STRIPE_CUSTOM_PRODUCT || + newProduct === BillingApiTypes.STRIPE_CUSTOM_PRODUCT || currentBillingCycle === null ) { return "custom"; @@ -160,11 +156,14 @@ export default function WorkspaceSubscription() { // Check if changing to a higher tier product const isUpgradingProduct = - (currentProduct === WorkspaceApiTypes.BASIC_PRODUCT && - [WorkspaceApiTypes.PRO_PRODUCT, WorkspaceApiTypes.PREMIUM_PRODUCT] + (currentProduct === BillingApiTypes.STRIPE_BASIC_PRODUCT && + [ + BillingApiTypes.STRIPE_PRO_PRODUCT as string, + BillingApiTypes.STRIPE_PREMIUM_PRODUCT as string, + ] .includes(newProduct)) || - (currentProduct === WorkspaceApiTypes.PRO_PRODUCT && - newProduct === WorkspaceApiTypes.PREMIUM_PRODUCT); + (currentProduct === BillingApiTypes.STRIPE_PRO_PRODUCT && + newProduct === BillingApiTypes.STRIPE_PREMIUM_PRODUCT); // If products are different, determine if upgrade or downgrade if (currentProduct !== newProduct) { @@ -174,8 +173,8 @@ export default function WorkspaceSubscription() { // If only billing cycle changed, yearly is an upgrade from monthly if (currentBillingCycle !== newBillingCycle) { const isUpgradingBilling = - currentBillingCycle === WorkspaceApiTypes.MONTHLY_BILLING_CYCLE && - newBillingCycle === WorkspaceApiTypes.YEARLY_BILLING_CYCLE; + currentBillingCycle === BillingApiTypes.STRIPE_MONTHLY_BILLING_CYCLE && + newBillingCycle === BillingApiTypes.STRIPE_YEARLY_BILLING_CYCLE; return isUpgradingBilling ? "upgrade" : "downgrade"; } @@ -313,7 +312,7 @@ export default function WorkspaceSubscription() { {/* Subscription Plans */} {subscription.product === - WorkspaceApiTypes.CUSTOM_PRODUCT + BillingApiTypes.STRIPE_CUSTOM_PRODUCT ? ( @@ -347,16 +346,18 @@ export default function WorkspaceSubscription() { value={billingCycle} onValueChange={(value) => setBillingCycle( - value as WorkspaceApiTypes.StripeBillingCycle, + value as BillingApiTypes.StripeBillingCycle, )} > Monthly - + Yearly @@ -364,13 +365,13 @@ export default function WorkspaceSubscription() { {billingCycle === - WorkspaceApiTypes.MONTHLY_BILLING_CYCLE + BillingApiTypes.STRIPE_MONTHLY_BILLING_CYCLE ? (
@@ -457,7 +458,7 @@ export default function WorkspaceSubscription() { @@ -530,12 +531,12 @@ export default function WorkspaceSubscription() { function SubscriptionCard(props: { workspaceId: number; currentSubscription: BillingApiTypes.SubscriptionDetails; - product: WorkspaceApiTypes.StripeProduct; + product: BillingApiTypes.StripeProduct; title: string; description: string; features: string[]; subscriptionPrice: string; - billingCycle: WorkspaceApiTypes.StripeBillingCycle; + billingCycle: BillingApiTypes.StripeBillingCycle; changeType: "upgrade" | "downgrade" | "same" | "custom"; role: MemberApiTypes.MemberRole | null; }) { @@ -600,8 +601,8 @@ function SubscriptionCard(props: { function ChangeSubscriptionDialog(props: { workspaceId: number; changeType: "upgrade" | "downgrade"; - newProduct: WorkspaceApiTypes.StripeProduct; - newBillingCycle: WorkspaceApiTypes.StripeBillingCycle; + newProduct: BillingApiTypes.StripeProduct; + newBillingCycle: BillingApiTypes.StripeBillingCycle; }) { const coreApi = useCoreApi(); const navigate = useNavigate(); @@ -640,7 +641,7 @@ function ChangeSubscriptionDialog(props: { setBusy(true); try { - if (props.newProduct === WorkspaceApiTypes.CUSTOM_PRODUCT) { + if (props.newProduct === BillingApiTypes.STRIPE_CUSTOM_PRODUCT) { throw new Error("Custom product is not supported"); } diff --git a/packages/core/deno.json b/packages/core/deno.json index 1788b06..208e533 100644 --- a/packages/core/deno.json +++ b/packages/core/deno.json @@ -3,17 +3,19 @@ "exports": "./src/index.ts", "tasks": { "dev": "deno run --env-file=../../.env -A --watch src/index.ts", - "migrate": "deno run -A src/db/scripts/migrate.ts", - "build": "deno compile --allow-all --include src/db/migrations --output ./dist/core ./src/index.ts", + "build": "deno compile --allow-all --include ../db/src/migrations --output ./dist/core ./src/index.ts", "email": "email dev -d src/email/templates" }, "imports": { - "@google-cloud/storage": "npm:@google-cloud/storage@^7.16.0", + "@stackcore/db": "../db/src/index.ts", + "@stackcore/settings": "../settings/src/index.ts", + "@stackcore/storage": "../storage/src/index.ts", + "@stackcore/manifests": "../manifests/src/index.ts", + "@stackcore/coreApiTypes": "../coreApiTypes/src/index.ts", "@oak/oak": "jsr:@oak/oak@^17.1.4", "@sentry/deno": "npm:@sentry/deno@^9.28.1", "@std/assert": "jsr:@std/assert@1", "@std/expect": "jsr:@std/expect@^1.0.16", - "@std/path": "jsr:@std/path@^1.0.9", "@types/html-to-text": "npm:@types/html-to-text@^9.0.4", "@types/pg": "npm:@types/pg@^8.15.2", "@types/pg-pool": "npm:@types/pg-pool@^2.0.6", diff --git a/packages/core/src/api/auth/middleware.ts b/packages/core/src/api/auth/middleware.ts index 5843b81..06eba21 100644 --- a/packages/core/src/api/auth/middleware.ts +++ b/packages/core/src/api/auth/middleware.ts @@ -1,7 +1,8 @@ import { type RouterContext, type RouterMiddleware, Status } from "@oak/oak"; import { AuthService } from "./service.ts"; import { TokenService } from "../token/service.ts"; -import { type Session, sessionSchema } from "./types.ts"; +import type { AuthApiTypes } from "@stackcore/coreApiTypes"; +import z from "zod"; export const authMiddleware: RouterMiddleware = async ( ctx: RouterContext, @@ -46,7 +47,7 @@ export const authMiddleware: RouterMiddleware = async ( ctx.state.session = { userId: verifiedUser.userId, email: verifiedUser.email, - } as Session; + } as AuthApiTypes.Session; await next(); }; @@ -55,7 +56,15 @@ export function getSession(ctx: RouterContext) { const userId = ctx.state.session?.userId; const email = ctx.state.session?.email; - const parsedSession = sessionSchema.safeParse({ userId, email }); + const sessionSchema = z.object({ + userId: z.number(), + email: z.string(), + }); + + const parsedSession = sessionSchema.safeParse({ + userId, + email, + }); if (!parsedSession.success) { throw new Error("Invalid session"); diff --git a/packages/core/src/api/auth/router.test.ts b/packages/core/src/api/auth/router.test.ts index 6af2937..4260abb 100644 --- a/packages/core/src/api/auth/router.test.ts +++ b/packages/core/src/api/auth/router.test.ts @@ -1,14 +1,18 @@ import { assertEquals, assertGreater, assertNotEquals } from "@std/assert"; import api from "../index.ts"; -import { db, destroyKyselyDb, initKyselyDb } from "../../db/database.ts"; +import { + ADMIN_ROLE, + db, + destroyKyselyDb, + initKyselyDb, + type User, +} from "@stackcore/db"; import { AuthService, secretCryptoKey } from "./service.ts"; import { resetTables } from "../../testHelpers/db.ts"; import { getNumericDate, verify } from "djwt"; -import settings from "../../settings.ts"; -import type { User } from "../../db/models/user.ts"; +import settings from "@stackcore/settings"; import { createTestUserAndToken } from "../../testHelpers/auth.ts"; -import { ADMIN_ROLE } from "../../db/models/member.ts"; -import { prepareMe, prepareRequestOtp, prepareVerifyOtp } from "./types.ts"; +import { AuthApiTypes } from "@stackcore/coreApiTypes"; Deno.test("request otp, invalid email", async () => { initKyselyDb(); @@ -17,7 +21,7 @@ Deno.test("request otp, invalid email", async () => { try { const email = `invalidemail`; - const requestInfo = prepareRequestOtp({ email }); + const requestInfo = AuthApiTypes.prepareRequestOtp({ email }); const response = await api.handle( new Request(`http://localhost:3000${requestInfo.url}`, { @@ -52,7 +56,7 @@ Deno.test("request otp for new user", async () => { // User should not exist assertEquals(user, undefined); - const requestInfo = prepareRequestOtp({ email }); + const requestInfo = AuthApiTypes.prepareRequestOtp({ email }); const response = await api.handle( new Request(`http://localhost:3000${requestInfo.url}`, { @@ -130,7 +134,7 @@ Deno.test("request otp for existing user", async () => { assertEquals(user.otp, null); assertEquals(user.otp_expires_at, null); - const requestInfo = prepareRequestOtp({ email }); + const requestInfo = AuthApiTypes.prepareRequestOtp({ email }); const response = await api.handle( new Request(`http://localhost:3000${requestInfo.url}`, { @@ -177,7 +181,7 @@ Deno.test("verify otp for new user", async () => { .executeTakeFirstOrThrow(); if (!otp) throw new Error("Failed to get OTP"); - const requestInfo = prepareVerifyOtp({ email, otp }); + const requestInfo = AuthApiTypes.prepareVerifyOtp({ email, otp }); const response = await api.handle( new Request(`http://localhost:3000${requestInfo.url}`, { @@ -253,7 +257,7 @@ Deno.test("request otp - should prevent spam requests within interval", async () const email = `test-${crypto.randomUUID()}@example.com`; // First OTP request should succeed - const firstRequestInfo = prepareRequestOtp({ email }); + const firstRequestInfo = AuthApiTypes.prepareRequestOtp({ email }); const firstResponse = await api.handle( new Request(`http://localhost:3000${firstRequestInfo.url}`, { method: firstRequestInfo.method, @@ -280,7 +284,7 @@ Deno.test("request otp - should prevent spam requests within interval", async () assertNotEquals(user?.otp_requested_at, null); // Second OTP request immediately after should be rejected - const secondRequestInfo = prepareRequestOtp({ email }); + const secondRequestInfo = AuthApiTypes.prepareRequestOtp({ email }); const secondResponse = await api.handle( new Request(`http://localhost:3000${secondRequestInfo.url}`, { method: secondRequestInfo.method, @@ -373,7 +377,7 @@ Deno.test("verify otp for existing user", async () => { .executeTakeFirstOrThrow(); if (!otp) throw new Error("Failed to get OTP"); - const requestInfo = prepareVerifyOtp({ email, otp }); + const requestInfo = AuthApiTypes.prepareVerifyOtp({ email, otp }); const response = await api.handle( new Request(`http://localhost:3000${requestInfo.url}`, { @@ -443,7 +447,10 @@ Deno.test("verify otp - should block after max failed attempts", async () => { // Make 3 failed attempts (assuming MAX_ATTEMPTS is 3) for (let i = 0; i < settings.OTP.MAX_ATTEMPTS; i++) { - const requestInfo = prepareVerifyOtp({ email, otp: wrongOtp }); + const requestInfo = AuthApiTypes.prepareVerifyOtp({ + email, + otp: wrongOtp, + }); const response = await api.handle( new Request(`http://localhost:3000${requestInfo.url}`, { @@ -461,7 +468,10 @@ Deno.test("verify otp - should block after max failed attempts", async () => { } // Next attempt should be blocked - const blockedRequestInfo = prepareVerifyOtp({ email, otp: wrongOtp }); + const blockedRequestInfo = AuthApiTypes.prepareVerifyOtp({ + email, + otp: wrongOtp, + }); const blockedResponse = await api.handle( new Request(`http://localhost:3000${blockedRequestInfo.url}`, { method: blockedRequestInfo.method, @@ -477,7 +487,10 @@ Deno.test("verify otp - should block after max failed attempts", async () => { assertEquals(blockedResponseBody.error, "otp_max_attempts"); // Even correct OTP should be blocked now - const correctRequestInfo = prepareVerifyOtp({ email, otp: validOtp }); + const correctRequestInfo = AuthApiTypes.prepareVerifyOtp({ + email, + otp: validOtp, + }); const correctResponse = await api.handle( new Request(`http://localhost:3000${correctRequestInfo.url}`, { method: correctRequestInfo.method, @@ -507,7 +520,7 @@ Deno.test( const email = `test-${crypto.randomUUID()}@example.com`; // Request initial OTP - const requestOtpInfo = prepareRequestOtp({ email }); + const requestOtpInfo = AuthApiTypes.prepareRequestOtp({ email }); await api.handle( new Request(`http://localhost:3000${requestOtpInfo.url}`, { method: requestOtpInfo.method, @@ -521,7 +534,10 @@ Deno.test( // Make some failed attempts const wrongOtp = "000000"; for (let i = 0; i < 2; i++) { - const verifyInfo = prepareVerifyOtp({ email, otp: wrongOtp }); + const verifyInfo = AuthApiTypes.prepareVerifyOtp({ + email, + otp: wrongOtp, + }); await api.handle( new Request(`http://localhost:3000${verifyInfo.url}`, { method: verifyInfo.method, @@ -569,7 +585,7 @@ Deno.test( assertEquals(user.otp_attempts, 0); // Should be reset // Should be able to verify with new OTP - const newVerifyInfo = prepareVerifyOtp({ email, otp }); + const newVerifyInfo = AuthApiTypes.prepareVerifyOtp({ email, otp }); const response = await api.handle( new Request(`http://localhost:3000${newVerifyInfo.url}`, { method: newVerifyInfo.method, @@ -603,7 +619,10 @@ Deno.test("verify otp - should track failed attempts correctly", async () => { const wrongOtp = "000000"; // Make first failed attempt - const firstAttemptInfo = prepareVerifyOtp({ email, otp: wrongOtp }); + const firstAttemptInfo = AuthApiTypes.prepareVerifyOtp({ + email, + otp: wrongOtp, + }); const firstResponse = await api.handle( new Request(`http://localhost:3000${firstAttemptInfo.url}`, { method: firstAttemptInfo.method, @@ -624,7 +643,10 @@ Deno.test("verify otp - should track failed attempts correctly", async () => { assertEquals(user.otp_attempts, 1); // Make second failed attempt - const secondAttemptInfo = prepareVerifyOtp({ email, otp: wrongOtp }); + const secondAttemptInfo = AuthApiTypes.prepareVerifyOtp({ + email, + otp: wrongOtp, + }); const secondResponse = await api.handle( new Request(`http://localhost:3000${secondAttemptInfo.url}`, { method: secondAttemptInfo.method, @@ -656,7 +678,7 @@ Deno.test("get user info", async () => { try { const { token, userId, email } = await createTestUserAndToken(); - const requestInfo = prepareMe(); + const requestInfo = AuthApiTypes.prepareMe(); const response = await api.handle( new Request(`http://localhost:3000${requestInfo.url}`, { diff --git a/packages/core/src/api/auth/router.ts b/packages/core/src/api/auth/router.ts index 985db9d..8a47325 100644 --- a/packages/core/src/api/auth/router.ts +++ b/packages/core/src/api/auth/router.ts @@ -1,7 +1,7 @@ import { Router, Status } from "@oak/oak"; import { AuthService } from "./service.ts"; -import { requestOtpSchema, verifyOtpSchema } from "./types.ts"; import { authMiddleware, getSession } from "./middleware.ts"; +import z from "zod"; const router = new Router(); @@ -9,6 +9,10 @@ const router = new Router(); router.post("/requestOtp", async (ctx) => { const body = await ctx.request.body.json(); + const requestOtpSchema = z.object({ + email: z.string().email(), + }); + const parsedBody = requestOtpSchema.safeParse(body); if (!parsedBody.success) { @@ -34,6 +38,11 @@ router.post("/requestOtp", async (ctx) => { router.post("/verifyOtp", async (ctx) => { const body = await ctx.request.body.json(); + const verifyOtpSchema = z.object({ + email: z.string().email(), + otp: z.string(), + }); + const parsedBody = verifyOtpSchema.safeParse(body); if (!parsedBody.success) { diff --git a/packages/core/src/api/auth/service.ts b/packages/core/src/api/auth/service.ts index fa99816..c425cd9 100644 --- a/packages/core/src/api/auth/service.ts +++ b/packages/core/src/api/auth/service.ts @@ -1,16 +1,10 @@ import { create, getNumericDate, verify } from "djwt"; -import { db } from "../../db/database.ts"; -import settings from "../../settings.ts"; +import { ADMIN_ROLE, db, type User } from "@stackcore/db"; +import settings from "@stackcore/settings"; import { sendOtpEmail, sendWelcomeEmail } from "../../email/index.ts"; import { StripeService } from "../../stripe/index.ts"; -import { - BASIC_PRODUCT, - MONTHLY_BILLING_CYCLE, - shouldHaveAccess, -} from "../../db/models/workspace.ts"; -import { ADMIN_ROLE } from "../../db/models/member.ts"; -import type { User } from "../../db/models/user.ts"; -import { type Session, sessionSchema } from "./types.ts"; +import { type AuthApiTypes, BillingApiTypes } from "@stackcore/coreApiTypes"; +import z from "zod"; export const secretCryptoKey = await crypto.subtle.importKey( "raw", @@ -191,12 +185,14 @@ export class AuthService { ); const subscription = await stripeService.createSubscription( customer.id, - BASIC_PRODUCT, - MONTHLY_BILLING_CYCLE, + BillingApiTypes.STRIPE_BASIC_PRODUCT, + BillingApiTypes.STRIPE_MONTHLY_BILLING_CYCLE, settings.STRIPE.BILLING_THRESHOLD_BASIC, ); - const accessEnabled = shouldHaveAccess(subscription.status); + const accessEnabled = stripeService.shouldHaveAccess( + subscription.status, + ); await trx .updateTable("workspace") @@ -243,7 +239,7 @@ export class AuthService { userId: user.id, email: user.email, exp: exp, - } as Session & { exp: number }, + } as AuthApiTypes.Session & { exp: number }, secretCryptoKey, ); } @@ -257,6 +253,11 @@ export class AuthService { try { const payload = await verify(token, secretCryptoKey); + const sessionSchema = z.object({ + userId: z.number(), + email: z.string(), + }); + const parsedPayload = sessionSchema.safeParse({ userId: payload.userId, email: payload.email, diff --git a/packages/core/src/api/auth/types.ts b/packages/core/src/api/auth/types.ts deleted file mode 100644 index 1bbb38b..0000000 --- a/packages/core/src/api/auth/types.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { z } from "zod"; - -export const sessionSchema = z.object({ - userId: z.number(), - email: z.string(), -}); -export type Session = z.infer; - -export const requestOtpSchema = z.object({ - email: z.string().email(), -}); -export type RequestOtpPayload = z.infer; - -export function prepareRequestOtp(payload: RequestOtpPayload) { - return { - url: "/auth/requestOtp", - method: "POST", - body: payload, - }; -} - -export const verifyOtpSchema = z.object({ - email: z.string().email(), - otp: z.string(), -}); -export type VerifyOtpPayload = z.infer; - -export function prepareVerifyOtp(payload: VerifyOtpPayload) { - return { - url: "/auth/verifyOtp", - method: "POST", - body: payload, - }; -} - -export type VerifyOtpResponse = { - token: string; -}; - -export function prepareMe() { - return { - url: "/auth/me", - method: "GET", - body: undefined, - }; -} diff --git a/packages/core/src/api/billing/router.test.ts b/packages/core/src/api/billing/router.test.ts index eddcf76..d136634 100644 --- a/packages/core/src/api/billing/router.test.ts +++ b/packages/core/src/api/billing/router.test.ts @@ -1,17 +1,16 @@ import { assertEquals } from "@std/assert"; import api from "../index.ts"; -import { db, destroyKyselyDb, initKyselyDb } from "../../db/database.ts"; +import { + type ADMIN_ROLE, + db, + destroyKyselyDb, + initKyselyDb, + MEMBER_ROLE, +} from "@stackcore/db"; import { resetTables } from "../../testHelpers/db.ts"; import { createTestUserAndToken } from "../../testHelpers/auth.ts"; import { WorkspaceService } from "../workspace/service.ts"; -import { BillingApiTypes } from "../responseType.ts"; -import { type ADMIN_ROLE, MEMBER_ROLE } from "../../db/models/member.ts"; -import { - BASIC_PRODUCT, - MONTHLY_BILLING_CYCLE, - PRO_PRODUCT, - YEARLY_BILLING_CYCLE, -} from "../../db/models/workspace.ts"; +import { BillingApiTypes } from "@stackcore/coreApiTypes"; import { StripeService } from "../../stripe/index.ts"; // Helper function to create a team workspace and add user as admin @@ -61,8 +60,8 @@ async function setupBasicSubscription(workspaceId: number) { const stripeService = new StripeService(); const subscription = await stripeService.createSubscription( workspace.stripe_customer_id, - BASIC_PRODUCT, - MONTHLY_BILLING_CYCLE, + BillingApiTypes.STRIPE_BASIC_PRODUCT, + BillingApiTypes.STRIPE_MONTHLY_BILLING_CYCLE, null, ); @@ -424,8 +423,8 @@ Deno.test("upgrade subscription from basic to pro", async () => { const { url, method, body } = BillingApiTypes.prepareUpgradeSubscription({ workspaceId: workspace.id, - product: PRO_PRODUCT, - billingCycle: MONTHLY_BILLING_CYCLE, + product: BillingApiTypes.STRIPE_PRO_PRODUCT, + billingCycle: BillingApiTypes.STRIPE_MONTHLY_BILLING_CYCLE, }); const response = await api.handle( @@ -475,8 +474,8 @@ Deno.test("upgrade subscription from basic to pro - with matching price ID and p const { url, method, body } = BillingApiTypes.prepareUpgradeSubscription({ workspaceId: workspace.id, - product: PRO_PRODUCT, - billingCycle: MONTHLY_BILLING_CYCLE, + product: BillingApiTypes.STRIPE_PRO_PRODUCT, + billingCycle: BillingApiTypes.STRIPE_MONTHLY_BILLING_CYCLE, }); const response = await api.handle( @@ -520,8 +519,8 @@ Deno.test("upgrade subscription - not a member", async () => { const { url, method, body } = BillingApiTypes.prepareUpgradeSubscription({ workspaceId: workspace.id, - product: PRO_PRODUCT, - billingCycle: MONTHLY_BILLING_CYCLE, + product: BillingApiTypes.STRIPE_PRO_PRODUCT, + billingCycle: BillingApiTypes.STRIPE_MONTHLY_BILLING_CYCLE, }); const response = await api.handle( @@ -563,8 +562,8 @@ Deno.test("upgrade subscription - not an admin", async () => { const { url, method, body } = BillingApiTypes.prepareUpgradeSubscription({ workspaceId: workspace.id, - product: PRO_PRODUCT, - billingCycle: MONTHLY_BILLING_CYCLE, + product: BillingApiTypes.STRIPE_PRO_PRODUCT, + billingCycle: BillingApiTypes.STRIPE_MONTHLY_BILLING_CYCLE, }); const response = await api.handle( @@ -604,7 +603,7 @@ Deno.test("upgrade subscription - invalid product", async () => { body: JSON.stringify({ workspaceId: workspace.id, product: "invalid_product", - billingCycle: MONTHLY_BILLING_CYCLE, + billingCycle: BillingApiTypes.STRIPE_MONTHLY_BILLING_CYCLE, }), headers: { "Content-Type": "application/json", @@ -638,8 +637,8 @@ Deno.test("upgrade subscription to same product and billing cycle", async () => const { url, method, body } = BillingApiTypes.prepareUpgradeSubscription({ workspaceId: workspace.id, - product: BASIC_PRODUCT, // Same product - billingCycle: MONTHLY_BILLING_CYCLE, // Same billing cycle + product: BillingApiTypes.STRIPE_BASIC_PRODUCT, // Same product + billingCycle: BillingApiTypes.STRIPE_MONTHLY_BILLING_CYCLE, // Same billing cycle }); const response = await api.handle( @@ -687,8 +686,8 @@ Deno.test("upgrade subscription - cannot upgrade from pro to basic (inferior pro } await stripeService.createSubscription( workspace.stripe_customer_id, - PRO_PRODUCT, - MONTHLY_BILLING_CYCLE, + BillingApiTypes.STRIPE_PRO_PRODUCT, + BillingApiTypes.STRIPE_MONTHLY_BILLING_CYCLE, null, ); @@ -701,8 +700,8 @@ Deno.test("upgrade subscription - cannot upgrade from pro to basic (inferior pro const { url, method, body } = BillingApiTypes.prepareUpgradeSubscription({ workspaceId: workspace.id, - product: BASIC_PRODUCT, // Trying to "upgrade" to inferior product - billingCycle: MONTHLY_BILLING_CYCLE, + product: BillingApiTypes.STRIPE_BASIC_PRODUCT, // Trying to "upgrade" to inferior product + billingCycle: BillingApiTypes.STRIPE_MONTHLY_BILLING_CYCLE, }); const response = await api.handle( @@ -747,15 +746,15 @@ Deno.test("downgrade subscription from pro to basic", async () => { } await stripeService.createSubscription( workspace.stripe_customer_id, - PRO_PRODUCT, - MONTHLY_BILLING_CYCLE, + BillingApiTypes.STRIPE_PRO_PRODUCT, + BillingApiTypes.STRIPE_MONTHLY_BILLING_CYCLE, null, ); const { url, method, body } = BillingApiTypes.prepareDowngradeSubscription({ workspaceId: workspace.id, - product: BASIC_PRODUCT, - billingCycle: MONTHLY_BILLING_CYCLE, + product: BillingApiTypes.STRIPE_BASIC_PRODUCT, + billingCycle: BillingApiTypes.STRIPE_MONTHLY_BILLING_CYCLE, }); const response = await api.handle( @@ -795,8 +794,8 @@ Deno.test("downgrade subscription - cannot downgrade to superior product", async const { url, method, body } = BillingApiTypes.prepareDowngradeSubscription({ workspaceId: workspace.id, - product: PRO_PRODUCT, // Superior product - billingCycle: MONTHLY_BILLING_CYCLE, + product: BillingApiTypes.STRIPE_PRO_PRODUCT, // Superior product + billingCycle: BillingApiTypes.STRIPE_MONTHLY_BILLING_CYCLE, }); const response = await api.handle( @@ -841,8 +840,8 @@ Deno.test("downgrade subscription from pro to basic - with matching price ID", a } await stripeService.createSubscription( workspace.stripe_customer_id, - PRO_PRODUCT, - MONTHLY_BILLING_CYCLE, + BillingApiTypes.STRIPE_PRO_PRODUCT, + BillingApiTypes.STRIPE_MONTHLY_BILLING_CYCLE, null, ); @@ -854,8 +853,8 @@ Deno.test("downgrade subscription from pro to basic - with matching price ID", a const { url, method, body } = BillingApiTypes.prepareDowngradeSubscription({ workspaceId: workspace.id, - product: BASIC_PRODUCT, - billingCycle: MONTHLY_BILLING_CYCLE, + product: BillingApiTypes.STRIPE_BASIC_PRODUCT, + billingCycle: BillingApiTypes.STRIPE_MONTHLY_BILLING_CYCLE, }); const response = await api.handle( @@ -1127,8 +1126,8 @@ Deno.test("all endpoints require authentication", async () => { method: "POST", body: { workspaceId: workspace.id, - product: PRO_PRODUCT, - billingCycle: MONTHLY_BILLING_CYCLE, + product: BillingApiTypes.STRIPE_PRO_PRODUCT, + billingCycle: BillingApiTypes.STRIPE_MONTHLY_BILLING_CYCLE, }, }, { @@ -1136,8 +1135,8 @@ Deno.test("all endpoints require authentication", async () => { method: "POST", body: { workspaceId: workspace.id, - product: BASIC_PRODUCT, - billingCycle: MONTHLY_BILLING_CYCLE, + product: BillingApiTypes.STRIPE_BASIC_PRODUCT, + billingCycle: BillingApiTypes.STRIPE_MONTHLY_BILLING_CYCLE, }, }, { @@ -1206,16 +1205,16 @@ Deno.test("upgrade subscription - yearly to monthly billing cycle change", async } await stripeService.createSubscription( workspace.stripe_customer_id, - PRO_PRODUCT, - YEARLY_BILLING_CYCLE, + BillingApiTypes.STRIPE_PRO_PRODUCT, + BillingApiTypes.STRIPE_YEARLY_BILLING_CYCLE, null, ); // Try to "upgrade" to monthly (which is actually inferior) const { url, method, body } = BillingApiTypes.prepareUpgradeSubscription({ workspaceId: workspace.id, - product: PRO_PRODUCT, - billingCycle: MONTHLY_BILLING_CYCLE, + product: BillingApiTypes.STRIPE_PRO_PRODUCT, + billingCycle: BillingApiTypes.STRIPE_MONTHLY_BILLING_CYCLE, }); const response = await api.handle( @@ -1257,16 +1256,16 @@ Deno.test("downgrade subscription - monthly to yearly billing cycle change", asy } await stripeService.createSubscription( workspace.stripe_customer_id, - PRO_PRODUCT, - MONTHLY_BILLING_CYCLE, + BillingApiTypes.STRIPE_PRO_PRODUCT, + BillingApiTypes.STRIPE_MONTHLY_BILLING_CYCLE, null, ); // Try to "downgrade" to yearly (which is actually superior) const { url, method, body } = BillingApiTypes.prepareDowngradeSubscription({ workspaceId: workspace.id, - product: PRO_PRODUCT, - billingCycle: YEARLY_BILLING_CYCLE, + product: BillingApiTypes.STRIPE_PRO_PRODUCT, + billingCycle: BillingApiTypes.STRIPE_YEARLY_BILLING_CYCLE, }); const response = await api.handle( @@ -1311,8 +1310,8 @@ Deno.test("upgrade subscription - pro monthly to pro yearly (billing cycle upgra } await stripeService.createSubscription( workspace.stripe_customer_id, - PRO_PRODUCT, - MONTHLY_BILLING_CYCLE, + BillingApiTypes.STRIPE_PRO_PRODUCT, + BillingApiTypes.STRIPE_MONTHLY_BILLING_CYCLE, null, ); @@ -1325,8 +1324,8 @@ Deno.test("upgrade subscription - pro monthly to pro yearly (billing cycle upgra const { url, method, body } = BillingApiTypes.prepareUpgradeSubscription({ workspaceId: workspace.id, - product: PRO_PRODUCT, // Same product - billingCycle: YEARLY_BILLING_CYCLE, // Upgrade to yearly + product: BillingApiTypes.STRIPE_PRO_PRODUCT, // Same product + billingCycle: BillingApiTypes.STRIPE_YEARLY_BILLING_CYCLE, // Upgrade to yearly }); const response = await api.handle( @@ -1380,8 +1379,8 @@ Deno.test( const { url, method, body } = BillingApiTypes.prepareUpgradeSubscription({ workspaceId: workspace.id, - product: BASIC_PRODUCT, // Same product - billingCycle: MONTHLY_BILLING_CYCLE, // Same billing cycle + product: BillingApiTypes.STRIPE_BASIC_PRODUCT, // Same product + billingCycle: BillingApiTypes.STRIPE_MONTHLY_BILLING_CYCLE, // Same billing cycle }); const response = await api.handle( diff --git a/packages/core/src/api/billing/router.ts b/packages/core/src/api/billing/router.ts index 65dfc41..241c59c 100644 --- a/packages/core/src/api/billing/router.ts +++ b/packages/core/src/api/billing/router.ts @@ -1,11 +1,5 @@ import { Router, Status } from "@oak/oak"; -import { - createPortalSessionRequestSchema, - type CreatePortalSessionResponse, - downgradeSubscriptionRequestSchema, - type SubscriptionDetails, - upgradeSubscriptionRequestSchema, -} from "./types.ts"; +import { BillingApiTypes } from "@stackcore/coreApiTypes"; import { BillingService } from "./service.ts"; import { authMiddleware, getSession } from "../auth/middleware.ts"; import { z } from "zod"; @@ -44,7 +38,8 @@ router.get("/subscription", authMiddleware, async (ctx) => { } ctx.response.status = Status.OK; - ctx.response.body = result.subscription as SubscriptionDetails; + ctx.response.body = result + .subscription as BillingApiTypes.SubscriptionDetails; }); router.post("/subscription/upgrade", authMiddleware, async (ctx) => { @@ -52,7 +47,22 @@ router.post("/subscription/upgrade", authMiddleware, async (ctx) => { const body = await ctx.request.body.json(); - const parsedBody = upgradeSubscriptionRequestSchema.safeParse(body); + const upgradeSubscriptionRequestSchema = z.object({ + workspaceId: z.number(), + product: z.enum([ + BillingApiTypes.STRIPE_BASIC_PRODUCT, + BillingApiTypes.STRIPE_PRO_PRODUCT, + BillingApiTypes.STRIPE_PREMIUM_PRODUCT, + ]), + billingCycle: z.enum([ + BillingApiTypes.STRIPE_MONTHLY_BILLING_CYCLE, + BillingApiTypes.STRIPE_YEARLY_BILLING_CYCLE, + ]), + }); + + const parsedBody = upgradeSubscriptionRequestSchema.safeParse( + body, + ); if (!parsedBody.success) { ctx.response.status = Status.BadRequest; @@ -83,7 +93,23 @@ router.post("/subscription/downgrade", authMiddleware, async (ctx) => { const body = await ctx.request.body.json(); - const parsedBody = downgradeSubscriptionRequestSchema.safeParse(body); + const downgradeSubscriptionRequestSchema = z.object({ + workspaceId: z.number(), + product: z.enum([ + BillingApiTypes.STRIPE_BASIC_PRODUCT, + BillingApiTypes.STRIPE_PRO_PRODUCT, + BillingApiTypes.STRIPE_PREMIUM_PRODUCT, + ]), + billingCycle: z.enum([ + BillingApiTypes.STRIPE_MONTHLY_BILLING_CYCLE, + BillingApiTypes.STRIPE_YEARLY_BILLING_CYCLE, + ]), + }); + + const parsedBody = downgradeSubscriptionRequestSchema + .safeParse( + body, + ); if (!parsedBody.success) { ctx.response.status = Status.BadRequest; @@ -114,7 +140,14 @@ router.post("/portal", authMiddleware, async (ctx) => { const body = await ctx.request.body.json(); - const parsedBody = createPortalSessionRequestSchema.safeParse(body); + const createPortalSessionRequestSchema = z.object({ + workspaceId: z.number(), + returnUrl: z.string(), + }); + + const parsedBody = createPortalSessionRequestSchema.safeParse( + body, + ); if (!parsedBody.success) { ctx.response.status = Status.BadRequest; @@ -140,7 +173,7 @@ router.post("/portal", authMiddleware, async (ctx) => { } ctx.response.status = Status.OK; - ctx.response.body = { url } as CreatePortalSessionResponse; + ctx.response.body = { url } as BillingApiTypes.CreatePortalSessionResponse; }); router.post("/portal/paymentMethod", authMiddleware, async (ctx) => { @@ -148,7 +181,14 @@ router.post("/portal/paymentMethod", authMiddleware, async (ctx) => { const body = await ctx.request.body.json(); - const parsedBody = createPortalSessionRequestSchema.safeParse(body); + const createPortalSessionRequestSchema = z.object({ + workspaceId: z.number(), + returnUrl: z.string(), + }); + + const parsedBody = createPortalSessionRequestSchema.safeParse( + body, + ); if (!parsedBody.success) { ctx.response.status = Status.BadRequest; @@ -174,7 +214,7 @@ router.post("/portal/paymentMethod", authMiddleware, async (ctx) => { } ctx.response.status = Status.OK; - ctx.response.body = { url } as CreatePortalSessionResponse; + ctx.response.body = { url } as BillingApiTypes.CreatePortalSessionResponse; }); router.post("/webhook", async (ctx) => { diff --git a/packages/core/src/api/billing/service.ts b/packages/core/src/api/billing/service.ts index 67f6f09..66c28f5 100644 --- a/packages/core/src/api/billing/service.ts +++ b/packages/core/src/api/billing/service.ts @@ -1,25 +1,13 @@ import { type Context, Status } from "@oak/oak"; -import { db } from "../../db/database.ts"; +import { ADMIN_ROLE, db } from "@stackcore/db"; import { StripeService } from "../../stripe/index.ts"; import type { Stripe } from "stripe"; -import { - BASIC_PRODUCT, - CUSTOM_PRODUCT, - MONTHLY_BILLING_CYCLE, - PREMIUM_PRODUCT, - PRO_PRODUCT, - type StripeBillingCycle, - type StripeProduct, - YEARLY_BILLING_CYCLE, -} from "../../db/models/workspace.ts"; -import { shouldHaveAccess } from "../../db/models/workspace.ts"; -import { ADMIN_ROLE } from "../../db/models/member.ts"; import { sendSubscriptionDowngradedEmail, sendSubscriptionUpgradedEmail, } from "../../email/index.ts"; -import settings from "../../settings.ts"; -import type { SubscriptionDetails } from "./types.ts"; +import settings from "@stackcore/settings"; +import { BillingApiTypes } from "@stackcore/coreApiTypes"; const notAMemberOfWorkspaceError = "not_a_member_of_workspace"; const notAnAdminError = "not_an_admin"; @@ -38,7 +26,9 @@ export class BillingService { public async getSubscription( userId: number, workspaceId: number, - ): Promise<{ subscription?: SubscriptionDetails; error?: string }> { + ): Promise< + { subscription?: BillingApiTypes.SubscriptionDetails; error?: string } + > { const isMember = await db .selectFrom("member") .select("user_id") @@ -88,40 +78,48 @@ export class BillingService { const priceId = subscription.items.data[0].price.id; const priceIdMap = { - [settings.STRIPE.PRODUCTS[BASIC_PRODUCT][MONTHLY_BILLING_CYCLE].PRICE_ID]: - { - product: BASIC_PRODUCT as StripeProduct, - billingCycle: MONTHLY_BILLING_CYCLE as StripeBillingCycle, - }, - [settings.STRIPE.PRODUCTS[PRO_PRODUCT][MONTHLY_BILLING_CYCLE].PRICE_ID]: { - product: PRO_PRODUCT as StripeProduct, - billingCycle: MONTHLY_BILLING_CYCLE as StripeBillingCycle, + [settings.STRIPE.PRODUCTS.BASIC.MONTHLY.PRICE_ID]: { + product: BillingApiTypes + .STRIPE_BASIC_PRODUCT as BillingApiTypes.StripeProduct, + billingCycle: BillingApiTypes + .STRIPE_MONTHLY_BILLING_CYCLE as BillingApiTypes.StripeBillingCycle, + }, + [settings.STRIPE.PRODUCTS.PRO.MONTHLY.PRICE_ID]: { + product: BillingApiTypes + .STRIPE_PRO_PRODUCT as BillingApiTypes.StripeProduct, + billingCycle: BillingApiTypes + .STRIPE_MONTHLY_BILLING_CYCLE as BillingApiTypes.StripeBillingCycle, }, - [settings.STRIPE.PRODUCTS[PRO_PRODUCT][YEARLY_BILLING_CYCLE].PRICE_ID]: { - product: PRO_PRODUCT as StripeProduct, - billingCycle: YEARLY_BILLING_CYCLE as StripeBillingCycle, + [settings.STRIPE.PRODUCTS.PRO.YEARLY.PRICE_ID]: { + product: BillingApiTypes + .STRIPE_PRO_PRODUCT as BillingApiTypes.StripeProduct, + billingCycle: BillingApiTypes + .STRIPE_YEARLY_BILLING_CYCLE as BillingApiTypes.StripeBillingCycle, }, [ - settings.STRIPE.PRODUCTS[PREMIUM_PRODUCT][MONTHLY_BILLING_CYCLE] - .PRICE_ID + settings.STRIPE.PRODUCTS.PREMIUM.MONTHLY.PRICE_ID ]: { - product: PREMIUM_PRODUCT as StripeProduct, - billingCycle: MONTHLY_BILLING_CYCLE as StripeBillingCycle, + product: BillingApiTypes + .STRIPE_PREMIUM_PRODUCT as BillingApiTypes.StripeProduct, + billingCycle: BillingApiTypes + .STRIPE_MONTHLY_BILLING_CYCLE as BillingApiTypes.StripeBillingCycle, }, [ - settings.STRIPE.PRODUCTS[PREMIUM_PRODUCT][YEARLY_BILLING_CYCLE].PRICE_ID + settings.STRIPE.PRODUCTS.PREMIUM.YEARLY.PRICE_ID ]: { - product: PREMIUM_PRODUCT as StripeProduct, - billingCycle: YEARLY_BILLING_CYCLE as StripeBillingCycle, + product: BillingApiTypes + .STRIPE_PREMIUM_PRODUCT as BillingApiTypes.StripeProduct, + billingCycle: BillingApiTypes + .STRIPE_YEARLY_BILLING_CYCLE as BillingApiTypes.StripeBillingCycle, }, }; const productInfo = priceIdMap[priceId]; if (!productInfo) { - const subscriptionDetails: SubscriptionDetails = { + const subscriptionDetails: BillingApiTypes.SubscriptionDetails = { currentUsage, - product: CUSTOM_PRODUCT, + product: BillingApiTypes.STRIPE_CUSTOM_PRODUCT, billingCycle: null, hasDefaultPaymentMethod, cancelAt: null, @@ -133,16 +131,18 @@ export class BillingService { } const cancelAt = subscription.cancel_at; - let newProductWhenCanceled: StripeProduct | null = null; - let newBillingCycleWhenCanceled: StripeBillingCycle | null = null; + let newProductWhenCanceled: BillingApiTypes.StripeProduct | null = null; + let newBillingCycleWhenCanceled: BillingApiTypes.StripeBillingCycle | null = + null; if (cancelAt) { newProductWhenCanceled = subscription.metadata - .new_product_when_canceled as StripeProduct || null; + .new_product_when_canceled as BillingApiTypes.StripeProduct || null; newBillingCycleWhenCanceled = subscription.metadata - .new_billing_cycle_when_canceled as StripeBillingCycle || null; + .new_billing_cycle_when_canceled as BillingApiTypes.StripeBillingCycle || + null; } - const subscriptionDetails: SubscriptionDetails = { + const subscriptionDetails: BillingApiTypes.SubscriptionDetails = { currentUsage, product: productInfo.product, billingCycle: productInfo.billingCycle, @@ -234,8 +234,8 @@ export class BillingService { public async upgradeSubscription( userId: number, workspaceId: number, - product: StripeProduct, - billingCycle: StripeBillingCycle, + product: BillingApiTypes.StripeProduct, + billingCycle: BillingApiTypes.StripeBillingCycle, ): Promise<{ error?: string }> { const isMember = await db .selectFrom("member") @@ -276,15 +276,22 @@ export class BillingService { // Check if user is upgrading to a superior product switch (currentSubscription.product) { - case CUSTOM_PRODUCT: + case BillingApiTypes.STRIPE_CUSTOM_PRODUCT: return { error: cannotChangeCustomProductError }; - case PREMIUM_PRODUCT: - if ([BASIC_PRODUCT, PRO_PRODUCT].includes(product)) { + case BillingApiTypes.STRIPE_PREMIUM_PRODUCT: + if ( + [ + BillingApiTypes.STRIPE_BASIC_PRODUCT as string, + BillingApiTypes.STRIPE_PRO_PRODUCT as string, + ].includes(product) + ) { return { error: cannotUpgradeToInferiorProductError }; } break; - case PRO_PRODUCT: - if ([BASIC_PRODUCT].includes(product)) { + case BillingApiTypes.STRIPE_PRO_PRODUCT: + if ( + [BillingApiTypes.STRIPE_BASIC_PRODUCT as string].includes(product) + ) { return { error: cannotUpgradeToInferiorProductError }; } break; @@ -293,8 +300,8 @@ export class BillingService { // Check if user is upgrading to a superior billing cycle if (currentSubscription.product === product) { switch (currentSubscription.billingCycle) { - case YEARLY_BILLING_CYCLE: - if (billingCycle === MONTHLY_BILLING_CYCLE) { + case BillingApiTypes.STRIPE_YEARLY_BILLING_CYCLE: + if (billingCycle === BillingApiTypes.STRIPE_MONTHLY_BILLING_CYCLE) { return { error: cannotUpgradeToInferiorProductError }; } break; @@ -368,8 +375,8 @@ export class BillingService { public async downgradeSubscription( userId: number, workspaceId: number, - product: StripeProduct, - billingCycle: StripeBillingCycle, + product: BillingApiTypes.StripeProduct, + billingCycle: BillingApiTypes.StripeBillingCycle, ) { const isMember = await db .selectFrom("member") @@ -406,15 +413,22 @@ export class BillingService { // check if user is downgrading to an inferior product switch (currentSubscription.product) { - case CUSTOM_PRODUCT: + case BillingApiTypes.STRIPE_CUSTOM_PRODUCT: return { error: cannotChangeCustomProductError }; - case PRO_PRODUCT: - if ([PREMIUM_PRODUCT].includes(product)) { + case BillingApiTypes.STRIPE_PRO_PRODUCT: + if ( + [BillingApiTypes.STRIPE_PREMIUM_PRODUCT as string].includes(product) + ) { return { error: cannotDowngradeToSuperiorProductError }; } break; - case BASIC_PRODUCT: - if ([PRO_PRODUCT, PREMIUM_PRODUCT].includes(product)) { + case BillingApiTypes.STRIPE_BASIC_PRODUCT: + if ( + [ + BillingApiTypes.STRIPE_PRO_PRODUCT as string, + BillingApiTypes.STRIPE_PREMIUM_PRODUCT as string, + ].includes(product) + ) { return { error: cannotDowngradeToSuperiorProductError }; } break; @@ -423,12 +437,12 @@ export class BillingService { // Check if the user is downgrading to an inferior billing cycle if (currentSubscription.product === product) { switch (currentSubscription.billingCycle) { - case MONTHLY_BILLING_CYCLE: - if (billingCycle === YEARLY_BILLING_CYCLE) { + case BillingApiTypes.STRIPE_MONTHLY_BILLING_CYCLE: + if (billingCycle === BillingApiTypes.STRIPE_YEARLY_BILLING_CYCLE) { return { error: cannotDowngradeToSuperiorProductError }; } break; - case YEARLY_BILLING_CYCLE: + case BillingApiTypes.STRIPE_YEARLY_BILLING_CYCLE: // this is allowed break; } @@ -576,12 +590,21 @@ export class BillingService { return; } - if (![BASIC_PRODUCT, PRO_PRODUCT, PREMIUM_PRODUCT].includes(newProduct)) { + if ( + ![ + BillingApiTypes.STRIPE_BASIC_PRODUCT as string, + BillingApiTypes.STRIPE_PRO_PRODUCT as string, + BillingApiTypes.STRIPE_PREMIUM_PRODUCT as string, + ].includes(newProduct) + ) { throw new Error(`Invalid product: ${newProduct}`); } if ( - ![MONTHLY_BILLING_CYCLE, YEARLY_BILLING_CYCLE].includes(newLicensePeriod) + ![ + BillingApiTypes.STRIPE_MONTHLY_BILLING_CYCLE as string, + BillingApiTypes.STRIPE_YEARLY_BILLING_CYCLE as string, + ].includes(newLicensePeriod) ) { throw new Error(`Invalid license period: ${newLicensePeriod}`); } @@ -596,12 +619,14 @@ export class BillingService { const newSubscription = await stripeService.createSubscription( customer.id, - newProduct as StripeProduct, - newLicensePeriod as StripeBillingCycle, + newProduct as BillingApiTypes.StripeProduct, + newLicensePeriod as BillingApiTypes.StripeBillingCycle, null, ); - const accessEnabled = shouldHaveAccess(newSubscription.status); + const accessEnabled = stripeService.shouldHaveAccess( + newSubscription.status, + ); await db .updateTable("workspace") @@ -627,10 +652,10 @@ export class BillingService { return; } - const accessEnabled = shouldHaveAccess(subscription.status); - const stripeService = new StripeService(); + const accessEnabled = stripeService.shouldHaveAccess(subscription.status); + await db .updateTable("workspace") .set({ @@ -657,7 +682,7 @@ export class BillingService { customer.id, ); - const accessEnabled = shouldHaveAccess(subscription.status); + const accessEnabled = stripeService.shouldHaveAccess(subscription.status); await db .updateTable("workspace") diff --git a/packages/core/src/api/health/router.test.ts b/packages/core/src/api/health/router.test.ts index 82f71f2..fb27a92 100644 --- a/packages/core/src/api/health/router.test.ts +++ b/packages/core/src/api/health/router.test.ts @@ -1,6 +1,6 @@ import { assertEquals, assertNotEquals } from "@std/assert"; import api from "../index.ts"; -import { destroyKyselyDb, initKyselyDb } from "../../db/database.ts"; +import { destroyKyselyDb, initKyselyDb } from "@stackcore/db"; import { resetTables } from "../../testHelpers/db.ts"; // GET /health/liveness diff --git a/packages/core/src/api/health/router.ts b/packages/core/src/api/health/router.ts index 7fb0567..ee6d244 100644 --- a/packages/core/src/api/health/router.ts +++ b/packages/core/src/api/health/router.ts @@ -1,5 +1,5 @@ import { Router, Status } from "@oak/oak"; -import { db } from "../../db/database.ts"; +import { db } from "@stackcore/db"; const router = new Router(); diff --git a/packages/core/src/api/index.ts b/packages/core/src/api/index.ts index 62b1f6d..4c45e53 100644 --- a/packages/core/src/api/index.ts +++ b/packages/core/src/api/index.ts @@ -8,6 +8,7 @@ import manifestRouter from "./manifest/router.ts"; import healthRouter from "./health/router.ts"; import billingRouter from "./billing/router.ts"; import tokenRouter from "./token/router.ts"; +import labelingRouter from "./labeling/router.ts"; const api = new Application(); @@ -63,4 +64,7 @@ api.use(manifestRouter.allowedMethods()); api.use(billingRouter.prefix("/billing").routes()); api.use(billingRouter.allowedMethods()); +api.use(labelingRouter.prefix("/labeling").routes()); +api.use(labelingRouter.allowedMethods()); + export default api; diff --git a/packages/core/src/api/invitation/router.test.ts b/packages/core/src/api/invitation/router.test.ts index 617fc8a..6708d30 100644 --- a/packages/core/src/api/invitation/router.test.ts +++ b/packages/core/src/api/invitation/router.test.ts @@ -1,6 +1,6 @@ import { assertEquals, assertNotEquals } from "@std/assert"; import api from "../index.ts"; -import { db, destroyKyselyDb, initKyselyDb } from "../../db/database.ts"; +import { db, destroyKyselyDb, initKyselyDb } from "@stackcore/db"; import { resetTables } from "../../testHelpers/db.ts"; import { WorkspaceService } from "../workspace/service.ts"; import { InvitationService } from "./service.ts"; @@ -13,7 +13,7 @@ import { notMemberOfWorkspaceError, workspaceNotTeamError, } from "./service.ts"; -import { prepareClaimInvitation, prepareCreateInvitation } from "./types.ts"; +import { InvitationApiTypes } from "@stackcore/coreApiTypes"; // POST /:workspaceId/invite (create invitation) Deno.test("create invitation - with invalid email", async () => { @@ -37,7 +37,7 @@ Deno.test("create invitation - with invalid email", async () => { .where("name", "=", "Test Workspace") .executeTakeFirstOrThrow(); - const { url, method, body } = prepareCreateInvitation({ + const { url, method, body } = InvitationApiTypes.prepareCreateInvitation({ workspaceId: workspace.id, email: "invalid-email", returnUrl: "http://localhost:3000/invitations/claim", @@ -82,7 +82,7 @@ Deno.test("create invitation - with personal workspace", async () => { const inviteeEmail = `invited-${crypto.randomUUID()}@example.com`; - const { url, method, body } = prepareCreateInvitation({ + const { url, method, body } = InvitationApiTypes.prepareCreateInvitation({ workspaceId: personalWorkspaceId, email: inviteeEmail, returnUrl: "http://localhost:3000/invitations/claim", @@ -148,7 +148,7 @@ Deno.test("create invitation - with non-admin user", async () => { }) .execute(); - const { url, method, body } = prepareCreateInvitation({ + const { url, method, body } = InvitationApiTypes.prepareCreateInvitation({ workspaceId: workspace.id, email: "test@example.com", returnUrl: "http://localhost:3000/invitations/claim", @@ -202,7 +202,7 @@ Deno.test("create invitation - with non-member user", async () => { // Create another user who is not a member const { token: nonMemberToken } = await createTestUserAndToken(); - const { url, method, body } = prepareCreateInvitation({ + const { url, method, body } = InvitationApiTypes.prepareCreateInvitation({ workspaceId: workspace.id, email: "test@example.com", returnUrl: "http://localhost:3000/invitations/claim", @@ -240,7 +240,7 @@ Deno.test("create invitation - with non-existent workspace", async () => { // Create a test user const { token } = await createTestUserAndToken(); - const { url, method, body } = prepareCreateInvitation({ + const { url, method, body } = InvitationApiTypes.prepareCreateInvitation({ workspaceId: 999999, email: "test@example.com", returnUrl: "http://localhost:3000/invitations/claim", @@ -293,7 +293,7 @@ Deno.test("create invitation - success", async () => { const inviteeEmail = `invited-${crypto.randomUUID()}@example.com`; - const { url, method, body } = prepareCreateInvitation({ + const { url, method, body } = InvitationApiTypes.prepareCreateInvitation({ workspaceId: workspace.id, email: inviteeEmail, returnUrl: "http://localhost:3000/invitations/claim", @@ -371,7 +371,9 @@ Deno.test("claim invitation - success", async () => { const { token: inviteeToken, userId: inviteeUserId } = await createTestUserAndToken(); - const { url, method } = prepareClaimInvitation(invitation.uuid); + const { url, method } = InvitationApiTypes.prepareClaimInvitation( + invitation.uuid, + ); const response = await api.handle( new Request( @@ -465,7 +467,9 @@ Deno.test("claim invitation - already a member", async () => { .execute(); // Try to claim invitation when already a member - const { url, method } = prepareClaimInvitation(invitation.uuid); + const { url, method } = InvitationApiTypes.prepareClaimInvitation( + invitation.uuid, + ); const response = await api.handle( new Request( @@ -496,7 +500,9 @@ Deno.test("claim invitation - with non-existent invitation", async () => { try { const { token } = await createTestUserAndToken(); - const { url, method } = prepareClaimInvitation(crypto.randomUUID()); + const { url, method } = InvitationApiTypes.prepareClaimInvitation( + crypto.randomUUID(), + ); const response = await api.handle( new Request( @@ -551,7 +557,9 @@ Deno.test("claim invitation - with non-team workspace", async () => { .returningAll() .executeTakeFirstOrThrow(); - const { url, method } = prepareClaimInvitation(invitation.uuid); + const { url, method } = InvitationApiTypes.prepareClaimInvitation( + invitation.uuid, + ); const response = await api.handle( new Request( @@ -608,7 +616,9 @@ Deno.test("claim invitation - with expired invitation", async () => { const { token } = await createTestUserAndToken(); - const { url, method } = prepareClaimInvitation(invitation.uuid); + const { url, method } = InvitationApiTypes.prepareClaimInvitation( + invitation.uuid, + ); const response = await api.handle( new Request( diff --git a/packages/core/src/api/invitation/router.ts b/packages/core/src/api/invitation/router.ts index 046b95d..d8bd6c5 100644 --- a/packages/core/src/api/invitation/router.ts +++ b/packages/core/src/api/invitation/router.ts @@ -1,7 +1,6 @@ import { Router, Status } from "@oak/oak"; import { InvitationService } from "./service.ts"; import { authMiddleware } from "../auth/middleware.ts"; -import { createInvitationSchema } from "./types.ts"; import z from "zod"; const invitationService = new InvitationService(); @@ -11,7 +10,13 @@ const router = new Router(); router.post("/", authMiddleware, async (ctx) => { const body = await ctx.request.body.json(); - const parsedBody = createInvitationSchema.safeParse(body); + const createInvitationRequestSchema = z.object({ + workspaceId: z.number(), + email: z.string().email(), + returnUrl: z.string(), + }); + + const parsedBody = createInvitationRequestSchema.safeParse(body); if (!parsedBody.success) { ctx.response.status = Status.BadRequest; diff --git a/packages/core/src/api/invitation/service.ts b/packages/core/src/api/invitation/service.ts index 7b1e9a4..5e0e955 100644 --- a/packages/core/src/api/invitation/service.ts +++ b/packages/core/src/api/invitation/service.ts @@ -1,7 +1,6 @@ -import { db } from "../../db/database.ts"; -import { ADMIN_ROLE, MEMBER_ROLE } from "../../db/models/member.ts"; +import { ADMIN_ROLE, db, MEMBER_ROLE } from "@stackcore/db"; import { sendInvitationEmail } from "../../email/index.ts"; -import settings from "../../settings.ts"; +import settings from "@stackcore/settings"; export const notMemberOfWorkspaceError = "not_member_of_workspace"; export const notAnAdminOfWorkspaceError = "not_an_admin_of_workspace"; diff --git a/packages/core/src/api/invitation/types.ts b/packages/core/src/api/invitation/types.ts deleted file mode 100644 index ea0a69b..0000000 --- a/packages/core/src/api/invitation/types.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { z } from "zod"; - -export const createInvitationSchema = z.object({ - workspaceId: z.number(), - email: z.string().email(), - returnUrl: z.string(), -}); -export type CreateInvitationPayload = z.infer; - -export function prepareCreateInvitation(payload: CreateInvitationPayload) { - return { - url: `/invitations`, - method: "POST", - body: payload, - }; -} - -export function prepareClaimInvitation(invitationUuid: string) { - return { - url: `/invitations/${invitationUuid}/claim`, - method: "POST", - body: undefined, - }; -} diff --git a/packages/core/src/api/labeling/router.ts b/packages/core/src/api/labeling/router.ts new file mode 100644 index 0000000..91401ca --- /dev/null +++ b/packages/core/src/api/labeling/router.ts @@ -0,0 +1,72 @@ +import { Router, Status } from "@oak/oak"; +import { authMiddleware, getSession } from "../auth/middleware.ts"; +import { LabelingService } from "./service.ts"; +import z from "zod"; + +const router = new Router(); + +router.post("/temp", authMiddleware, async (ctx) => { + const body = await ctx.request.body.json(); + + const uploadTemporaryContentPayloadSchema = z.object({ + path: z.string(), + content: z.string(), + }); + + const parsedBody = uploadTemporaryContentPayloadSchema.safeParse(body); + + if (!parsedBody.success) { + ctx.response.status = Status.BadRequest; + ctx.response.body = { error: parsedBody.error }; + return; + } + + const labelingService = new LabelingService(); + + const response = await labelingService.uploadTemporaryContent( + parsedBody.data.path, + parsedBody.data.content, + ); + + ctx.response.body = response; +}); + +router.post("/request", authMiddleware, async (ctx) => { + const session = getSession(ctx); + + const body = await ctx.request.body.json(); + + const startLabelingPayloadSchema = z.object({ + manifestId: z.number(), + fileMapName: z.string(), + }); + + const parsedBody = startLabelingPayloadSchema.safeParse( + body, + ); + + if (!parsedBody.success) { + ctx.response.status = Status.BadRequest; + ctx.response.body = { error: parsedBody.error }; + return; + } + + const labelingService = new LabelingService(); + + const response = await labelingService.startLabeling( + session.userId, + parsedBody.data.manifestId, + parsedBody.data.fileMapName, + ); + + if ("error" in response) { + ctx.response.status = Status.BadRequest; + ctx.response.body = { error: response.error }; + return; + } + + ctx.response.status = Status.OK; + ctx.response.body = response; +}); + +export default router; diff --git a/packages/core/src/api/labeling/service.ts b/packages/core/src/api/labeling/service.ts new file mode 100644 index 0000000..151382b --- /dev/null +++ b/packages/core/src/api/labeling/service.ts @@ -0,0 +1,60 @@ +import { db } from "@stackcore/db"; +import { uploadTemporaryFileToBucket } from "@stackcore/storage"; +import type { LabelingApiTypes } from "@stackcore/coreApiTypes"; +import settings from "@stackcore/settings"; + +export const manifestNotFoundError = "manifest_not_found"; + +export class LabelingService { + public async uploadTemporaryContent( + path: string, + content: string, + ): Promise { + const fileName = `${Date.now()}|${path}`; + + await uploadTemporaryFileToBucket(fileName, content); + + return { path, bucketName: fileName }; + } + + public async startLabeling( + userId: number, + manifestId: number, + fileMapName: string, + ): Promise<{ error: string } | { message: string }> { + const manifest = await db + .selectFrom("manifest") + .selectAll("manifest") + .innerJoin("project", "project.id", "manifest.project_id") + .innerJoin("workspace", "workspace.id", "project.workspace_id") + .innerJoin("member", "member.workspace_id", "project.workspace_id") + .where("manifest.id", "=", manifestId) + .where("member.user_id", "=", userId) + .where("workspace.deactivated", "=", false) + .executeTakeFirst(); + + if (!manifest) { + return { error: manifestNotFoundError }; + } + + if (!settings.LABELER.SKIP_CLOUD_TASK) { + // We do not await this, to simulate send and forget, like a cloud task + fetch(`${settings.LABELER.SERVICE_URL}/start`, { + method: "POST", + body: JSON.stringify({ + manifestId, + fileMapName, + }), + headers: { + "Content-Type": "application/json", + "X-API-KEY": settings.LABELER.LABELER_API_KEY, + }, + }); + } else { + // TODO: implement cloud task + throw new Error("Cloud task not implemented"); + } + + return { message: "Labeling successfully requested" }; + } +} diff --git a/packages/core/src/api/manifest/router.test.ts b/packages/core/src/api/manifest/router.test.ts index 57e1ad5..8a16784 100644 --- a/packages/core/src/api/manifest/router.test.ts +++ b/packages/core/src/api/manifest/router.test.ts @@ -1,12 +1,12 @@ import { assertEquals, assertNotEquals } from "@std/assert"; import api from "../index.ts"; -import { db, destroyKyselyDb, initKyselyDb } from "../../db/database.ts"; +import { db, destroyKyselyDb, initKyselyDb } from "@stackcore/db"; import { resetTables } from "../../testHelpers/db.ts"; import { createTestUserAndToken } from "../../testHelpers/auth.ts"; import { WorkspaceService } from "../workspace/service.ts"; import { ProjectService } from "../project/service.ts"; import { ManifestService } from "./service.ts"; -import { ManifestApiTypes } from "../responseType.ts"; +import { ManifestApiTypes } from "@stackcore/coreApiTypes"; // Helper function to provide default project configuration values function getDefaultProjectConfig() { diff --git a/packages/core/src/api/manifest/router.ts b/packages/core/src/api/manifest/router.ts index d6822dd..997d2bd 100644 --- a/packages/core/src/api/manifest/router.ts +++ b/packages/core/src/api/manifest/router.ts @@ -1,15 +1,9 @@ import { Router, Status } from "@oak/oak"; import { ManifestService } from "./service.ts"; import { authMiddleware, getSession } from "../auth/middleware.ts"; -import { - createManifestPayloadSchema, - type CreateManifestResponse, - type GetManifestAuditResponse, - type GetManifestDetailsResponse, - type GetManifestsResponse, -} from "./types.ts"; +import type { ManifestApiTypes } from "@stackcore/coreApiTypes"; import z from "zod"; -import settings from "../../settings.ts"; +import settings from "@stackcore/settings"; const manifestService = new ManifestService(); const router = new Router(); @@ -20,7 +14,19 @@ router.post("/", authMiddleware, async (ctx) => { const body = await ctx.request.body.json(); - const parsedBody = createManifestPayloadSchema.safeParse(body); + const createManifestPayloadSchema = z.object({ + projectId: z.number(), + branch: z.string().nullable(), + commitSha: z.string().nullable(), + commitShaDate: z.string().nullable().transform((val) => + val ? new Date(val) : null + ), + manifest: z.object({}).passthrough(), // Allow any object structure + }); + + const parsedBody = createManifestPayloadSchema.safeParse( + body, + ); if (!parsedBody.success) { ctx.response.status = Status.BadRequest; @@ -44,7 +50,7 @@ router.post("/", authMiddleware, async (ctx) => { } ctx.response.status = Status.Created; - ctx.response.body = response as CreateManifestResponse; + ctx.response.body = response as ManifestApiTypes.CreateManifestResponse; }); // Get manifests with pagination and filtering @@ -95,7 +101,7 @@ router.get("/", authMiddleware, async (ctx) => { } ctx.response.status = Status.OK; - ctx.response.body = response as GetManifestsResponse; + ctx.response.body = response as ManifestApiTypes.GetManifestsResponse; }); // Get manifest details @@ -128,7 +134,7 @@ router.get("/:manifestId", authMiddleware, async (ctx) => { } ctx.response.status = Status.OK; - ctx.response.body = response as GetManifestDetailsResponse; + ctx.response.body = response as ManifestApiTypes.GetManifestDetailsResponse; }); // Delete a manifest @@ -193,7 +199,7 @@ router.get("/:manifestId/audit", authMiddleware, async (ctx) => { } ctx.response.status = Status.OK; - ctx.response.body = response as GetManifestAuditResponse; + ctx.response.body = response as ManifestApiTypes.GetManifestAuditResponse; }); export default router; diff --git a/packages/core/src/api/manifest/service.ts b/packages/core/src/api/manifest/service.ts index 90beb74..f3fdc60 100644 --- a/packages/core/src/api/manifest/service.ts +++ b/packages/core/src/api/manifest/service.ts @@ -1,18 +1,14 @@ -import { db } from "../../db/database.ts"; -import type { DependencyManifest } from "../../manifest/dependencyManifest/types.ts"; +import { db } from "@stackcore/db"; import { generateAuditManifest } from "../../manifest/service.ts"; -import settings from "../../settings.ts"; +import settings from "@stackcore/settings"; import { StripeService } from "../../stripe/index.ts"; -import type { - GetManifestAuditResponse, - GetManifestDetailsResponse, - GetManifestsResponse, -} from "./types.ts"; +import type { ManifestApiTypes } from "@stackcore/coreApiTypes"; import { - downloadJsonFromBucket, - getPublicLink, - uploadJsonToBucket, -} from "../../bucketStorage/index.ts"; + downloadManifestFromBucket, + getManifestPublicLink, + uploadManifestToBucket, +} from "@stackcore/storage"; +import type { DependencyManifest } from "@stackcore/manifests"; export const manifestNotFoundError = "manifest_not_found"; export const projectNotFoundError = "project_not_found"; @@ -72,7 +68,7 @@ export class ManifestService { } const manifestFileName = `${projectId}-${Date.now()}.json`; - await uploadJsonToBucket(manifest, manifestFileName); + await uploadManifestToBucket(manifest, manifestFileName); // Create the manifest const newManifest = await db @@ -110,7 +106,7 @@ export class ManifestService { projectId?: number, workspaceId?: number, ): Promise< - GetManifestsResponse | { + ManifestApiTypes.GetManifestsResponse | { error?: string; } > { @@ -207,7 +203,7 @@ export class ManifestService { public async getManifestDetails( userId: number, manifestId: number, - ): Promise { + ): Promise { // Get manifest with access check const manifest = await db .selectFrom("manifest") @@ -224,7 +220,7 @@ export class ManifestService { return { error: manifestNotFoundError }; } - const publicLink = await getPublicLink(manifest.manifest); + const publicLink = await getManifestPublicLink(manifest.manifest); return { ...manifest, @@ -269,7 +265,7 @@ export class ManifestService { public async getManifestAudit( userId: number, manifestId: number, - ): Promise { + ): Promise { // Get manifest with access check const manifest = await db .selectFrom("manifest") @@ -297,14 +293,14 @@ export class ManifestService { return { error: projectNotFoundError }; } - const manifestJson = await downloadJsonFromBucket( + const manifestJson = await downloadManifestFromBucket( manifest.manifest, - ) as DependencyManifest; + ); try { const auditManifest = generateAuditManifest( manifest.version, - manifestJson, + manifestJson as DependencyManifest, { file: { maxCodeChar: project.max_char_per_file, diff --git a/packages/core/src/api/manifest/types.ts b/packages/core/src/api/manifest/types.ts deleted file mode 100644 index a936c5c..0000000 --- a/packages/core/src/api/manifest/types.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { z } from "zod"; -import type { Manifest } from "../../db/models/manifest.ts"; -import type { AuditManifest } from "../../manifest/auditManifest/types.ts"; - -export const createManifestPayloadSchema = z.object({ - projectId: z.number(), - branch: z.string().nullable(), - commitSha: z.string().nullable(), - commitShaDate: z.string().nullable().transform((val) => - val ? new Date(val) : null - ), - manifest: z.object({}).passthrough(), // Allow any object structure -}); - -export type CreateManifestPayload = z.infer< - typeof createManifestPayloadSchema ->; - -export type CreateManifestResponse = { - id: number; -}; - -export function prepareCreateManifest(payload: CreateManifestPayload) { - return { - url: "/manifests", - method: "POST", - body: payload, - }; -} - -export function prepareGetManifests(payload: { - page: number; - limit: number; - search?: string; - projectId?: number; - workspaceId?: number; -}) { - const searchParams = new URLSearchParams(); - searchParams.set("page", payload.page.toString()); - searchParams.set("limit", payload.limit.toString()); - if (payload.search) { - searchParams.set("search", payload.search); - } - if (payload.projectId) { - searchParams.set("projectId", payload.projectId.toString()); - } - if (payload.workspaceId) { - searchParams.set("workspaceId", payload.workspaceId.toString()); - } - - return { - url: `/manifests?${searchParams.toString()}`, - method: "GET", - body: undefined, - }; -} - -export function prepareGetManifestDetails(manifestId: number) { - return { - url: `/manifests/${manifestId}`, - method: "GET", - body: undefined, - }; -} - -export function prepareDeleteManifest(manifestId: number) { - return { - url: `/manifests/${manifestId}`, - method: "DELETE", - body: undefined, - }; -} - -export type GetManifestsResponse = { - results: Omit[]; // Only return metadata for list view - total: number; -}; - -export type GetManifestDetailsResponse = Manifest; // Return full manifest details - -export function prepareGetManifestAudit(manifestId: number) { - return { - url: `/manifests/${manifestId}/audit`, - method: "GET", - body: undefined, - }; -} - -export type GetManifestAuditResponse = AuditManifest; diff --git a/packages/core/src/api/member/router.test.ts b/packages/core/src/api/member/router.test.ts index 90c288c..454d16b 100644 --- a/packages/core/src/api/member/router.test.ts +++ b/packages/core/src/api/member/router.test.ts @@ -1,6 +1,11 @@ import { assertEquals, assertNotEquals } from "@std/assert"; import api from "../index.ts"; -import { db, destroyKyselyDb, initKyselyDb } from "../../db/database.ts"; +import { + db, + destroyKyselyDb, + initKyselyDb, + type MemberRole, +} from "@stackcore/db"; import { resetTables } from "../../testHelpers/db.ts"; import { WorkspaceService } from "../workspace/service.ts"; import { createTestUserAndToken } from "../../testHelpers/auth.ts"; @@ -10,8 +15,7 @@ import { notAdminOfWorkspaceError, notMemberOfWorkspaceError, } from "./service.ts"; -import { MemberApiTypes } from "../responseType.ts"; -import type { MemberRole } from "../../db/models/member.ts"; +import { MemberApiTypes } from "@stackcore/coreApiTypes"; // GET /:workspaceId/members (list members) Deno.test("get workspace members", async () => { diff --git a/packages/core/src/api/member/router.ts b/packages/core/src/api/member/router.ts index d6e8577..9079a34 100644 --- a/packages/core/src/api/member/router.ts +++ b/packages/core/src/api/member/router.ts @@ -1,9 +1,9 @@ import { Router, Status } from "@oak/oak"; import { MemberService } from "./service.ts"; import { authMiddleware } from "../auth/middleware.ts"; -import { type GetMembersResponse, updateMemberRoleSchema } from "./types.ts"; +import { MemberApiTypes } from "@stackcore/coreApiTypes"; import z from "zod"; -import settings from "../../settings.ts"; +import settings from "@stackcore/settings"; const memberService = new MemberService(); const router = new Router(); @@ -47,7 +47,7 @@ router.get("/", authMiddleware, async (ctx) => { } ctx.response.status = Status.OK; - ctx.response.body = response as GetMembersResponse; + ctx.response.body = response as MemberApiTypes.GetMembersResponse; }); router.patch( @@ -70,7 +70,11 @@ router.patch( const body = await ctx.request.body.json(); - const parsedBody = updateMemberRoleSchema.safeParse(body); + const updateMemberRolePayloadSchema = z.object({ + role: z.enum([MemberApiTypes.ADMIN_ROLE, MemberApiTypes.MEMBER_ROLE]), + }); + + const parsedBody = updateMemberRolePayloadSchema.safeParse(body); if (!parsedBody.success) { ctx.response.status = Status.BadRequest; diff --git a/packages/core/src/api/member/service.ts b/packages/core/src/api/member/service.ts index 42e0191..58849a2 100644 --- a/packages/core/src/api/member/service.ts +++ b/packages/core/src/api/member/service.ts @@ -1,6 +1,5 @@ -import { db } from "../../db/database.ts"; -import { ADMIN_ROLE, type MemberRole } from "../../db/models/member.ts"; -import type { GetMembersResponse } from "./types.ts"; +import { ADMIN_ROLE, db, type MemberRole } from "@stackcore/db"; +import type { MemberApiTypes } from "@stackcore/coreApiTypes"; export const notMemberOfWorkspaceError = "not_member_of_workspace"; export const notAdminOfWorkspaceError = "not_admin_of_workspace"; @@ -17,7 +16,7 @@ export class MemberService { page: number, limit: number, search?: string, - ): Promise<{ error: string } | GetMembersResponse> { + ): Promise<{ error: string } | MemberApiTypes.GetMembersResponse> { // check if user is a member of the workspace const userMember = await db .selectFrom("member") diff --git a/packages/core/src/api/project/router.test.ts b/packages/core/src/api/project/router.test.ts index ee70f3e..49c2a89 100644 --- a/packages/core/src/api/project/router.test.ts +++ b/packages/core/src/api/project/router.test.ts @@ -1,11 +1,11 @@ import { assertEquals, assertNotEquals } from "@std/assert"; import api from "../index.ts"; -import { db, destroyKyselyDb, initKyselyDb } from "../../db/database.ts"; +import { db, destroyKyselyDb, initKyselyDb } from "@stackcore/db"; import { resetTables } from "../../testHelpers/db.ts"; import { createTestUserAndToken } from "../../testHelpers/auth.ts"; import { WorkspaceService } from "../workspace/service.ts"; import { ProjectService } from "./service.ts"; -import { ProjectApiTypes } from "../responseType.ts"; +import { ProjectApiTypes } from "@stackcore/coreApiTypes"; // --- CREATE PROJECT TESTS --- Deno.test("create a project", async () => { diff --git a/packages/core/src/api/project/router.ts b/packages/core/src/api/project/router.ts index 77d4682..e7bb0cf 100644 --- a/packages/core/src/api/project/router.ts +++ b/packages/core/src/api/project/router.ts @@ -1,15 +1,9 @@ import { Router, Status } from "@oak/oak"; import { ProjectService } from "./service.ts"; import { authMiddleware, getSession } from "../auth/middleware.ts"; -import { - createProjectPayloadSchema, - type CreateProjectResponse, - type GetProjectDetailsResponse, - type GetProjectsResponse, - updateProjectSchema, -} from "./types.ts"; +import type { ProjectApiTypes } from "@stackcore/coreApiTypes"; import z from "zod"; -import settings from "../../settings.ts"; +import settings from "@stackcore/settings"; const projectService = new ProjectService(); const router = new Router(); @@ -20,6 +14,26 @@ router.post("/", authMiddleware, async (ctx) => { const body = await ctx.request.body.json(); + const createProjectPayloadSchema = z.object({ + name: z.string(), + repoUrl: z.string(), + workspaceId: z.number(), + maxCodeCharPerSymbol: z.number().int().min(1), + maxCodeCharPerFile: z.number().int().min(1), + maxCharPerSymbol: z.number().int().min(1), + maxCharPerFile: z.number().int().min(1), + maxCodeLinePerSymbol: z.number().int().min(1), + maxCodeLinePerFile: z.number().int().min(1), + maxLinePerSymbol: z.number().int().min(1), + maxLinePerFile: z.number().int().min(1), + maxDependencyPerSymbol: z.number().int().min(1), + maxDependencyPerFile: z.number().int().min(1), + maxDependentPerSymbol: z.number().int().min(1), + maxDependentPerFile: z.number().int().min(1), + maxCyclomaticComplexityPerSymbol: z.number().int().min(1), + maxCyclomaticComplexityPerFile: z.number().int().min(1), + }); + const parsedBody = createProjectPayloadSchema.safeParse(body); if (!parsedBody.success) { @@ -60,7 +74,7 @@ router.post("/", authMiddleware, async (ctx) => { } ctx.response.status = Status.Created; - ctx.response.body = response as CreateProjectResponse; + ctx.response.body = response as ProjectApiTypes.CreateProjectResponse; }); // Get all projects for an workspace @@ -106,7 +120,7 @@ router.get("/", authMiddleware, async (ctx) => { } ctx.response.status = Status.OK; - ctx.response.body = response as GetProjectsResponse; + ctx.response.body = response as ProjectApiTypes.GetProjectsResponse; }); // Get project details @@ -139,7 +153,7 @@ router.get("/:projectId", authMiddleware, async (ctx) => { } ctx.response.status = Status.OK; - ctx.response.body = response as GetProjectDetailsResponse; + ctx.response.body = response as ProjectApiTypes.GetProjectDetailsResponse; }); // Update a project @@ -162,7 +176,26 @@ router.patch("/:projectId", authMiddleware, async (ctx) => { const body = await ctx.request.body.json(); - const parsedBody = updateProjectSchema.safeParse(body); + const updateProjectPayloadSchema = z.object({ + name: z.string(), + repoUrl: z.string(), + maxCodeCharPerSymbol: z.number().int().min(1), + maxCodeCharPerFile: z.number().int().min(1), + maxCharPerSymbol: z.number().int().min(1), + maxCharPerFile: z.number().int().min(1), + maxCodeLinePerSymbol: z.number().int().min(1), + maxCodeLinePerFile: z.number().int().min(1), + maxLinePerSymbol: z.number().int().min(1), + maxLinePerFile: z.number().int().min(1), + maxDependencyPerSymbol: z.number().int().min(1), + maxDependencyPerFile: z.number().int().min(1), + maxDependentPerSymbol: z.number().int().min(1), + maxDependentPerFile: z.number().int().min(1), + maxCyclomaticComplexityPerSymbol: z.number().int().min(1), + maxCyclomaticComplexityPerFile: z.number().int().min(1), + }); + + const parsedBody = updateProjectPayloadSchema.safeParse(body); if (!parsedBody.success) { ctx.response.status = Status.BadRequest; diff --git a/packages/core/src/api/project/service.ts b/packages/core/src/api/project/service.ts index 4a186d2..f06e784 100644 --- a/packages/core/src/api/project/service.ts +++ b/packages/core/src/api/project/service.ts @@ -1,8 +1,5 @@ -import { db } from "../../db/database.ts"; -import type { - GetProjectDetailsResponse, - GetProjectsResponse, -} from "./types.ts"; +import { db } from "@stackcore/db"; +import type { ProjectApiTypes } from "@stackcore/coreApiTypes"; export const projectAlreadyExistsErrorCode = "project_already_exists"; export const projectNotFoundError = "project_not_found"; @@ -107,7 +104,7 @@ export class ProjectService { public async getProjectDetails( userId: number, projectId: number, - ): Promise<{ error?: string } | GetProjectDetailsResponse> { + ): Promise<{ error?: string } | ProjectApiTypes.GetProjectDetailsResponse> { // Get project with access check via workspace membership const project = await db .selectFrom("project") @@ -131,7 +128,7 @@ export class ProjectService { return { error: projectNotFoundError }; } - return project as GetProjectDetailsResponse; + return project as ProjectApiTypes.GetProjectDetailsResponse; } /** @@ -143,7 +140,7 @@ export class ProjectService { limit: number, search?: string, workspaceId?: number, - ): Promise<{ error: string } | GetProjectsResponse> { + ): Promise<{ error: string } | ProjectApiTypes.GetProjectsResponse> { // First check if user has access to the workspace if (workspaceId) { const hasAccess = await db diff --git a/packages/core/src/api/project/types.ts b/packages/core/src/api/project/types.ts deleted file mode 100644 index ecf3d46..0000000 --- a/packages/core/src/api/project/types.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { z } from "zod"; -import type { Project } from "../../db/models/project.ts"; - -export const createProjectPayloadSchema = z.object({ - name: z.string(), - repoUrl: z.string(), - workspaceId: z.number(), - maxCodeCharPerSymbol: z.number().int().min(1), - maxCodeCharPerFile: z.number().int().min(1), - maxCharPerSymbol: z.number().int().min(1), - maxCharPerFile: z.number().int().min(1), - maxCodeLinePerSymbol: z.number().int().min(1), - maxCodeLinePerFile: z.number().int().min(1), - maxLinePerSymbol: z.number().int().min(1), - maxLinePerFile: z.number().int().min(1), - maxDependencyPerSymbol: z.number().int().min(1), - maxDependencyPerFile: z.number().int().min(1), - maxDependentPerSymbol: z.number().int().min(1), - maxDependentPerFile: z.number().int().min(1), - maxCyclomaticComplexityPerSymbol: z.number().int().min(1), - maxCyclomaticComplexityPerFile: z.number().int().min(1), -}); - -export type CreateProjectPayload = z.infer< - typeof createProjectPayloadSchema ->; - -export type CreateProjectResponse = { - id: number; -}; - -export function prepareCreateProject(payload: CreateProjectPayload) { - return { - url: "/projects", - method: "POST", - body: payload, - }; -} - -export function prepareGetProjects(payload: { - page: number; - limit: number; - search?: string; - workspaceId?: number; -}) { - const searchParams = new URLSearchParams(); - searchParams.set("page", payload.page.toString()); - searchParams.set("limit", payload.limit.toString()); - if (payload.search) { - searchParams.set("search", payload.search); - } - if (payload.workspaceId) { - searchParams.set("workspaceId", payload.workspaceId.toString()); - } - - return { - url: `/projects?${searchParams.toString()}`, - method: "GET", - body: undefined, - }; -} - -export function prepareGetProjectDetails(projectId: number) { - return { - url: `/projects/${projectId}`, - method: "GET", - body: undefined, - }; -} - -export const updateProjectSchema = z.object({ - name: z.string(), - repoUrl: z.string(), - maxCodeCharPerSymbol: z.number().int().min(1), - maxCodeCharPerFile: z.number().int().min(1), - maxCharPerSymbol: z.number().int().min(1), - maxCharPerFile: z.number().int().min(1), - maxCodeLinePerSymbol: z.number().int().min(1), - maxCodeLinePerFile: z.number().int().min(1), - maxLinePerSymbol: z.number().int().min(1), - maxLinePerFile: z.number().int().min(1), - maxDependencyPerSymbol: z.number().int().min(1), - maxDependencyPerFile: z.number().int().min(1), - maxDependentPerSymbol: z.number().int().min(1), - maxDependentPerFile: z.number().int().min(1), - maxCyclomaticComplexityPerSymbol: z.number().int().min(1), - maxCyclomaticComplexityPerFile: z.number().int().min(1), -}); - -export type UpdateProjectPayload = z.infer< - typeof updateProjectSchema ->; - -export function prepareUpdateProject( - projectId: number, - payload: UpdateProjectPayload, -) { - return { - url: `/projects/${projectId}`, - method: "PATCH", - body: payload, - }; -} - -export function prepareDeleteProject(projectId: number) { - return { - url: `/projects/${projectId}`, - method: "DELETE", - body: undefined, - }; -} - -export type GetProjectsResponse = { - results: Project[]; - total: number; -}; - -export type GetProjectDetailsResponse = Project; diff --git a/packages/core/src/api/responseType.ts b/packages/core/src/api/responseType.ts deleted file mode 100644 index 945f937..0000000 --- a/packages/core/src/api/responseType.ts +++ /dev/null @@ -1,19 +0,0 @@ -import * as AuthApiTypes from "./auth/types.ts"; -import * as WorkspaceApiTypes from "./workspace/types.ts"; -import * as InvitationApiTypes from "./invitation/types.ts"; -import * as MemberApiTypes from "./member/types.ts"; -import * as BillingApiTypes from "./billing/types.ts"; -import * as ProjectApiTypes from "./project/types.ts"; -import * as ManifestApiTypes from "./manifest/types.ts"; -import * as TokenApiTypes from "./token/types.ts"; - -export { - AuthApiTypes, - BillingApiTypes, - InvitationApiTypes, - ManifestApiTypes, - MemberApiTypes, - ProjectApiTypes, - TokenApiTypes, - WorkspaceApiTypes, -}; diff --git a/packages/core/src/api/token/router.test.ts b/packages/core/src/api/token/router.test.ts index e24fce9..3109bb7 100644 --- a/packages/core/src/api/token/router.test.ts +++ b/packages/core/src/api/token/router.test.ts @@ -1,10 +1,10 @@ import { assertEquals, assertNotEquals } from "@std/assert"; import api from "../index.ts"; -import { db, destroyKyselyDb, initKyselyDb } from "../../db/database.ts"; +import { db, destroyKyselyDb, initKyselyDb } from "@stackcore/db"; import { resetTables } from "../../testHelpers/db.ts"; import { createTestUserAndToken } from "../../testHelpers/auth.ts"; import { tokenNotFoundError } from "./service.ts"; -import { TokenApiTypes } from "../responseType.ts"; +import { TokenApiTypes } from "@stackcore/coreApiTypes"; // POST /tokens (create token) Deno.test("create token", async () => { diff --git a/packages/core/src/api/token/router.ts b/packages/core/src/api/token/router.ts index c01b7c1..008ca60 100644 --- a/packages/core/src/api/token/router.ts +++ b/packages/core/src/api/token/router.ts @@ -1,9 +1,8 @@ import { Router, Status } from "@oak/oak"; import { TokenService } from "./service.ts"; import { authMiddleware } from "../auth/middleware.ts"; -import { createTokenSchema } from "./types.ts"; import z from "zod"; -import settings from "../../settings.ts"; +import settings from "@stackcore/settings"; const tokenService = new TokenService(); const router = new Router(); @@ -12,7 +11,11 @@ const router = new Router(); router.post("/", authMiddleware, async (ctx) => { const body = await ctx.request.body.json(); - const parsedBody = createTokenSchema.safeParse(body); + const createTokenPayloadSchema = z.object({ + name: z.string().nonempty(), + }); + + const parsedBody = createTokenPayloadSchema.safeParse(body); if (!parsedBody.success) { ctx.response.status = Status.BadRequest; diff --git a/packages/core/src/api/token/service.ts b/packages/core/src/api/token/service.ts index f6fab7b..8d281de 100644 --- a/packages/core/src/api/token/service.ts +++ b/packages/core/src/api/token/service.ts @@ -1,6 +1,5 @@ -import { db } from "../../db/database.ts"; -import type { NewToken } from "../../db/models/token.ts"; -import type { CreateTokenResponse, GetTokensResponse } from "./types.ts"; +import { db, type NewToken } from "@stackcore/db"; +import type { TokenApiTypes } from "@stackcore/coreApiTypes"; export const tokenNotFoundError = "token_not_found"; export const invalidTokenNameError = "invalid_token_name"; @@ -12,7 +11,7 @@ export class TokenService { public async createToken( userId: number, name: string, - ): Promise { + ): Promise { const newToken: NewToken = { user_id: userId, name: name.trim(), @@ -36,7 +35,7 @@ export class TokenService { page: number, limit: number, search?: string, - ): Promise { + ): Promise { // Get total count const totalResult = await db .selectFrom("token") diff --git a/packages/core/src/api/workspace/router.test.ts b/packages/core/src/api/workspace/router.test.ts index 54a1951..dc514b3 100644 --- a/packages/core/src/api/workspace/router.test.ts +++ b/packages/core/src/api/workspace/router.test.ts @@ -1,6 +1,6 @@ import { assertEquals, assertNotEquals } from "@std/assert"; import api from "../index.ts"; -import { db, destroyKyselyDb, initKyselyDb } from "../../db/database.ts"; +import { db, destroyKyselyDb, initKyselyDb } from "@stackcore/db"; import { resetTables } from "../../testHelpers/db.ts"; import { cannotDeactivatePersonalWorkspaceError, @@ -9,7 +9,7 @@ import { WorkspaceService, } from "./service.ts"; import { createTestUserAndToken } from "../../testHelpers/auth.ts"; -import { WorkspaceApiTypes } from "../responseType.ts"; +import { WorkspaceApiTypes } from "@stackcore/coreApiTypes"; // POST / (create workspace) Deno.test("create a team workspace", async () => { diff --git a/packages/core/src/api/workspace/router.ts b/packages/core/src/api/workspace/router.ts index c811cdc..405d086 100644 --- a/packages/core/src/api/workspace/router.ts +++ b/packages/core/src/api/workspace/router.ts @@ -1,14 +1,9 @@ import { Router, Status } from "@oak/oak"; import { WorkspaceService } from "./service.ts"; import { authMiddleware } from "../auth/middleware.ts"; -import { - createWorkspacePayloadSchema, - type CreateWorkspaceResponse, - type GetWorkspacesResponse, - updateWorkspaceSchema, -} from "./types.ts"; +import type { WorkspaceApiTypes } from "@stackcore/coreApiTypes"; import z from "zod"; -import settings from "../../settings.ts"; +import settings from "@stackcore/settings"; const workspaceService = new WorkspaceService(); const router = new Router(); @@ -17,6 +12,10 @@ const router = new Router(); router.post("/", authMiddleware, async (ctx) => { const body = await ctx.request.body.json(); + const createWorkspacePayloadSchema = z.object({ + name: z.string(), + }); + const parsedBody = createWorkspacePayloadSchema.safeParse(body); if (!parsedBody.success) { @@ -39,7 +38,7 @@ router.post("/", authMiddleware, async (ctx) => { } ctx.response.status = Status.Created; - ctx.response.body = result as CreateWorkspaceResponse; + ctx.response.body = result as WorkspaceApiTypes.CreateWorkspaceResponse; }); // Get all workspaces for current user @@ -66,13 +65,14 @@ router.get("/", authMiddleware, async (ctx) => { return; } - const response: GetWorkspacesResponse = await workspaceService - .getWorkspaces( - userId, - parsedSearchParams.data.page, - parsedSearchParams.data.limit, - parsedSearchParams.data.search, - ); + const response: WorkspaceApiTypes.GetWorkspacesResponse = + await workspaceService + .getWorkspaces( + userId, + parsedSearchParams.data.page, + parsedSearchParams.data.limit, + parsedSearchParams.data.search, + ); ctx.response.status = Status.OK; ctx.response.body = response; @@ -96,7 +96,11 @@ router.patch("/:workspaceId", authMiddleware, async (ctx) => { const body = await ctx.request.body.json(); - const parsedBody = updateWorkspaceSchema.safeParse(body); + const updateWorkspacePayloadSchema = z.object({ + name: z.string(), + }); + + const parsedBody = updateWorkspacePayloadSchema.safeParse(body); if (!parsedBody.success) { ctx.response.status = Status.BadRequest; diff --git a/packages/core/src/api/workspace/service.ts b/packages/core/src/api/workspace/service.ts index 03aa552..a19f0af 100644 --- a/packages/core/src/api/workspace/service.ts +++ b/packages/core/src/api/workspace/service.ts @@ -1,12 +1,7 @@ -import { db } from "../../db/database.ts"; -import { shouldHaveAccess } from "../../db/models/workspace.ts"; -import { ADMIN_ROLE } from "../../db/models/member.ts"; +import { ADMIN_ROLE, db } from "@stackcore/db"; import { StripeService } from "../../stripe/index.ts"; -import type { - CreateWorkspaceResponse, - GetWorkspacesResponse, -} from "./types.ts"; -import settings from "../../settings.ts"; +import type { WorkspaceApiTypes } from "@stackcore/coreApiTypes"; +import settings from "@stackcore/settings"; export const workspaceAlreadyExistsErrorCode = "workspace_already_exists"; export const workspaceNotFoundError = "workspace_not_found"; @@ -32,7 +27,7 @@ export class WorkspaceService { name: string, userId: number, ): Promise< - CreateWorkspaceResponse | { + WorkspaceApiTypes.CreateWorkspaceResponse | { error: typeof workspaceAlreadyExistsErrorCode; } > { @@ -94,7 +89,9 @@ export class WorkspaceService { settings.STRIPE.BILLING_THRESHOLD_BASIC, ); - const accessEnabled = shouldHaveAccess(subscription.status); + const accessEnabled = stripeService.shouldHaveAccess( + subscription.status, + ); // Update workspace with Stripe customer ID await trx @@ -122,7 +119,7 @@ export class WorkspaceService { page: number, limit: number, search?: string, - ): Promise { + ): Promise { // Get total count of workspaces const totalResult = await db .selectFrom("member") diff --git a/packages/core/src/bucketStorage/index.ts b/packages/core/src/bucketStorage/index.ts deleted file mode 100644 index eb7bf66..0000000 --- a/packages/core/src/bucketStorage/index.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { Storage } from "@google-cloud/storage"; -import settings from "../settings.ts"; -import { generateKeyPairSync } from "node:crypto"; - -async function getStorage() { - // local dev uses fake-gcs-server - if (settings.GCP_BUCKET.USE_FAKE_GCS_SERVER) { - // generate a valid private key to generate a signed URL - const { privateKey } = generateKeyPairSync("rsa", { - modulusLength: 2048, - }); - const privateKeyPem = privateKey.export({ - type: "pkcs8", - format: "pem", - }).toString("base64"); - - const storage = new Storage({ - apiEndpoint: "http://localhost:4443", - projectId: "fake-project-id", - credentials: { - client_email: "fake@example.com", - private_key: privateKeyPem, - }, - }); - - const bucket = storage.bucket(settings.GCP_BUCKET.BUCKET_NAME); - - const [exists] = await bucket.exists(); - if (!exists) { - console.info("Bucket does not exist, creating..."); - await bucket.create(); - console.info("Bucket created"); - } - - return storage; - } - // production uses real GCP - return new Storage({ - projectId: settings.GCP_BUCKET.PROJECT_ID, - }); -} - -export async function uploadJsonToBucket(json: object, fileName: string) { - const storage = await getStorage(); - - const jsonString = JSON.stringify(json); - const bucket = storage.bucket(settings.GCP_BUCKET.BUCKET_NAME); - const file = bucket.file(fileName); - await file.save(jsonString, { - contentType: "application/json", - }); -} - -export async function downloadJsonFromBucket(fileName: string) { - const storage = await getStorage(); - const file = storage.bucket(settings.GCP_BUCKET.BUCKET_NAME).file(fileName); - const [content] = await file.download(); - return JSON.parse(content.toString()); -} - -export async function getPublicLink(fileName: string) { - const storage = await getStorage(); - const bucket = storage.bucket(settings.GCP_BUCKET.BUCKET_NAME); - const file = bucket.file(fileName); - - const [url] = await file.getSignedUrl({ - action: "read", - expires: Date.now() + 1000 * settings.GCP_BUCKET.SIGNED_URL_EXPIRY_SECONDS, - }); - - return url; -} diff --git a/packages/core/src/db/models/workspace.ts b/packages/core/src/db/models/workspace.ts deleted file mode 100644 index a4adb44..0000000 --- a/packages/core/src/db/models/workspace.ts +++ /dev/null @@ -1,74 +0,0 @@ -import type { - ColumnType, - Generated, - Insertable, - Selectable, - Updateable, -} from "kysely"; -import type Stripe from "stripe"; -export const BASIC_PRODUCT = "BASIC"; -export const PRO_PRODUCT = "PRO"; -export const PREMIUM_PRODUCT = "PREMIUM"; -export const CUSTOM_PRODUCT = "CUSTOM"; -export type StripeProduct = - | typeof BASIC_PRODUCT - | typeof PRO_PRODUCT - | typeof PREMIUM_PRODUCT - | typeof CUSTOM_PRODUCT; - -export const MONTHLY_BILLING_CYCLE = "MONTHLY"; -export const YEARLY_BILLING_CYCLE = "YEARLY"; -export type StripeBillingCycle = - | typeof MONTHLY_BILLING_CYCLE - | typeof YEARLY_BILLING_CYCLE; - -export interface WorkspaceTable { - id: Generated; - name: string; - isTeam: boolean; - stripe_customer_id: string | null; - access_enabled: boolean; - deactivated: boolean; - created_at: ColumnType; -} - -export type Workspace = Selectable; -export type NewWorkspace = Insertable; -export type WorkspaceUpdate = Updateable; - -/* - subscriptionStatus: The status of the subscription in Stripe - Returns true if the workspace should have access, false otherwise -*/ -export function shouldHaveAccess( - subscriptionStatus: Stripe.Subscription.Status, -) { - switch (subscriptionStatus) { - // All good - case "active": - // trial period, all good - case "trialing": - // invoice cannot be paid, - // after 23h will move to incomplete_expired, where we will block access - case "incomplete": - return true; - // this will move to unpaid eventually, - // once stripe has exhausted all retry (can be configured) - case "past_due": - // most likely user changed their subscription. - // We block access, when new subscription is created, - // another event is triggered and access is restored - case "canceled": - // Subscription paused from dashboard, we block access - case "paused": - // Stripe cannot pay the invoice, we block access - case "unpaid": - // Invoice expired, we block access - case "incomplete_expired": - return false; - - // Should not happen - default: - throw new Error(`Unknown subscription status: ${subscriptionStatus}`); - } -} diff --git a/packages/core/src/email/index.ts b/packages/core/src/email/index.ts index 4944e25..c8ba28b 100644 --- a/packages/core/src/email/index.ts +++ b/packages/core/src/email/index.ts @@ -1,9 +1,6 @@ -import type { - StripeBillingCycle, - StripeProduct, -} from "../db/models/workspace.ts"; +import type { BillingApiTypes } from "@stackcore/coreApiTypes"; import { Resend } from "resend"; -import settings from "../settings.ts"; +import settings from "@stackcore/settings"; import OtpEmail from "./templates/OtpEmail.tsx"; import WelcomeEmail from "./templates/WelcomeEmail.tsx"; @@ -129,12 +126,12 @@ export async function sendSubscriptionUpgradedEmail( emails: string[]; workspaceName: string; oldSubscription: { - product: StripeProduct; - billingCycle: StripeBillingCycle | null; + product: BillingApiTypes.StripeProduct; + billingCycle: BillingApiTypes.StripeBillingCycle | null; }; newSubscription: { - product: StripeProduct; - billingCycle: StripeBillingCycle | null; + product: BillingApiTypes.StripeProduct; + billingCycle: BillingApiTypes.StripeBillingCycle | null; }; }, ) { @@ -151,12 +148,12 @@ export async function sendSubscriptionDowngradedEmail( emails: string[]; workspaceName: string; oldSubscription: { - product: StripeProduct; - billingCycle: StripeBillingCycle | null; + product: BillingApiTypes.StripeProduct; + billingCycle: BillingApiTypes.StripeBillingCycle | null; }; newSubscription: { - product: StripeProduct; - billingCycle: StripeBillingCycle | null; + product: BillingApiTypes.StripeProduct; + billingCycle: BillingApiTypes.StripeBillingCycle | null; }; newSubscriptionDate: string; }, diff --git a/packages/core/src/email/templates/DowngradeEmail.tsx b/packages/core/src/email/templates/DowngradeEmail.tsx index 2c24ca4..5bd6af0 100644 --- a/packages/core/src/email/templates/DowngradeEmail.tsx +++ b/packages/core/src/email/templates/DowngradeEmail.tsx @@ -1,19 +1,16 @@ import { baseTemplate } from "./base.tsx"; -import type { - StripeBillingCycle, - StripeProduct, -} from "../../db/models/workspace.ts"; +import type { BillingApiTypes } from "@stackcore/coreApiTypes"; const DowngradeEmail = (props: { emails: string[]; workspaceName: string; oldSubscription: { - product: StripeProduct; - billingCycle: StripeBillingCycle | null; + product: BillingApiTypes.StripeProduct; + billingCycle: BillingApiTypes.StripeBillingCycle | null; }; newSubscription: { - product: StripeProduct; - billingCycle: StripeBillingCycle | null; + product: BillingApiTypes.StripeProduct; + billingCycle: BillingApiTypes.StripeBillingCycle | null; }; newSubscriptionDate: string; }) => { diff --git a/packages/core/src/email/templates/UpgradeEmail.tsx b/packages/core/src/email/templates/UpgradeEmail.tsx index d1bdf5b..1aa0209 100644 --- a/packages/core/src/email/templates/UpgradeEmail.tsx +++ b/packages/core/src/email/templates/UpgradeEmail.tsx @@ -1,19 +1,16 @@ import { baseTemplate } from "./base.tsx"; -import type { - StripeBillingCycle, - StripeProduct, -} from "../../db/models/workspace.ts"; +import type { BillingApiTypes } from "@stackcore/coreApiTypes"; const UpgradeEmail = (props: { emails: string[]; workspaceName: string; oldSubscription: { - product: StripeProduct; - billingCycle: StripeBillingCycle | null; + product: BillingApiTypes.StripeProduct; + billingCycle: BillingApiTypes.StripeBillingCycle | null; }; newSubscription: { - product: StripeProduct; - billingCycle: StripeBillingCycle | null; + product: BillingApiTypes.StripeProduct; + billingCycle: BillingApiTypes.StripeBillingCycle | null; }; }) => { const previewText = "Confirmation of your subscription upgrade | NanoAPI"; diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 018ed7f..19688df 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -1,8 +1,9 @@ -import { initKyselyDb } from "./db/database.ts"; +import { initKyselyDb } from "@stackcore/db"; +import { migrateToLatest } from "@stackcore/db"; + import api from "./api/index.ts"; -import { migrateToLatest } from "./db/migrator.ts"; import * as Sentry from "@sentry/deno"; -import settings from "./settings.ts"; +import settings from "@stackcore/settings"; if (settings.SENTRY.DSN) { console.info("Initializing Sentry..."); @@ -21,7 +22,9 @@ if (settings.SENTRY.DSN) { console.error("Skipping Sentry initialization, no DSN provided"); } +console.info("Initializing database..."); initKyselyDb(); +console.info("Database initialized"); console.info("Migrating database to latest version..."); await migrateToLatest(); diff --git a/packages/core/src/manifest/auditManifest/types.ts b/packages/core/src/manifest/auditManifest/types.ts deleted file mode 100644 index c2c36ec..0000000 --- a/packages/core/src/manifest/auditManifest/types.ts +++ /dev/null @@ -1,42 +0,0 @@ -import z from "zod"; -import { - metricCharacterCount, - metricCodeCharacterCount, - metricCodeLineCount, - metricCyclomaticComplexity, - metricDependencyCount, - metricDependentCount, - metricLinesCount, -} from "../dependencyManifest/types.ts"; - -const auditAlertSchema = z.object({ - metric: z.enum([ - metricLinesCount, - metricCodeLineCount, - metricCharacterCount, - metricCodeCharacterCount, - metricDependencyCount, - metricDependentCount, - metricCyclomaticComplexity, - ]), - severity: z.number().min(1).max(5), - message: z.object({ - short: z.string(), - long: z.string(), - }), -}); - -const symbolAuditManifestSchema = z.object({ - id: z.string(), - alerts: z.record(z.string(), auditAlertSchema), -}); - -const fileAuditManifestSchema = z.object({ - id: z.string(), - alerts: z.record(z.string(), auditAlertSchema), - symbols: z.record(z.string(), symbolAuditManifestSchema), -}); - -const auditManifestSchema = z.record(z.string(), fileAuditManifestSchema); - -export type AuditManifest = z.infer; diff --git a/packages/core/src/manifest/service.ts b/packages/core/src/manifest/service.ts index 0dc0c70..b789f73 100644 --- a/packages/core/src/manifest/service.ts +++ b/packages/core/src/manifest/service.ts @@ -1,5 +1,5 @@ -import type { AuditManifest } from "./auditManifest/types.ts"; import { + type AuditManifest, type DependencyManifest, type DependencyManifestV1, metricCharacterCount, @@ -9,7 +9,7 @@ import { metricDependencyCount, metricDependentCount, metricLinesCount, -} from "./dependencyManifest/types.ts"; +} from "@stackcore/manifests"; function getNumberSeverityLevel( value: number, diff --git a/packages/core/src/manifest/types.ts b/packages/core/src/manifest/types.ts deleted file mode 100644 index 21fb53f..0000000 --- a/packages/core/src/manifest/types.ts +++ /dev/null @@ -1,23 +0,0 @@ -export { - type DependencyManifest, - type Metric, - metricCharacterCount, - metricCodeCharacterCount, - metricCodeLineCount, - metricCyclomaticComplexity, - metricDependencyCount, - metricDependentCount, - metricLinesCount, - type SymbolType, - symbolTypeClass, - symbolTypeDelegate, - symbolTypeEnum, - symbolTypeFunction, - symbolTypeInterface, - symbolTypeRecord, - symbolTypeStruct, - symbolTypeTypedef, - symbolTypeUnion, - symbolTypeVariable, -} from "./dependencyManifest/types.ts"; -export type { AuditManifest } from "./auditManifest/types.ts"; diff --git a/packages/core/src/stripe/index.ts b/packages/core/src/stripe/index.ts index 76f9519..c31c72d 100644 --- a/packages/core/src/stripe/index.ts +++ b/packages/core/src/stripe/index.ts @@ -1,14 +1,6 @@ import stripe from "stripe"; -import settings from "../settings.ts"; -import { - BASIC_PRODUCT, - MONTHLY_BILLING_CYCLE, - PREMIUM_PRODUCT, - PRO_PRODUCT, - type StripeBillingCycle, - type StripeProduct, - YEARLY_BILLING_CYCLE, -} from "../db/models/workspace.ts"; +import settings from "@stackcore/settings"; +import { BillingApiTypes } from "@stackcore/coreApiTypes"; export function getStripe() { if (settings.STRIPE.USE_MOCK) { @@ -43,35 +35,30 @@ export class StripeService { } private getStandardStripeProductPriceId( - product: StripeProduct, - billingCycle: StripeBillingCycle, + product: BillingApiTypes.StripeProduct, + billingCycle: BillingApiTypes.StripeBillingCycle, ) { - if (product === BASIC_PRODUCT) { - if (billingCycle === MONTHLY_BILLING_CYCLE) { - return settings.STRIPE.PRODUCTS[BASIC_PRODUCT][MONTHLY_BILLING_CYCLE] - .PRICE_ID; + if (product === BillingApiTypes.STRIPE_BASIC_PRODUCT) { + if (billingCycle === BillingApiTypes.STRIPE_MONTHLY_BILLING_CYCLE) { + return settings.STRIPE.PRODUCTS.BASIC.MONTHLY.PRICE_ID; } } - if (product === PRO_PRODUCT) { - if (billingCycle === MONTHLY_BILLING_CYCLE) { - return settings.STRIPE.PRODUCTS[PRO_PRODUCT][MONTHLY_BILLING_CYCLE] - .PRICE_ID; + if (product === BillingApiTypes.STRIPE_PRO_PRODUCT) { + if (billingCycle === BillingApiTypes.STRIPE_MONTHLY_BILLING_CYCLE) { + return settings.STRIPE.PRODUCTS.PRO.MONTHLY.PRICE_ID; } - if (billingCycle === YEARLY_BILLING_CYCLE) { - return settings.STRIPE.PRODUCTS[PRO_PRODUCT][YEARLY_BILLING_CYCLE] - .PRICE_ID; + if (billingCycle === BillingApiTypes.STRIPE_YEARLY_BILLING_CYCLE) { + return settings.STRIPE.PRODUCTS.PRO.YEARLY.PRICE_ID; } } - if (product === PREMIUM_PRODUCT) { - if (billingCycle === MONTHLY_BILLING_CYCLE) { - return settings.STRIPE.PRODUCTS[PREMIUM_PRODUCT][MONTHLY_BILLING_CYCLE] - .PRICE_ID; + if (product === BillingApiTypes.STRIPE_PREMIUM_PRODUCT) { + if (billingCycle === BillingApiTypes.STRIPE_MONTHLY_BILLING_CYCLE) { + return settings.STRIPE.PRODUCTS.PREMIUM.MONTHLY.PRICE_ID; } - if (billingCycle === YEARLY_BILLING_CYCLE) { - return settings.STRIPE.PRODUCTS[PREMIUM_PRODUCT][YEARLY_BILLING_CYCLE] - .PRICE_ID; + if (billingCycle === BillingApiTypes.STRIPE_YEARLY_BILLING_CYCLE) { + return settings.STRIPE.PRODUCTS.PREMIUM.YEARLY.PRICE_ID; } } @@ -81,8 +68,8 @@ export class StripeService { public async createSubscription( stripeCustomerId: string, - product: StripeProduct, - licensePeriod: StripeBillingCycle, + product: BillingApiTypes.StripeProduct, + licensePeriod: BillingApiTypes.StripeBillingCycle, billingThreshold: number | null, ) { const priceId = this.getStandardStripeProductPriceId( @@ -129,8 +116,8 @@ export class StripeService { public async switchSubscription( stripeCustomerId: string, - product: StripeProduct, - licensePeriod: StripeBillingCycle, + product: BillingApiTypes.StripeProduct, + licensePeriod: BillingApiTypes.StripeBillingCycle, billingThreshold: number | null, switchMethod: "upgrade" | "downgrade", ) { @@ -320,4 +307,41 @@ export class StripeService { return totalUsage; } + + /* + subscriptionStatus: The status of the subscription in Stripe + Returns true if the workspace should have access, false otherwise + */ + public shouldHaveAccess( + subscriptionStatus: stripe.Subscription.Status, + ) { + switch (subscriptionStatus) { + // All good + case "active": + // trial period, all good + case "trialing": + // invoice cannot be paid, + // after 23h will move to incomplete_expired, where we will block access + case "incomplete": + return true; + // this will move to unpaid eventually, + // once stripe has exhausted all retry (can be configured) + case "past_due": + // most likely user changed their subscription. + // We block access, when new subscription is created, + // another event is triggered and access is restored + case "canceled": + // Subscription paused from dashboard, we block access + case "paused": + // Stripe cannot pay the invoice, we block access + case "unpaid": + // Invoice expired, we block access + case "incomplete_expired": + return false; + + // Should not happen + default: + throw new Error(`Unknown subscription status: ${subscriptionStatus}`); + } + } } diff --git a/packages/core/src/testHelpers/auth.ts b/packages/core/src/testHelpers/auth.ts index 26a22f2..247d2dd 100644 --- a/packages/core/src/testHelpers/auth.ts +++ b/packages/core/src/testHelpers/auth.ts @@ -1,5 +1,5 @@ import { AuthService } from "../api/auth/service.ts"; -import { db } from "../db/database.ts"; +import { db } from "@stackcore/db"; export async function createTestUserAndToken() { const email = `test-${crypto.randomUUID()}@example.com`; diff --git a/packages/core/src/testHelpers/db.ts b/packages/core/src/testHelpers/db.ts index e7c7781..21ca303 100644 --- a/packages/core/src/testHelpers/db.ts +++ b/packages/core/src/testHelpers/db.ts @@ -1,4 +1,4 @@ -import { db } from "../db/database.ts"; +import { db } from "@stackcore/db"; export async function resetTables() { await db.deleteFrom("manifest").execute(); diff --git a/packages/coreApiTypes/deno.json b/packages/coreApiTypes/deno.json new file mode 100644 index 0000000..6e0554f --- /dev/null +++ b/packages/coreApiTypes/deno.json @@ -0,0 +1,7 @@ +{ + "name": "@stackcore/coreApiTypes", + "exports": "./src/index.ts", + "imports": { + "@stackcore/manifests": "../manifests/src/index.ts" + } +} diff --git a/packages/coreApiTypes/src/apis/auth.ts b/packages/coreApiTypes/src/apis/auth.ts new file mode 100644 index 0000000..40c1186 --- /dev/null +++ b/packages/coreApiTypes/src/apis/auth.ts @@ -0,0 +1,53 @@ +export type Session = { + userId: number; + email: string; +}; + +export type RequestOtpPayload = { + email: string; +}; + +export function prepareRequestOtp(payload: RequestOtpPayload): { + url: string; + method: "POST"; + body: RequestOtpPayload; +} { + return { + url: "/auth/requestOtp", + method: "POST", + body: payload, + }; +} + +export type VerifyOtpPayload = { + email: string; + otp: string; +}; + +export function prepareVerifyOtp(payload: VerifyOtpPayload): { + url: string; + method: "POST"; + body: VerifyOtpPayload; +} { + return { + url: "/auth/verifyOtp", + method: "POST", + body: payload, + }; +} + +export type VerifyOtpResponse = { + token: string; +}; + +export function prepareMe(): { + url: string; + method: "GET"; + body: undefined; +} { + return { + url: "/auth/me", + method: "GET", + body: undefined, + }; +} diff --git a/packages/core/src/api/billing/types.ts b/packages/coreApiTypes/src/apis/billing.ts similarity index 51% rename from packages/core/src/api/billing/types.ts rename to packages/coreApiTypes/src/apis/billing.ts index 9ce9c11..99daa06 100644 --- a/packages/core/src/api/billing/types.ts +++ b/packages/coreApiTypes/src/apis/billing.ts @@ -1,13 +1,20 @@ -import { z } from "zod"; -import { - BASIC_PRODUCT, - MONTHLY_BILLING_CYCLE, - PREMIUM_PRODUCT, - PRO_PRODUCT, - type StripeBillingCycle, - type StripeProduct, - YEARLY_BILLING_CYCLE, -} from "../../db/models/workspace.ts"; +export const STRIPE_MONTHLY_BILLING_CYCLE = "MONTHLY"; +export const STRIPE_YEARLY_BILLING_CYCLE = "YEARLY"; + +export type StripeBillingCycle = + | typeof STRIPE_MONTHLY_BILLING_CYCLE + | typeof STRIPE_YEARLY_BILLING_CYCLE; + +export const STRIPE_BASIC_PRODUCT = "BASIC"; +export const STRIPE_PRO_PRODUCT = "PRO"; +export const STRIPE_PREMIUM_PRODUCT = "PREMIUM"; +export const STRIPE_CUSTOM_PRODUCT = "CUSTOM"; + +export type StripeProduct = + | typeof STRIPE_BASIC_PRODUCT + | typeof STRIPE_PRO_PRODUCT + | typeof STRIPE_PREMIUM_PRODUCT + | typeof STRIPE_CUSTOM_PRODUCT; export type SubscriptionDetails = { currentUsage: number; @@ -21,29 +28,34 @@ export type SubscriptionDetails = { export function prepareGetSubscription( workspaceId: number, -) { +): { + url: string; + method: "GET"; + body: undefined; +} { const searchParams = new URLSearchParams(); searchParams.set("workspaceId", workspaceId.toString()); return { url: `/billing/subscription?${searchParams.toString()}`, method: "GET", + body: undefined, }; } -export const upgradeSubscriptionRequestSchema = z.object({ - workspaceId: z.number(), - product: z.enum([BASIC_PRODUCT, PRO_PRODUCT, PREMIUM_PRODUCT]), - billingCycle: z.enum([MONTHLY_BILLING_CYCLE, YEARLY_BILLING_CYCLE]), -}); - -export type UpgradeSubscriptionRequest = z.infer< - typeof upgradeSubscriptionRequestSchema ->; +export type UpgradeSubscriptionRequest = { + workspaceId: number; + product: StripeProduct; + billingCycle: StripeBillingCycle; +}; export function prepareUpgradeSubscription( payload: UpgradeSubscriptionRequest, -) { +): { + url: string; + method: "POST"; + body: UpgradeSubscriptionRequest; +} { return { url: "/billing/subscription/upgrade", method: "POST", @@ -51,19 +63,19 @@ export function prepareUpgradeSubscription( }; } -export const downgradeSubscriptionRequestSchema = z.object({ - workspaceId: z.number(), - product: z.enum([BASIC_PRODUCT, PRO_PRODUCT, PREMIUM_PRODUCT]), - billingCycle: z.enum([MONTHLY_BILLING_CYCLE, YEARLY_BILLING_CYCLE]), -}); - -export type DowngradeSubscriptionRequest = z.infer< - typeof downgradeSubscriptionRequestSchema ->; +export type DowngradeSubscriptionRequest = { + workspaceId: number; + product: StripeProduct; + billingCycle: StripeBillingCycle; +}; export function prepareDowngradeSubscription( payload: DowngradeSubscriptionRequest, -) { +): { + url: string; + method: "POST"; + body: DowngradeSubscriptionRequest; +} { return { url: "/billing/subscription/downgrade", method: "POST", @@ -71,14 +83,10 @@ export function prepareDowngradeSubscription( }; } -export const createPortalSessionRequestSchema = z.object({ - workspaceId: z.number(), - returnUrl: z.string(), -}); - -export type CreatePortalSessionRequest = z.infer< - typeof createPortalSessionRequestSchema ->; +export type CreatePortalSessionRequest = { + workspaceId: number; + returnUrl: string; +}; export type CreatePortalSessionResponse = { url: string; @@ -86,7 +94,11 @@ export type CreatePortalSessionResponse = { export function prepareCreatePortalSession( payload: CreatePortalSessionRequest, -) { +): { + url: string; + method: "POST"; + body: CreatePortalSessionRequest; +} { return { url: "/billing/portal", method: "POST", @@ -96,7 +108,11 @@ export function prepareCreatePortalSession( export function prepareCreatePortalSessionPaymentMethod( payload: CreatePortalSessionRequest, -) { +): { + url: string; + method: "POST"; + body: CreatePortalSessionRequest; +} { return { url: "/billing/portal/paymentMethod", method: "POST", diff --git a/packages/coreApiTypes/src/apis/invitation.ts b/packages/coreApiTypes/src/apis/invitation.ts new file mode 100644 index 0000000..0426713 --- /dev/null +++ b/packages/coreApiTypes/src/apis/invitation.ts @@ -0,0 +1,31 @@ +export type CreateInvitationPayload = { + workspaceId: number; + email: string; + returnUrl: string; +}; + +export function prepareCreateInvitation( + payload: CreateInvitationPayload, +): { + url: string; + method: "POST"; + body: CreateInvitationPayload; +} { + return { + url: `/invitations`, + method: "POST", + body: payload, + }; +} + +export function prepareClaimInvitation(invitationUuid: string): { + url: string; + method: "POST"; + body: undefined; +} { + return { + url: `/invitations/${invitationUuid}/claim`, + method: "POST", + body: undefined, + }; +} diff --git a/packages/coreApiTypes/src/apis/labeling.ts b/packages/coreApiTypes/src/apis/labeling.ts new file mode 100644 index 0000000..a4131a0 --- /dev/null +++ b/packages/coreApiTypes/src/apis/labeling.ts @@ -0,0 +1,32 @@ +export type UploadTemporaryContentPayload = { + path: string; + content: string; +}; + +export function prepareUploadTemporaryContent( + payload: UploadTemporaryContentPayload, +): { + url: string; + method: "POST"; + body: UploadTemporaryContentPayload; +} { + return { + url: "/temp", + method: "POST", + body: payload, + }; +} + +export type UploadTemporaryContentResponse = { + path: string; + bucketName: string; +}; + +export type StartLabelingPayload = { + manifestId: number; + fileMapName: string; +}; + +export type StartLabelingResponse = { + message: string; +}; diff --git a/packages/coreApiTypes/src/apis/manifest.ts b/packages/coreApiTypes/src/apis/manifest.ts new file mode 100644 index 0000000..20daa60 --- /dev/null +++ b/packages/coreApiTypes/src/apis/manifest.ts @@ -0,0 +1,126 @@ +import type { AuditManifest } from "@stackcore/manifests"; + +export type CreateManifestPayload = { + projectId: number; + branch: string | null; + commitSha: string | null; + commitShaDate: Date | null; + manifest: Record; +}; + +export type CreateManifestResponse = { + id: number; +}; + +export function prepareCreateManifest( + payload: CreateManifestPayload, +): { + url: string; + method: "POST"; + body: CreateManifestPayload; +} { + return { + url: "/manifests", + method: "POST", + body: payload, + }; +} + +export function prepareGetManifests(payload: { + page: number; + limit: number; + search?: string; + projectId?: number; + workspaceId?: number; +}): { + url: string; + method: "GET"; + body: undefined; +} { + const searchParams = new URLSearchParams(); + searchParams.set("page", payload.page.toString()); + searchParams.set("limit", payload.limit.toString()); + if (payload.search) { + searchParams.set("search", payload.search); + } + if (payload.projectId) { + searchParams.set("projectId", payload.projectId.toString()); + } + if (payload.workspaceId) { + searchParams.set("workspaceId", payload.workspaceId.toString()); + } + + return { + url: `/manifests?${searchParams.toString()}`, + method: "GET", + body: undefined, + }; +} + +export function prepareGetManifestDetails( + manifestId: number, +): { + url: string; + method: "GET"; + body: undefined; +} { + return { + url: `/manifests/${manifestId}`, + method: "GET", + body: undefined, + }; +} + +export function prepareDeleteManifest( + manifestId: number, +): { + url: string; + method: "DELETE"; + body: undefined; +} { + return { + url: `/manifests/${manifestId}`, + method: "DELETE", + body: undefined, + }; +} + +export type GetManifestsResponse = { + results: { + id: number; + project_id: number; + branch: string | null; + commitSha: string | null; + commitShaDate: Date | null; + version: number; + created_at: Date; + }[]; + total: number; +}; + +export type GetManifestDetailsResponse = { + id: number; + project_id: number; + branch: string | null; + commitSha: string | null; + commitShaDate: Date | null; + version: number; + manifest: string; + created_at: Date; +}; + +export function prepareGetManifestAudit( + manifestId: number, +): { + url: string; + method: "GET"; + body: undefined; +} { + return { + url: `/manifests/${manifestId}/audit`, + method: "GET", + body: undefined, + }; +} + +export type GetManifestAuditResponse = AuditManifest; diff --git a/packages/core/src/api/member/types.ts b/packages/coreApiTypes/src/apis/member.ts similarity index 68% rename from packages/core/src/api/member/types.ts rename to packages/coreApiTypes/src/apis/member.ts index f3546b9..8c43ddc 100644 --- a/packages/core/src/api/member/types.ts +++ b/packages/coreApiTypes/src/apis/member.ts @@ -1,18 +1,17 @@ -import { z } from "zod"; -import type { MemberRole } from "../../db/models/member.ts"; - -export { - ADMIN_ROLE, - MEMBER_ROLE, - type MemberRole, -} from "../../db/models/member.ts"; +export const ADMIN_ROLE = "admin"; +export const MEMBER_ROLE = "member"; +export type MemberRole = typeof ADMIN_ROLE | typeof MEMBER_ROLE; export function prepareGetMembers(payload: { workspaceId: number; page: number; limit: number; search?: string; -}) { +}): { + url: string; + method: "GET"; + body: undefined; +} { const searchParams = new URLSearchParams(); searchParams.set("workspaceId", payload.workspaceId.toString()); searchParams.set("page", payload.page.toString()); @@ -38,15 +37,18 @@ export type GetMembersResponse = { total: number; }; -export const updateMemberRoleSchema = z.object({ - role: z.enum(["admin", "member"]), -}); -export type UpdateMemberRolePayload = z.infer; +export type UpdateMemberRolePayload = { + role: MemberRole; +}; export function prepareUpdateMemberRole( memberId: number, payload: UpdateMemberRolePayload, -) { +): { + url: string; + method: "PATCH"; + body: UpdateMemberRolePayload; +} { return { url: `/members/${memberId}`, method: "PATCH", @@ -54,7 +56,13 @@ export function prepareUpdateMemberRole( }; } -export function prepareDeleteMember(memberId: number) { +export function prepareDeleteMember( + memberId: number, +): { + url: string; + method: "DELETE"; + body: undefined; +} { return { url: `/members/${memberId}`, method: "DELETE", diff --git a/packages/coreApiTypes/src/apis/project.ts b/packages/coreApiTypes/src/apis/project.ts new file mode 100644 index 0000000..4fed4b3 --- /dev/null +++ b/packages/coreApiTypes/src/apis/project.ts @@ -0,0 +1,173 @@ +export type CreateProjectPayload = { + name: string; + repoUrl: string; + workspaceId: number; + maxCodeCharPerSymbol: number; + maxCodeCharPerFile: number; + maxCharPerSymbol: number; + maxCharPerFile: number; + maxCodeLinePerSymbol: number; + maxCodeLinePerFile: number; + maxLinePerSymbol: number; + maxLinePerFile: number; + maxDependencyPerSymbol: number; + maxDependencyPerFile: number; + maxDependentPerSymbol: number; + maxDependentPerFile: number; + maxCyclomaticComplexityPerSymbol: number; + maxCyclomaticComplexityPerFile: number; +}; + +export type CreateProjectResponse = { + id: number; +}; + +export function prepareCreateProject( + payload: CreateProjectPayload, +): { + url: string; + method: "POST"; + body: CreateProjectPayload; +} { + return { + url: "/projects", + method: "POST", + body: payload, + }; +} + +export function prepareGetProjects(payload: { + page: number; + limit: number; + search?: string; + workspaceId?: number; +}): { + url: string; + method: "GET"; + body: undefined; +} { + const searchParams = new URLSearchParams(); + searchParams.set("page", payload.page.toString()); + searchParams.set("limit", payload.limit.toString()); + if (payload.search) { + searchParams.set("search", payload.search); + } + if (payload.workspaceId) { + searchParams.set("workspaceId", payload.workspaceId.toString()); + } + + return { + url: `/projects?${searchParams.toString()}`, + method: "GET", + body: undefined, + }; +} + +export function prepareGetProjectDetails( + projectId: number, +): { + url: string; + method: "GET"; + body: undefined; +} { + return { + url: `/projects/${projectId}`, + method: "GET", + body: undefined, + }; +} + +export type UpdateProjectPayload = { + name: string; + repoUrl: string; + maxCodeCharPerSymbol: number; + maxCodeCharPerFile: number; + maxCharPerSymbol: number; + maxCharPerFile: number; + maxCodeLinePerSymbol: number; + maxCodeLinePerFile: number; + maxLinePerSymbol: number; + maxLinePerFile: number; + maxDependencyPerSymbol: number; + maxDependencyPerFile: number; + maxDependentPerSymbol: number; + maxDependentPerFile: number; + maxCyclomaticComplexityPerSymbol: number; + maxCyclomaticComplexityPerFile: number; +}; + +export function prepareUpdateProject( + projectId: number, + payload: UpdateProjectPayload, +): { + url: string; + method: "PATCH"; + body: UpdateProjectPayload; +} { + return { + url: `/projects/${projectId}`, + method: "PATCH", + body: payload, + }; +} + +export function prepareDeleteProject( + projectId: number, +): { + url: string; + method: "DELETE"; + body: undefined; +} { + return { + url: `/projects/${projectId}`, + method: "DELETE", + body: undefined, + }; +} + +export type GetProjectsResponse = { + results: { + id: number; + name: string; + repo_url: string; + workspace_id: number; + max_code_char_per_symbol: number; + max_code_char_per_file: number; + max_char_per_symbol: number; + max_char_per_file: number; + max_code_line_per_symbol: number; + max_code_line_per_file: number; + max_line_per_symbol: number; + max_line_per_file: number; + max_dependency_per_symbol: number; + max_dependency_per_file: number; + max_dependent_per_symbol: number; + max_dependent_per_file: number; + max_cyclomatic_complexity_per_symbol: number; + max_cyclomatic_complexity_per_file: number; + created_at: Date; + }[]; + total: number; +}; + +export type GetProjectDetailsResponse = { + id: number; + name: string; + repo_url: string; + workspace_id: number; + max_code_char_per_symbol: number; + max_code_char_per_file: number; + max_char_per_symbol: number; + max_char_per_file: number; + max_code_line_per_symbol: number; + max_code_line_per_file: number; + max_line_per_symbol: number; + max_line_per_file: number; + max_dependency_per_symbol: number; + max_dependency_per_file: number; + max_dependent_per_symbol: number; + max_dependent_per_file: number; + max_cyclomatic_complexity_per_symbol: number; + max_cyclomatic_complexity_per_file: number; + created_at: Date; +}; diff --git a/packages/core/src/api/token/types.ts b/packages/coreApiTypes/src/apis/token.ts similarity index 69% rename from packages/core/src/api/token/types.ts rename to packages/coreApiTypes/src/apis/token.ts index 1948e95..0ceed27 100644 --- a/packages/core/src/api/token/types.ts +++ b/packages/coreApiTypes/src/apis/token.ts @@ -1,10 +1,6 @@ -import { z } from "zod"; - -export const createTokenSchema = z.object({ - name: z.string().nonempty(), -}); - -export type CreateTokenPayload = z.infer; +export type CreateTokenPayload = { + name: string; +}; export type CreateTokenResponse = { uuid: string; @@ -21,7 +17,13 @@ export type GetTokensResponse = { total: number; }; -export function prepareCreateToken(payload: CreateTokenPayload) { +export function prepareCreateToken( + payload: CreateTokenPayload, +): { + url: string; + method: "POST"; + body: CreateTokenPayload; +} { return { url: `/tokens`, method: "POST", @@ -33,7 +35,11 @@ export function prepareGetTokens(payload: { page: number; limit: number; search?: string; -}) { +}): { + url: string; + method: "GET"; + body: undefined; +} { const searchParams = new URLSearchParams(); searchParams.set("page", payload.page.toString()); searchParams.set("limit", payload.limit.toString()); @@ -48,7 +54,13 @@ export function prepareGetTokens(payload: { }; } -export function prepareDeleteToken(tokenId: number) { +export function prepareDeleteToken( + tokenId: number, +): { + url: string; + method: "DELETE"; + body: undefined; +} { return { url: `/tokens/${tokenId}`, method: "DELETE", diff --git a/packages/core/src/api/workspace/types.ts b/packages/coreApiTypes/src/apis/workspace.ts similarity index 58% rename from packages/core/src/api/workspace/types.ts rename to packages/coreApiTypes/src/apis/workspace.ts index b798445..8db349b 100644 --- a/packages/core/src/api/workspace/types.ts +++ b/packages/coreApiTypes/src/apis/workspace.ts @@ -1,29 +1,20 @@ -import { z } from "zod"; -import type { MemberRole } from "../../db/models/member.ts"; +import type { MemberRole } from "./member.ts"; -export { - BASIC_PRODUCT, - CUSTOM_PRODUCT, - MONTHLY_BILLING_CYCLE, - PREMIUM_PRODUCT, - PRO_PRODUCT, - type StripeBillingCycle, - type StripeProduct, - YEARLY_BILLING_CYCLE, -} from "../../db/models/workspace.ts"; - -export const createWorkspacePayloadSchema = z.object({ - name: z.string(), -}); -export type CreateWorkspacePayload = z.infer< - typeof createWorkspacePayloadSchema ->; +export type CreateWorkspacePayload = { + name: string; +}; export type CreateWorkspaceResponse = { id: number; }; -export function prepareCreateWorkspace(payload: CreateWorkspacePayload) { +export function prepareCreateWorkspace( + payload: CreateWorkspacePayload, +): { + url: string; + method: "POST"; + body: CreateWorkspacePayload; +} { return { url: "/workspaces", method: "POST", @@ -35,7 +26,11 @@ export function prepareGetWorkspaces(payload: { page: number; limit: number; search?: string; -}) { +}): { + url: string; + method: "GET"; + body: undefined; +} { const searchParams = new URLSearchParams(); searchParams.set("page", payload.page.toString()); searchParams.set("limit", payload.limit.toString()); @@ -61,17 +56,18 @@ export type GetWorkspacesResponse = { total: number; }; -export const updateWorkspaceSchema = z.object({ - name: z.string(), -}); -export type UpdateWorkspacePayload = z.infer< - typeof updateWorkspaceSchema ->; +export type UpdateWorkspacePayload = { + name: string; +}; export function prepareUpdateWorkspace( workspaceId: number, payload: UpdateWorkspacePayload, -) { +): { + url: string; + method: "PATCH"; + body: UpdateWorkspacePayload; +} { return { url: `/workspaces/${workspaceId}`, method: "PATCH", @@ -79,7 +75,13 @@ export function prepareUpdateWorkspace( }; } -export function prepareDeactivateWorkspace(workspaceId: number) { +export function prepareDeactivateWorkspace( + workspaceId: number, +): { + url: string; + method: "POST"; + body: undefined; +} { return { url: `/workspaces/${workspaceId}/deactivate`, method: "POST", diff --git a/packages/coreApiTypes/src/index.ts b/packages/coreApiTypes/src/index.ts new file mode 100644 index 0000000..24aa3f4 --- /dev/null +++ b/packages/coreApiTypes/src/index.ts @@ -0,0 +1,21 @@ +import * as AuthApiTypes from "./apis/auth.ts"; +import * as WorkspaceApiTypes from "./apis/workspace.ts"; +import * as InvitationApiTypes from "./apis/invitation.ts"; +import * as MemberApiTypes from "./apis/member.ts"; +import * as BillingApiTypes from "./apis/billing.ts"; +import * as ProjectApiTypes from "./apis/project.ts"; +import * as ManifestApiTypes from "./apis/manifest.ts"; +import * as TokenApiTypes from "./apis/token.ts"; +import * as LabelingApiTypes from "./apis/labeling.ts"; + +export { + AuthApiTypes, + BillingApiTypes, + InvitationApiTypes, + LabelingApiTypes, + ManifestApiTypes, + MemberApiTypes, + ProjectApiTypes, + TokenApiTypes, + WorkspaceApiTypes, +}; diff --git a/packages/db/deno.json b/packages/db/deno.json new file mode 100644 index 0000000..12fd302 --- /dev/null +++ b/packages/db/deno.json @@ -0,0 +1,13 @@ +{ + "name": "@stackcore/db", + "exports": "./src/index.ts", + "imports": { + "@std/path": "jsr:@std/path@^1.0.9", + "@stackcore/settings": "../settings/src/index.ts", + "@types/pg": "npm:@types/pg@^8.15.2", + "@types/pg-pool": "npm:@types/pg-pool@^2.0.6", + "kysely": "npm:kysely@^0.28.2", + "pg": "npm:pg@^8.16.0", + "pg-pool": "npm:pg-pool@^3.10.0" + } +} diff --git a/packages/core/src/db/database.ts b/packages/db/src/index.ts similarity index 59% rename from packages/core/src/db/database.ts rename to packages/db/src/index.ts index f6a697d..b82f4a5 100644 --- a/packages/core/src/db/database.ts +++ b/packages/db/src/index.ts @@ -1,7 +1,17 @@ import type { Database } from "./types.ts"; import { Kysely, PostgresDialect } from "kysely"; import Pool from "pg-pool"; -import settings from "../settings.ts"; +import settings from "@stackcore/settings"; + +// Export models and types +export * from "./models/user.ts"; +export * from "./models/workspace.ts"; +export * from "./models/member.ts"; +export * from "./models/invitation.ts"; +export * from "./models/project.ts"; +export * from "./models/manifest.ts"; +export * from "./models/token.ts"; +export * from "./types.ts"; export let db: Kysely; @@ -24,3 +34,5 @@ export function initKyselyDb() { export async function destroyKyselyDb() { await db.destroy(); } + +export { migrateToLatest } from "./migrator.ts"; diff --git a/packages/core/src/db/migrations/0001.migration.ts b/packages/db/src/migrations/0001.migration.ts similarity index 100% rename from packages/core/src/db/migrations/0001.migration.ts rename to packages/db/src/migrations/0001.migration.ts diff --git a/packages/core/src/db/migrator.ts b/packages/db/src/migrator.ts similarity index 96% rename from packages/core/src/db/migrator.ts rename to packages/db/src/migrator.ts index 2b23acd..22ad80f 100644 --- a/packages/core/src/db/migrator.ts +++ b/packages/db/src/migrator.ts @@ -1,7 +1,7 @@ import * as path from "@std/path"; import { promises } from "node:fs"; import { FileMigrationProvider, Migrator } from "kysely"; -import { db } from "./database.ts"; +import { db } from "./index.ts"; export async function migrateToLatest() { const migrator = new Migrator({ diff --git a/packages/core/src/db/models/invitation.ts b/packages/db/src/models/invitation.ts similarity index 100% rename from packages/core/src/db/models/invitation.ts rename to packages/db/src/models/invitation.ts diff --git a/packages/core/src/db/models/manifest.ts b/packages/db/src/models/manifest.ts similarity index 100% rename from packages/core/src/db/models/manifest.ts rename to packages/db/src/models/manifest.ts diff --git a/packages/core/src/db/models/member.ts b/packages/db/src/models/member.ts similarity index 100% rename from packages/core/src/db/models/member.ts rename to packages/db/src/models/member.ts diff --git a/packages/core/src/db/models/project.ts b/packages/db/src/models/project.ts similarity index 100% rename from packages/core/src/db/models/project.ts rename to packages/db/src/models/project.ts diff --git a/packages/core/src/db/models/token.ts b/packages/db/src/models/token.ts similarity index 100% rename from packages/core/src/db/models/token.ts rename to packages/db/src/models/token.ts diff --git a/packages/core/src/db/models/user.ts b/packages/db/src/models/user.ts similarity index 100% rename from packages/core/src/db/models/user.ts rename to packages/db/src/models/user.ts diff --git a/packages/db/src/models/workspace.ts b/packages/db/src/models/workspace.ts new file mode 100644 index 0000000..155896f --- /dev/null +++ b/packages/db/src/models/workspace.ts @@ -0,0 +1,21 @@ +import type { + ColumnType, + Generated, + Insertable, + Selectable, + Updateable, +} from "kysely"; + +export interface WorkspaceTable { + id: Generated; + name: string; + isTeam: boolean; + stripe_customer_id: string | null; + access_enabled: boolean; + deactivated: boolean; + created_at: ColumnType; +} + +export type Workspace = Selectable; +export type NewWorkspace = Insertable; +export type WorkspaceUpdate = Updateable; diff --git a/packages/core/src/db/scripts/migrate.ts b/packages/db/src/scripts/migrate.ts similarity index 64% rename from packages/core/src/db/scripts/migrate.ts rename to packages/db/src/scripts/migrate.ts index fab561c..621a21d 100644 --- a/packages/core/src/db/scripts/migrate.ts +++ b/packages/db/src/scripts/migrate.ts @@ -1,4 +1,4 @@ -import { destroyKyselyDb, initKyselyDb } from "../database.ts"; +import { destroyKyselyDb, initKyselyDb } from "../index.ts"; import { migrateToLatest } from "../migrator.ts"; initKyselyDb(); diff --git a/packages/core/src/db/types.ts b/packages/db/src/types.ts similarity index 100% rename from packages/core/src/db/types.ts rename to packages/db/src/types.ts diff --git a/packages/labeler/Dockerfile b/packages/labeler/Dockerfile new file mode 100644 index 0000000..5308504 --- /dev/null +++ b/packages/labeler/Dockerfile @@ -0,0 +1,12 @@ +FROM debian:bookworm-slim + +WORKDIR /app + +# Copy the pre-built executable +COPY dist/labeler /app/labeler + +# Expose the port (adjust if your app uses a different port) +EXPOSE 4001 + +# Run the compiled binary +CMD ["/app/labeler"] \ No newline at end of file diff --git a/packages/labeler/deno.json b/packages/labeler/deno.json new file mode 100644 index 0000000..442531a --- /dev/null +++ b/packages/labeler/deno.json @@ -0,0 +1,17 @@ +{ + "name": "@stackcore/labeler", + "exports": "./src/index.ts", + "tasks": { + "dev": "deno run --env-file=../../.env -A --watch src/index.ts", + "build": "deno compile --allow-all --output ./dist/labeler ./src/index.ts" + }, + "imports": { + "@stackcore/db": "../db/src/index.ts", + "@stackcore/settings": "../settings/src/index.ts", + "@stackcore/storage": "../storage/src/index.ts", + "@google-cloud/storage": "npm:@google-cloud/storage@^7.16.0", + "@sentry/deno": "npm:@sentry/deno@^9.28.1", + "zod": "npm:zod@^3.24.4", + "@oak/oak": "jsr:@oak/oak@^17.1.4" + } +} diff --git a/packages/labeler/src/api/index.ts b/packages/labeler/src/api/index.ts new file mode 100644 index 0000000..a28ec56 --- /dev/null +++ b/packages/labeler/src/api/index.ts @@ -0,0 +1,113 @@ +import { Application, Router, Status } from "@oak/oak"; +import { db } from "@stackcore/db"; +import { z } from "zod"; +import { startLabeling } from "../labeler/index.ts"; +import settings from "@stackcore/settings"; + +const api = new Application(); + +api.use((ctx, next) => { + ctx.response.headers.set("Access-Control-Allow-Origin", "*"); + ctx.response.headers.set( + "Access-Control-Allow-Headers", + "Content-Type, Authorization", + ); + ctx.response.headers.set( + "Access-Control-Allow-Methods", + "GET, POST, PUT, PATCH, DELETE, OPTIONS", + ); + + if (ctx.request.method === "OPTIONS") { + ctx.response.status = 204; + return; + } + + return next(); +}); + +// log all requests +api.use((ctx, next) => { + console.info(`${ctx.request.method} ${ctx.request.url}`); + return next(); +}); + +// check api key is valid +api.use((ctx, next) => { + const apiKey = ctx.request.headers.get("X-API-KEY"); + if (apiKey !== settings.LABELER.LABELER_API_KEY) { + ctx.response.status = Status.Unauthorized; + return; + } + return next(); +}); + +const router = new Router(); + +router.get("/health/liveness", (ctx) => { + ctx.response.status = 200; + ctx.response.body = { + status: "ok", + timestamp: new Date().toISOString(), + service: "stackcore/labeler", + }; +}); + +router.get("/health/readiness", async (ctx) => { + const checks = { + database: false, + }; + + let overallStatus = "ok"; + let statusCode = Status.OK; + + try { + // Check database connectivity + await db.selectFrom("user").select("id").limit(1).execute(); + checks.database = true; + } catch (error) { + console.error("Database health check failed:", error); + checks.database = false; + overallStatus = "error"; + statusCode = Status.ServiceUnavailable; + } + + ctx.response.status = statusCode; + ctx.response.body = { + status: overallStatus, + timestamp: new Date().toISOString(), + service: "stackcore/labeler", + checks, + }; +}); + +router.post("/start", async (ctx) => { + const body = await ctx.request.body.json(); + + const labelingStartSchema = z.object({ + manifestId: z.number(), + fileMapName: z.string(), + }); + + const parsedBody = labelingStartSchema.safeParse(body); + + if (!parsedBody.success) { + ctx.response.status = Status.BadRequest; + ctx.response.body = { error: parsedBody.error }; + return; + } + + startLabeling( + parsedBody.data.manifestId, + parsedBody.data.fileMapName, + ); + + ctx.response.status = Status.OK; + ctx.response.body = { + status: "ok", + }; +}); + +api.use(router.routes()); +api.use(router.allowedMethods()); + +export default api; diff --git a/packages/labeler/src/index.ts b/packages/labeler/src/index.ts new file mode 100644 index 0000000..433161a --- /dev/null +++ b/packages/labeler/src/index.ts @@ -0,0 +1,29 @@ +import { initKyselyDb } from "@stackcore/db"; + +import api from "./api/index.ts"; +import * as Sentry from "@sentry/deno"; +import settings from "@stackcore/settings"; + +if (settings.SENTRY.DSN) { + console.info("Initializing Sentry..."); + Sentry.init({ + dsn: settings.SENTRY.DSN, + }); + + // Oak handles errors automatically + // So we capture them here so sentry can get them + api.addEventListener("error", (event) => { + Sentry.captureException(event.error); + }); + + console.info("Sentry initialized"); +} else { + console.error("Skipping Sentry initialization, no DSN provided"); +} + +initKyselyDb(); + +console.info("Starting server..."); +const port = 4001; +api.listen({ port }); +console.info(`Server is running on port http://localhost:${port}`); diff --git a/packages/labeler/src/labeler/index.ts b/packages/labeler/src/labeler/index.ts new file mode 100644 index 0000000..7e395d6 --- /dev/null +++ b/packages/labeler/src/labeler/index.ts @@ -0,0 +1,26 @@ +import { downloadTemporaryFileFromBucket } from "@stackcore/storage"; +import z from "zod"; + +export async function startLabeling( + manifestId: number, + fileMapName: string, +) { + const fileMapContent = await downloadTemporaryFileFromBucket(fileMapName); + const fileMapSchema = z.record(z.string(), z.string().nullable()); + + const fileMapParsed = fileMapSchema.safeParse(JSON.parse(fileMapContent)); + + if (!fileMapParsed.success) { + throw new Error(`Invalid file map: ${fileMapContent}`); + } + + const fileMap = fileMapParsed.data; + + // TODO: perform labeling + console.log( + "TODO start labeling manifestId:", + manifestId, + "fileMap:", + fileMap, + ); +} diff --git a/packages/manifests/deno.json b/packages/manifests/deno.json new file mode 100644 index 0000000..0649a37 --- /dev/null +++ b/packages/manifests/deno.json @@ -0,0 +1,4 @@ +{ + "name": "@stackcore/settings", + "exports": "./src/index.ts" +} diff --git a/packages/manifests/src/index.ts b/packages/manifests/src/index.ts new file mode 100644 index 0000000..9ce30e5 --- /dev/null +++ b/packages/manifests/src/index.ts @@ -0,0 +1,2 @@ +export * from "./manifest/auditManifest.ts"; +export * from "./manifest/dependencyManifest.ts"; diff --git a/packages/manifests/src/manifest/auditManifest.ts b/packages/manifests/src/manifest/auditManifest.ts new file mode 100644 index 0000000..f614187 --- /dev/null +++ b/packages/manifests/src/manifest/auditManifest.ts @@ -0,0 +1,24 @@ +import type { Metric } from "./dependencyManifest.ts"; + +export type AuditManifest = Record; + symbols: Record; + }>; +}>; diff --git a/packages/core/src/manifest/dependencyManifest/types.ts b/packages/manifests/src/manifest/dependencyManifest.ts similarity index 51% rename from packages/core/src/manifest/dependencyManifest/types.ts rename to packages/manifests/src/manifest/dependencyManifest.ts index 0e5d415..caa1a5c 100644 --- a/packages/core/src/manifest/dependencyManifest/types.ts +++ b/packages/manifests/src/manifest/dependencyManifest.ts @@ -1,5 +1,3 @@ -import z from "zod"; - // Dependency manifest version 1 // We will add more versions in the future for backward compatibility @@ -43,64 +41,48 @@ export type SymbolType = | typeof symbolTypeRecord | typeof symbolTypeDelegate; -export const metricsSchema = z.object({ - [metricLinesCount]: z.number(), - [metricCodeLineCount]: z.number(), - [metricCharacterCount]: z.number(), - [metricCodeCharacterCount]: z.number(), - [metricDependencyCount]: z.number(), - [metricDependentCount]: z.number(), - [metricCyclomaticComplexity]: z.number(), -}); - -const symbolTypeSchema = z.enum([ - symbolTypeClass, - symbolTypeFunction, - symbolTypeVariable, - symbolTypeStruct, - symbolTypeEnum, - symbolTypeUnion, - symbolTypeTypedef, - symbolTypeInterface, - symbolTypeRecord, - symbolTypeDelegate, -]); - -const dependencyInfoSchema = z.object({ - id: z.string(), - isExternal: z.boolean(), - symbols: z.record(z.string(), z.string()), -}); - -const dependentInfoSchema = z.object({ - id: z.string(), - symbols: z.record(z.string(), z.string()), -}); - -const symbolDependencyManifestSchema = z.object({ - id: z.string(), - type: symbolTypeSchema, - metrics: metricsSchema, - dependencies: z.record(z.string(), dependencyInfoSchema), - dependents: z.record(z.string(), dependentInfoSchema), -}); - -const fileDependencyManifestSchema = z.object({ - id: z.string(), - filePath: z.string(), - language: z.string(), - metrics: metricsSchema, - dependencies: z.record(z.string(), dependencyInfoSchema), - dependents: z.record(z.string(), dependentInfoSchema), - symbols: z.record(z.string(), symbolDependencyManifestSchema), -}); - -const dependencyManifestV1Schema = z.record( - z.string(), - fileDependencyManifestSchema, -); - -export type DependencyManifestV1 = z.infer; +export type DependencyManifestV1 = Record; + } + >; + dependents: Record< + string, + { + id: string; + symbols: Record; + } + >; + symbols: Record< + string, + { + id: string; + type: SymbolType; + metrics: { + [key in Metric]: number; + }; + dependencies: Record; + }>; + dependents: Record; + }>; + } + >; +}>; // Union type for all dependency manifest versions // Add new versions here as they are created diff --git a/packages/settings/deno.json b/packages/settings/deno.json new file mode 100644 index 0000000..99c76db --- /dev/null +++ b/packages/settings/deno.json @@ -0,0 +1,4 @@ +{ + "name": "@stackcore/manifests", + "exports": "./src/index.ts" +} diff --git a/packages/core/src/settings.ts b/packages/settings/src/index.ts similarity index 52% rename from packages/core/src/settings.ts rename to packages/settings/src/index.ts index eaa04b2..2ebc70c 100644 --- a/packages/core/src/settings.ts +++ b/packages/settings/src/index.ts @@ -1,12 +1,4 @@ -import { - BASIC_PRODUCT, - MONTHLY_BILLING_CYCLE, - PREMIUM_PRODUCT, - PRO_PRODUCT, - YEARLY_BILLING_CYCLE, -} from "./db/models/workspace.ts"; - -function getEnv(key: string, defaultValue: string) { +function getEnv(key: string, defaultValue: string): string { const value = Deno.env.get(key); if (value) return value; @@ -16,7 +8,88 @@ function getEnv(key: string, defaultValue: string) { throw new Error(`Environment variable ${key} is not set`); } -export default { +const settings: { + SECRET_KEY: string; + OTP: { + REQUEST_INTERVAL_SECONDS: number; + EXPIRY_MINUTES: number; + MAX_ATTEMPTS: number; + SKIP_OTP: boolean; + }; + JWT: { + EXPIRY_DAYS: number; + }; + INVITATION: { + EXPIRY_DAYS: number; + }; + MANIFEST: { + DEFAULT_VERSION: number; + }; + STRIPE: { + SECRET_KEY: string; + WEBHOOK_SECRET: string; + USE_MOCK: boolean; + METER: { + CREDIT_USAGE_METER_ID: string; + CREDIT_USAGE_EVENT_NAME: string; + CREDIT_USAGE_MANIFEST_CREATE: number; + }; + BILLING_THRESHOLD_BASIC: number; + PRODUCTS: { + BASIC: { + MONTHLY: { + PRICE_ID: string; + }; + }; + PRO: { + MONTHLY: { + PRICE_ID: string; + }; + YEARLY: { + PRICE_ID: string; + }; + }; + PREMIUM: { + MONTHLY: { + PRICE_ID: string; + }; + YEARLY: { + PRICE_ID: string; + }; + }; + }; + }; + PAGINATION: { + MAX_LIMIT: number; + }; + DATABASE: { + HOST: string; + PORT: number; + USER: string; + PASSWORD: string; + DATABASE: string; + }; + EMAIL: { + RESEND_API_KEY: string; + FROM_EMAIL: string; + USE_CONSOLE: boolean; + }; + SENTRY: { + DSN: string | undefined; + }; + GCP_BUCKET: { + USE_MOCK_GCP_STORAGE: boolean; + PROJECT_ID: string; + MANIFEST_BUCKET_NAME: string; + TEMPORARY_BUCKET_NAME: string; + SIGNED_URL_EXPIRY_SECONDS: number; + }; + LABELER: { + LABELER_API_KEY: string; + SKIP_CLOUD_TASK: boolean; + SERVICE_URL: string; + }; +} = { SECRET_KEY: getEnv("SECRET_KEY", "secret"), OTP: { REQUEST_INTERVAL_SECONDS: 10, @@ -48,36 +121,36 @@ export default { // This is to make sure that BASIC user that exceed their included credits have a card on file BILLING_THRESHOLD_BASIC: 6, PRODUCTS: { - [BASIC_PRODUCT]: { - [MONTHLY_BILLING_CYCLE]: { + BASIC: { + MONTHLY: { PRICE_ID: getEnv( "STRIPE_PRODUCT_BASIC_MONTHLY_PRICE_ID", "price_basic_monthly", ), }, }, - [PRO_PRODUCT]: { - [MONTHLY_BILLING_CYCLE]: { + PRO: { + MONTHLY: { PRICE_ID: getEnv( "STRIPE_PRODUCT_PRO_MONTHLY_PRICE_ID", "price_pro_monthly", ), }, - [YEARLY_BILLING_CYCLE]: { + YEARLY: { PRICE_ID: getEnv( "STRIPE_PRODUCT_PRO_YEARLY_PRICE_ID", "price_pro_yearly", ), }, }, - [PREMIUM_PRODUCT]: { - [MONTHLY_BILLING_CYCLE]: { + PREMIUM: { + MONTHLY: { PRICE_ID: getEnv( "STRIPE_PRODUCT_PREMIUM_MONTHLY_PRICE_ID", "price_premium_monthly", ), }, - [YEARLY_BILLING_CYCLE]: { + YEARLY: { PRICE_ID: getEnv( "STRIPE_PRODUCT_PREMIUM_YEARLY_PRICE_ID", "price_premium_yearly", @@ -105,9 +178,23 @@ export default { DSN: Deno.env.get("SENTRY_DSN"), }, GCP_BUCKET: { - USE_FAKE_GCS_SERVER: getEnv("GCP_USE_FAKE_GCS_SERVER", "false") === "true", + USE_MOCK_GCP_STORAGE: getEnv("USE_MOCK_GCP_STORAGE", "false") === "true", PROJECT_ID: getEnv("GCP_PROJECT_ID", "your-project-id"), - BUCKET_NAME: getEnv("GCP_BUCKET_NAME", "your-bucket-name"), + MANIFEST_BUCKET_NAME: getEnv( + "GCP_MANIFEST_BUCKET_NAME", + "your-bucket-name", + ), + TEMPORARY_BUCKET_NAME: getEnv( + "GCP_TEMPORARY_BUCKET_NAME", + "your-bucket-name", + ), SIGNED_URL_EXPIRY_SECONDS: 60 * 60, }, + LABELER: { + LABELER_API_KEY: getEnv("LABELER_API_KEY", "labeler_api_key"), + SKIP_CLOUD_TASK: getEnv("LABELER_SKIP_CLOUD_TASK", "false") === "true", + SERVICE_URL: getEnv("LABELER_SERVICE_URL", "http://localhost:4001"), + }, }; + +export default settings; diff --git a/packages/storage/deno.json b/packages/storage/deno.json new file mode 100644 index 0000000..03a94b7 --- /dev/null +++ b/packages/storage/deno.json @@ -0,0 +1,8 @@ +{ + "name": "@stackcore/storage", + "exports": "./src/index.ts", + "imports": { + "@stackcore/settings": "../settings/src/index.ts", + "@google-cloud/storage": "npm:@google-cloud/storage@^7.1.0" + } +} diff --git a/packages/storage/src/index.ts b/packages/storage/src/index.ts new file mode 100644 index 0000000..e4c274e --- /dev/null +++ b/packages/storage/src/index.ts @@ -0,0 +1,115 @@ +import { Storage } from "@google-cloud/storage"; +import settings from "@stackcore/settings"; +import { generateKeyPairSync } from "node:crypto"; + +async function getStorage() { + // local dev uses fake-gcs-server + if (settings.GCP_BUCKET.USE_MOCK_GCP_STORAGE) { + // generate a valid private key to generate a signed URL + const { privateKey } = generateKeyPairSync("rsa", { + modulusLength: 2048, + }); + const privateKeyPem = privateKey.export({ + type: "pkcs8", + format: "pem", + }).toString("base64"); + + const storage = new Storage({ + apiEndpoint: "http://localhost:4443", + projectId: "fake-project-id", + // needed to generate a signed URL + credentials: { + client_email: "fake@example.com", + private_key: privateKeyPem, + }, + }); + + const manifestBucket = storage.bucket( + settings.GCP_BUCKET.MANIFEST_BUCKET_NAME, + ); + const [manifestExists] = await manifestBucket.exists(); + if (!manifestExists) { + console.info("Bucket does not exist, creating..."); + await manifestBucket.create(); + console.info("Bucket created"); + } + + const temporaryBucket = storage.bucket( + settings.GCP_BUCKET.TEMPORARY_BUCKET_NAME, + ); + const [temporaryExists] = await temporaryBucket.exists(); + if (!temporaryExists) { + console.info("Bucket does not exist, creating..."); + await temporaryBucket.create(); + console.info("Bucket created"); + } + + return storage; + } + // production uses real GCP + return new Storage({ + projectId: settings.GCP_BUCKET.PROJECT_ID, + }); +} + +async function getManifestBucket() { + const storage = await getStorage(); + return storage.bucket(settings.GCP_BUCKET.MANIFEST_BUCKET_NAME); +} + +async function getTemporaryBucket() { + const storage = await getStorage(); + return storage.bucket(settings.GCP_BUCKET.TEMPORARY_BUCKET_NAME); +} + +export async function uploadManifestToBucket(json: object, fileName: string) { + const bucket = await getManifestBucket(); + const file = bucket.file(fileName); + const jsonString = JSON.stringify(json); + await file.save(jsonString, { + contentType: "application/json", + }); +} + +export async function downloadManifestFromBucket( + fileName: string, +): Promise { + const bucket = await getManifestBucket(); + const file = bucket.file(fileName); + const [content] = await file.download(); + const manifest = JSON.parse(content.toString()) as object; + return manifest; +} + +export async function getManifestPublicLink( + fileName: string, +): Promise { + const bucket = await getManifestBucket(); + + const file = bucket.file(fileName); + const [url] = await file.getSignedUrl({ + action: "read", + expires: Date.now() + + 1000 * settings.GCP_BUCKET.SIGNED_URL_EXPIRY_SECONDS, + }); + + return url; +} + +export async function uploadTemporaryFileToBucket( + fileName: string, + content: string, +) { + const bucket = await getTemporaryBucket(); + const file = bucket.file(fileName); + await file.save(content); +} + +export async function downloadTemporaryFileFromBucket( + fileName: string, +): Promise { + const bucket = await getTemporaryBucket(); + const file = bucket.file(fileName); + const [content] = await file.download(); + return content.toString(); +}