Skip to content

Commit c63e02f

Browse files
committed
feat: add sys_project schema and provisioning service for project management
- Introduced sys_project object schema for managing tenant projects with fields for ID, organization, slug, display name, project type, status, and database connection details. - Implemented project provisioning service to handle the creation of projects and their associated databases, including support for various database adapters (Turso, SQLite, in-memory). - Added project package installation protocol to manage package versions associated with projects, ensuring lifecycle management (install, upgrade, rollback). - Established request and response schemas for provisioning projects and organizations, including validation for project attributes and metadata.
1 parent 80bc2f4 commit c63e02f

46 files changed

Lines changed: 1362 additions & 1552 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/studio/src/components/app-sidebar.tsx

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -158,12 +158,12 @@ interface AppSidebarProps extends React.ComponentProps<typeof Sidebar> {
158158
packages: InstalledPackage[];
159159
selectedPackage: InstalledPackage | null;
160160
onSelectPackage: (pkg: InstalledPackage) => void;
161-
/** When set, all package-content URLs are rooted at /environments/:envId/:pkg/* */
162-
environmentId?: string;
161+
/** When set, all package-content URLs are rooted at /projects/:projectId/:pkg/* */
162+
projectId?: string;
163163
}
164164

165165
export function AppSidebar({
166-
packages, selectedPackage, onSelectPackage, environmentId,
166+
packages, selectedPackage, onSelectPackage, projectId,
167167
...props
168168
}: AppSidebarProps) {
169169
const client = useClient();
@@ -311,8 +311,8 @@ export function AppSidebar({
311311
isActive={!!params.package && !params.name && !params.type}
312312
onClick={() => {
313313
const pkgId = selectedPackage?.manifest?.id || 'default';
314-
if (environmentId) {
315-
navigate({ to: `/environments/${environmentId}/${pkgId}/` });
314+
if (projectId) {
315+
navigate({ to: `/projects/${projectId}/${pkgId}/` });
316316
} else {
317317
navigate({ to: `/${pkgId}` });
318318
}
@@ -412,11 +412,11 @@ export function AppSidebar({
412412

413413
const packagePath = selectedPackage?.manifest?.id || 'default';
414414
const handleClick = isObjectType
415-
? () => environmentId
416-
? navigate({ to: `/environments/${environmentId}/${packagePath}/objects/${itemName}` })
415+
? () => projectId
416+
? navigate({ to: `/projects/${projectId}/${packagePath}/objects/${itemName}` })
417417
: navigate({ to: `/${packagePath}/objects/${itemName}` })
418-
: () => environmentId
419-
? navigate({ to: `/environments/${environmentId}/${packagePath}/metadata/${type}/${itemName}` })
418+
: () => projectId
419+
? navigate({ to: `/projects/${projectId}/${packagePath}/metadata/${type}/${itemName}` })
420420
: navigate({ to: `/${packagePath}/metadata/${type}/${itemName}` });
421421

422422
return (
@@ -533,11 +533,11 @@ export function AppSidebar({
533533
tooltip="Packages"
534534
isActive={location.pathname.endsWith('/packages')}
535535
onClick={() => {
536-
const envId = params.environmentId as string | undefined;
536+
const projectId = params.projectId as string | undefined;
537537
if (envId) {
538-
navigate({ to: '/environments/$environmentId/packages', params: { environmentId: envId } });
538+
navigate({ to: '/projects/$projectId/packages', params: { projectId: envId } });
539539
} else {
540-
navigate({ to: '/environments' });
540+
navigate({ to: '/projects' });
541541
}
542542
}}
543543
>

apps/studio/src/components/global-sidebar.tsx

Lines changed: 23 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,17 @@
55
*
66
* Top-level navigation shell rendered on routes that are NOT scoped to a
77
* specific package — i.e. the home page, organization management, the
8-
* environments list, an environment's overview page, and the per-environment
8+
* projects list, a project's overview page, and the per-project
99
* packages management page.
1010
*
1111
* The sidebar deliberately exposes only two navigation entries:
1212
*
13-
* 1. **Environments** — links to `/environments` (browse / pick an env).
14-
* 2. **Packages** — links to `/environments/:envId/packages`. Disabled
15-
* until the user has selected an environment.
13+
* 1. **Projects** — links to `/projects` (browse / pick a project).
14+
* 2. **Packages** — links to `/projects/:projectId/packages`. Disabled
15+
* until the user has selected a project.
1616
*
1717
* Once the user drills into a specific package
18-
* (`/environments/:envId/:package/*`), the package-scoped {@link AppSidebar}
18+
* (`/projects/:projectId/:package/*`), the package-scoped {@link AppSidebar}
1919
* takes over instead. The two sidebars are mutually exclusive and share the
2020
* same `SidebarProvider` in `routes/__root.tsx`.
2121
*
@@ -47,14 +47,14 @@ import {
4747
import { useSession } from '@/hooks/useSession';
4848

4949
/**
50-
* Extract the `:envId` segment from the current pathname when the user is
51-
* anywhere under `/environments/:envId(...)`. Returns undefined on the
52-
* environments list page (`/environments`) or any non-environment route.
50+
* Extract the `:projectId` segment from the current pathname when the user is
51+
* anywhere under `/projects/:projectId(...)`. Returns undefined on the
52+
* projects list page (`/projects`) or any non-project route.
5353
*/
54-
function useActiveEnvironmentId(): string | undefined {
54+
function useActiveProjectId(): string | undefined {
5555
const location = useLocation();
5656
return useMemo(() => {
57-
const m = location.pathname.match(/^\/environments\/([^/]+)/);
57+
const m = location.pathname.match(/^\/projects\/([^/]+)/);
5858
return m?.[1];
5959
}, [location.pathname]);
6060
}
@@ -64,10 +64,10 @@ export function GlobalSidebar() {
6464
const pathname = location.pathname;
6565
const { session } = useSession();
6666
const activeOrgId = session?.activeOrganizationId ?? undefined;
67-
const envId = useActiveEnvironmentId();
67+
const projectId = useActiveProjectId();
6868

69-
const envsActive = pathname === '/environments';
70-
const packagesHref = envId ? `/environments/${envId}/packages` : undefined;
69+
const projectsActive = pathname === '/projects';
70+
const packagesHref = projectId ? `/projects/${projectId}/packages` : undefined;
7171
const packagesActive = !!packagesHref && pathname === packagesHref;
7272
const apiConsoleActive = pathname === '/api-console';
7373

@@ -77,28 +77,28 @@ export function GlobalSidebar() {
7777
<SidebarGroup>
7878
<SidebarGroupContent>
7979
<SidebarMenu>
80-
{/* Environments — single-row entry, no expansion. */}
80+
{/* Projects — single-row entry, no expansion. */}
8181
<SidebarMenuItem>
82-
<SidebarMenuButton asChild isActive={envsActive} tooltip="Environments">
83-
<Link to="/environments">
82+
<SidebarMenuButton asChild isActive={projectsActive} tooltip="Projects">
83+
<Link to="/projects">
8484
<Boxes className="size-4" />
85-
<span>Environments</span>
85+
<span>Projects</span>
8686
</Link>
8787
</SidebarMenuButton>
8888
</SidebarMenuItem>
8989

90-
{/* Packages — single-row entry. Depends on a selected environment;
90+
{/* Packages — single-row entry. Depends on a selected project;
9191
disabled and tooltipped when none is selected. */}
9292
<SidebarMenuItem>
93-
{envId ? (
93+
{projectId ? (
9494
<SidebarMenuButton
9595
asChild
9696
isActive={packagesActive}
9797
tooltip="Packages"
9898
>
9999
<Link
100-
to="/environments/$environmentId/packages"
101-
params={{ environmentId: envId }}
100+
to="/projects/$projectId/packages"
101+
params={{ projectId }}
102102
>
103103
<PackageIcon className="size-4" />
104104
<span>Packages</span>
@@ -108,7 +108,7 @@ export function GlobalSidebar() {
108108
<SidebarMenuButton
109109
disabled
110110
aria-disabled="true"
111-
tooltip="Select an environment first"
111+
tooltip="Select a project first"
112112
className="cursor-not-allowed opacity-50"
113113
>
114114
<PackageIcon className="size-4" />
@@ -118,7 +118,7 @@ export function GlobalSidebar() {
118118
</SidebarMenuItem>
119119

120120
{/* API Console — always available; the console discovers
121-
endpoints dynamically from the active client/environment. */}
121+
endpoints dynamically from the active client/project. */}
122122
<SidebarMenuItem>
123123
<SidebarMenuButton asChild isActive={apiConsoleActive} tooltip="API Console">
124124
<Link to="/api-console">

apps/studio/src/components/new-environment-dialog.tsx renamed to apps/studio/src/components/new-project-dialog.tsx

Lines changed: 29 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,20 @@
11
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
22

33
/**
4-
* NewEnvironmentDialog — provisions a new environment for the current
5-
* organization via `client.environments.create()`.
4+
* NewProjectDialog — provisions a new project for the current
5+
* organization via `client.projects.create()`.
66
*
7-
* Form fields mirror {@link ProvisionEnvironmentRequestSchema}:
8-
* slug, displayName, envType, region, plan. The `organizationId` and
7+
* Form fields mirror {@link ProvisionProjectRequestSchema}:
8+
* slug, displayName, projectType, region, plan. The `organizationId` and
99
* `createdBy` fields are injected by the backend from the session.
1010
*
1111
* On success, the dialog invokes `onCreated(env)` and closes; the parent
12-
* (EnvironmentSwitcher) is responsible for reloading the environment list
13-
* and navigating into the new environment.
12+
* (ProjectSwitcher) is responsible for reloading the project list
13+
* and navigating into the new project.
1414
*/
1515

1616
import { useEffect, useState } from 'react';
17-
import type { Environment, EnvironmentType } from '@objectstack/spec/cloud';
17+
import type { Project, ProjectType } from '@objectstack/spec/cloud';
1818
import {
1919
Dialog,
2020
DialogContent,
@@ -33,11 +33,11 @@ import {
3333
SelectTrigger,
3434
SelectValue,
3535
} from '@/components/ui/select';
36-
import { useDrivers, useProvisionEnvironment } from '@/hooks/useEnvironments';
36+
import { useDrivers, useProvisionProject } from '@/hooks/useProjects';
3737
import { toast } from '@/hooks/use-toast';
3838
import { useActiveOrganizationId, useSession } from '@/hooks/useSession';
3939

40-
const ENV_TYPES: { value: EnvironmentType; label: string; hint: string }[] = [
40+
const PROJECT_TYPES: { value: ProjectType; label: string; hint: string }[] = [
4141
{ value: 'development', label: 'Development', hint: 'For makers building and iterating' },
4242
{ value: 'test', label: 'Test', hint: 'Automated test runs, throwaway data' },
4343
{ value: 'sandbox', label: 'Sandbox', hint: 'Isolated clone of production' },
@@ -47,24 +47,24 @@ const ENV_TYPES: { value: EnvironmentType; label: string; hint: string }[] = [
4747
{ value: 'trial', label: 'Trial', hint: 'Time-boxed demo workspace' },
4848
];
4949

50-
export interface NewEnvironmentDialogProps {
50+
export interface NewProjectDialogProps {
5151
open: boolean;
5252
onOpenChange: (open: boolean) => void;
53-
onCreated?: (env: Environment) => void;
53+
onCreated?: (env: Project) => void;
5454
}
5555

56-
export function NewEnvironmentDialog({
56+
export function NewProjectDialog({
5757
open,
5858
onOpenChange,
5959
onCreated,
60-
}: NewEnvironmentDialogProps) {
61-
const { provision, provisioning } = useProvisionEnvironment();
60+
}: NewProjectDialogProps) {
61+
const { provision, provisioning } = useProvisionProject();
6262
const { drivers, loading: driversLoading } = useDrivers();
6363
const activeOrgId = useActiveOrganizationId();
6464
const { user } = useSession();
6565
const [slug, setSlug] = useState('');
6666
const [displayName, setDisplayName] = useState('');
67-
const [envType, setEnvType] = useState<EnvironmentType>('development');
67+
const [projectType, setProjectType] = useState<ProjectType>('development');
6868
const [region, setRegion] = useState('');
6969
const [driver, setDriver] = useState<string>('');
7070

@@ -82,7 +82,7 @@ export function NewEnvironmentDialog({
8282
const reset = () => {
8383
setSlug('');
8484
setDisplayName('');
85-
setEnvType('development');
85+
setProjectType('development');
8686
setRegion('');
8787
setDriver('');
8888
};
@@ -104,18 +104,18 @@ export function NewEnvironmentDialog({
104104
createdBy: user?.id ?? '__session__',
105105
slug: slug.trim(),
106106
displayName: displayName.trim() || undefined,
107-
envType,
107+
projectType,
108108
region: region.trim() || undefined,
109109
driver: driver || undefined,
110110
} as any);
111-
const env = (res?.environment ?? res) as Environment;
111+
const project = (res?.project ?? res) as Project;
112112
toast({
113-
title: 'Environment provisioned',
114-
description: `${env.displayName} (${env.slug}) is ready.`,
113+
title: 'Project provisioned',
114+
description: `${project.displayName} (${project.slug}) is ready.`,
115115
});
116116
reset();
117117
onOpenChange(false);
118-
onCreated?.(env);
118+
onCreated?.(project);
119119
} catch (err) {
120120
toast({
121121
title: 'Provisioning failed',
@@ -130,10 +130,10 @@ export function NewEnvironmentDialog({
130130
<DialogContent className="sm:max-w-md">
131131
<form onSubmit={handleSubmit}>
132132
<DialogHeader>
133-
<DialogTitle>New environment</DialogTitle>
133+
<DialogTitle>New project</DialogTitle>
134134
<DialogDescription>
135-
Provisions a physically isolated database for this environment.
136-
Data in different environments is never shared.
135+
Provisions a physically isolated database for this project.
136+
Data in different projects is never shared.
137137
</DialogDescription>
138138
</DialogHeader>
139139

@@ -168,14 +168,14 @@ export function NewEnvironmentDialog({
168168
<div className="grid gap-1.5">
169169
<Label>Type</Label>
170170
<Select
171-
value={envType}
172-
onValueChange={(v) => setEnvType(v as EnvironmentType)}
171+
value={projectType}
172+
onValueChange={(v) => setProjectType(v as ProjectType)}
173173
>
174174
<SelectTrigger>
175175
<SelectValue />
176176
</SelectTrigger>
177177
<SelectContent>
178-
{ENV_TYPES.map((t) => (
178+
{PROJECT_TYPES.map((t) => (
179179
<SelectItem key={t.value} value={t.value}>
180180
<div className="flex flex-col">
181181
<span>{t.label}</span>
@@ -221,7 +221,7 @@ export function NewEnvironmentDialog({
221221
</SelectContent>
222222
</Select>
223223
<p className="text-[11px] text-muted-foreground">
224-
Where this environment's data will be stored. `memory` is ideal
224+
Where this project's data will be stored. `memory` is ideal
225225
for tests; `turso` persists to libSQL.
226226
</p>
227227
</div>
@@ -247,7 +247,7 @@ export function NewEnvironmentDialog({
247247
Cancel
248248
</Button>
249249
<Button type="submit" disabled={provisioning || !slug.trim()}>
250-
{provisioning ? 'Provisioning…' : 'Create environment'}
250+
{provisioning ? 'Provisioning…' : 'Create project'}
251251
</Button>
252252
</DialogFooter>
253253
</form>

apps/studio/src/components/organization-switcher.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
/**
44
* OrganizationSwitcher
55
*
6-
* Sits immediately left of the EnvironmentSwitcher in the site header.
6+
* Sits immediately left of the ProjectSwitcher in the site header.
77
* Reads org list + active org from `useOrganizations()` / `useSession()`.
88
* Selecting an org calls `organizations.setActive()` and refreshes the
99
* session (so the rest of the app picks up the new `activeOrganizationId`).

0 commit comments

Comments
 (0)