diff --git a/ee/apps/den-api/src/organization-limits.ts b/ee/apps/den-api/src/organization-limits.ts new file mode 100644 index 000000000..017535576 --- /dev/null +++ b/ee/apps/den-api/src/organization-limits.ts @@ -0,0 +1,143 @@ +import { eq, sql } from "@openwork-ee/den-db/drizzle" +import { MemberTable, OrganizationTable, WorkerTable } from "@openwork-ee/den-db/schema" +import { db } from "./db.js" + +export const DEFAULT_ORGANIZATION_LIMITS = { + members: 5, + workers: 1, +} as const + +export type OrganizationLimitType = keyof typeof DEFAULT_ORGANIZATION_LIMITS + +export type OrganizationLimits = { + members: number + workers: number +} + +type OrganizationId = typeof OrganizationTable.$inferSelect.id + +export type OrganizationMetadata = { + limits: OrganizationLimits +} & Record + +type OrganizationMetadataInput = Record | string | null | undefined + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null +} + +function normalizePositiveInteger(value: unknown, fallback: number) { + if (typeof value === "number" && Number.isInteger(value) && value > 0) { + return value + } + + if (typeof value === "string") { + const parsed = Number(value) + if (Number.isInteger(parsed) && parsed > 0) { + return parsed + } + } + + return fallback +} + +function parseMetadata(input: OrganizationMetadataInput): Record { + if (!input) { + return {} + } + + if (typeof input === "string") { + try { + const parsed = JSON.parse(input) as unknown + return isRecord(parsed) ? parsed : {} + } catch { + return {} + } + } + + return isRecord(input) ? input : {} +} + +export function normalizeOrganizationMetadata(input: OrganizationMetadataInput): { + metadata: OrganizationMetadata + changed: boolean +} { + const parsed = parseMetadata(input) + const rawLimits = isRecord(parsed.limits) ? parsed.limits : null + const members = normalizePositiveInteger(rawLimits?.members, DEFAULT_ORGANIZATION_LIMITS.members) + const workers = normalizePositiveInteger(rawLimits?.workers ?? rawLimits?.Workers, DEFAULT_ORGANIZATION_LIMITS.workers) + + const metadata: OrganizationMetadata = { + ...parsed, + limits: { + members, + workers, + }, + } as OrganizationMetadata + + const changed = + !isRecord(parsed.limits) || + Object.keys(parsed).length === 0 || + rawLimits?.members !== members || + (rawLimits?.workers ?? rawLimits?.Workers) !== workers + + return { metadata, changed } +} + +export function serializeOrganizationMetadata(metadata: OrganizationMetadataInput) { + const parsed = parseMetadata(metadata) + return Object.keys(parsed).length > 0 ? JSON.stringify(parsed) : null +} + +export async function getOrInitializeOrganizationMetadata(organizationId: OrganizationId) { + const rows = await db + .select({ metadata: OrganizationTable.metadata }) + .from(OrganizationTable) + .where(eq(OrganizationTable.id, organizationId)) + .limit(1) + + const { metadata, changed } = normalizeOrganizationMetadata(rows[0]?.metadata) + if (changed) { + await db + .update(OrganizationTable) + .set({ metadata }) + .where(eq(OrganizationTable.id, organizationId)) + } + + return metadata +} + +async function countOrganizationMembers(organizationId: OrganizationId) { + const rows = await db + .select({ count: sql`count(*)` }) + .from(MemberTable) + .where(eq(MemberTable.organizationId, organizationId)) + + return Number(rows[0]?.count ?? 0) +} + +async function countOrganizationWorkers(organizationId: OrganizationId) { + const rows = await db + .select({ count: sql`count(*)` }) + .from(WorkerTable) + .where(eq(WorkerTable.org_id, organizationId)) + + return Number(rows[0]?.count ?? 0) +} + +export async function getOrganizationLimitStatus(organizationId: OrganizationId, limitType: OrganizationLimitType) { + const metadata = await getOrInitializeOrganizationMetadata(organizationId) + const currentCount = + limitType === "members" + ? await countOrganizationMembers(organizationId) + : await countOrganizationWorkers(organizationId) + + const limit = metadata.limits[limitType] + + return { + metadata, + currentCount, + limit, + exceeded: currentCount >= limit, + } +} diff --git a/ee/apps/den-api/src/orgs.ts b/ee/apps/den-api/src/orgs.ts index e77bac3a3..98173b76d 100644 --- a/ee/apps/den-api/src/orgs.ts +++ b/ee/apps/den-api/src/orgs.ts @@ -11,6 +11,7 @@ import { } from "@openwork-ee/den-db/schema" import { createDenTypeId, normalizeDenTypeId } from "@openwork-ee/utils/typeid" import { db } from "./db.js" +import { DEFAULT_ORGANIZATION_LIMITS, serializeOrganizationMetadata } from "./organization-limits.js" import { denDefaultDynamicOrganizationRoles, denOrganizationStaticRoles } from "./organization-access.js" type UserId = typeof AuthUserTable.$inferSelect.id @@ -401,16 +402,23 @@ async function createOrganizationRecord(input: { userId: UserId name: string logo?: string | null - metadata?: string | null + metadata?: Record | null }) { const organizationId = createDenTypeId("organization") + const metadata = + input.metadata ?? { + limits: { + members: DEFAULT_ORGANIZATION_LIMITS.members, + workers: DEFAULT_ORGANIZATION_LIMITS.workers, + }, + } await db.insert(OrganizationTable).values({ id: organizationId, name: input.name, slug: organizationId, logo: input.logo ?? null, - metadata: input.metadata ?? null, + metadata, }) await db.insert(MemberTable).values({ @@ -511,7 +519,7 @@ export async function listUserOrgs(userId: UserId) { name: row.organization.name, slug: row.organization.slug, logo: row.organization.logo, - metadata: row.organization.metadata, + metadata: serializeOrganizationMetadata(row.organization.metadata), role: row.role, orgMemberId: row.membershipId, membershipId: row.membershipId, @@ -524,7 +532,7 @@ export async function resolveUserOrganizations(input: { activeOrganizationId?: string | null userId: UserId }) { - await ensurePersonalOrganizationForUser(input.userId) + await ensureUserOrgAccess({ userId: input.userId }) const orgs = await listUserOrgs(input.userId) @@ -628,7 +636,7 @@ export async function getOrganizationContextForUser(input: { name: organization.name, slug: organization.slug, logo: organization.logo, - metadata: organization.metadata, + metadata: serializeOrganizationMetadata(organization.metadata), createdAt: organization.createdAt, updatedAt: organization.updatedAt, }, diff --git a/ee/apps/den-api/src/routes/org/core.ts b/ee/apps/den-api/src/routes/org/core.ts index 469f9c77c..7dc3df688 100644 --- a/ee/apps/den-api/src/routes/org/core.ts +++ b/ee/apps/den-api/src/routes/org/core.ts @@ -3,7 +3,9 @@ import { OrganizationTable } from "@openwork-ee/den-db/schema" import { normalizeDenTypeId } from "@openwork-ee/utils/typeid" import type { Hono } from "hono" import { z } from "zod" +import { requireCloudWorkerAccess } from "../../billing/polar.js" import { db } from "../../db.js" +import { env } from "../../env.js" import { jsonValidator, paramValidator, queryValidator, requireUserMiddleware, resolveMemberTeamsMiddleware, resolveOrganizationContextMiddleware } from "../../middleware/index.js" import { acceptInvitationForUser, createOrganizationForUser, getInvitationPreview, setSessionActiveOrganization } from "../../orgs.js" import { getRequiredUserEmail } from "../../user.js" @@ -27,6 +29,29 @@ export function registerOrgCoreRoutes 0) { - const email = getRequiredUserEmail(user) - if (!email) { - return c.json({ error: "user_email_required" }, 400) - } - - const access = await requireCloudAccessOrPayment({ - userId: user.id, - email, - name: user.name ?? user.email ?? "OpenWork User", - }) - if (!access.allowed) { + if (input.destination === "cloud") { + const workerLimit = await getOrganizationLimitStatus(orgId, "workers") + if (workerLimit.exceeded) { return c.json({ - error: "payment_required", - message: "Additional cloud workers require an active Den Cloud plan.", - polar: { - checkoutUrl: access.checkoutUrl, - productId: env.polar.productId, - benefitId: env.polar.benefitId, - }, - }, 402) + error: "org_limit_reached", + limitType: "workers", + limit: workerLimit.limit, + currentCount: workerLimit.currentCount, + message: `This workspace currently supports up to ${workerLimit.limit} workers. Contact support to increase the limit.`, + }, 409) } } diff --git a/ee/apps/den-web/app/(den)/_components/checkout-screen.tsx b/ee/apps/den-web/app/(den)/_components/checkout-screen.tsx index 2fc483bee..da96b1a9d 100644 --- a/ee/apps/den-web/app/(den)/_components/checkout-screen.tsx +++ b/ee/apps/den-web/app/(den)/_components/checkout-screen.tsx @@ -125,7 +125,18 @@ export function CheckoutScreen({ customerSessionToken }: { customerSessionToken: ]); useEffect(() => { - if (!sessionHydrated || !user || resuming || onboardingPending || mockMode || redirectingRef.current) { + if ( + !sessionHydrated || + !user || + resuming || + onboardingPending || + mockMode || + redirectingRef.current || + billingBusy || + billingCheckoutBusy || + !billingSummary || + (billingSummary.featureGateEnabled && !billingSummary.hasActivePlan) + ) { return; } @@ -143,7 +154,19 @@ export function CheckoutScreen({ customerSessionToken }: { customerSessionToken: .finally(() => { redirectingRef.current = false; }); - }, [mockMode, onboardingPending, pathname, resolveUserLandingRoute, resuming, router, sessionHydrated, user]); + }, [ + billingBusy, + billingCheckoutBusy, + billingSummary, + mockMode, + onboardingPending, + pathname, + resolveUserLandingRoute, + resuming, + router, + sessionHydrated, + user, + ]); if (!sessionHydrated || (!user && !mockMode)) { return ( @@ -174,16 +197,16 @@ export function CheckoutScreen({ customerSessionToken }: { customerSessionToken:

OpenWork Cloud

-

Purchase worker access before launch.

+

Purchase a plan before creating your workspace.

- Workers are disabled by default. Add one hosted OpenWork worker for $50/month, then launch it from your dashboard. + Start with one workspace plan for $50/month. Each plan includes up to 5 members and 1 hosted worker.

{checkoutHref ? ( - Purchase worker — $50/month + Purchase plan — $50/month ) : ( - + {createError ?

{createError}

: null} + +
+ +

Signed in as {user?.email ?? "your account"}

+
+ +
+ ) : ( +
+
+

Settings

+

Manage your profile and organization memberships.

+
- {error ? ( -
- {error} +
+ +
- ) : null} - {activeTab === "profile" ? ( -
+ {error ? ( +
+ {error} +
+ ) : null} + + {activeTab === "profile" ? ( +
{userInitials} @@ -223,130 +286,119 @@ export function OrganizationScreen() { />
-
- ) : ( - <> -
-

- Organizations are independent environments. In each organization you can collaborate with other members and manage your own resources. -

- -
- - {showCreate ? ( -
-

Create an Organization

-
- - -
- - -
- - {createError && ( -

{createError}

- )} -
+ ) : ( + <> +
+

+ Organizations are independent environments. In each organization you can collaborate with other members and manage your own resources. +

+
- ) : null} -
-
- - - - - - - - - - {orgs.length === 0 && !busy ? ( + {showCreate ? ( +
+

Create an Organization

+
+ + +
+ + +
+ + {createError ?

{createError}

: null} + +
+ ) : null} + +
+
+
OrganizationSeat TypeAction
+ - + + + - ) : null} - {orgs.map((org) => ( - - - - + + {orgs.map((org) => ( + + + + - - ))} - -
- No organizations found. Create one to get started. - OrganizationSeat TypeAction
-
{org.name}
-
- {org.role === "owner" ? "Creator plan" : "Free plan"} • 1 member -
-
- {formatRoleLabel(org.role)} - - {org.isActive ? ( - - Current Organization - - ) : ( +
+
{org.name}
+
+ {org.role === "owner" ? "Creator plan" : "Free plan"} • 1 member +
+
+ {formatRoleLabel(org.role)} + + {org.isActive ? ( + + Current Organization + + ) : ( + + )} - )} - -
+ + + ))} + + +
-
-

- You have no pending organization invites. -

- - )} -
+

You have no pending organization invites.

+ + )} +
+ )}
); diff --git a/ee/apps/den-web/app/(den)/_lib/den-flow.ts b/ee/apps/den-web/app/(den)/_lib/den-flow.ts index 8a98f2406..fb8ec7220 100644 --- a/ee/apps/den-web/app/(den)/_lib/den-flow.ts +++ b/ee/apps/den-web/app/(den)/_lib/den-flow.ts @@ -51,6 +51,14 @@ export type BillingSummary = { benefitId: string | null; }; +export type OrgLimitError = { + error: "org_limit_reached"; + message: string; + limitType: "members" | "workers"; + currentCount: number; + limit: number; +}; + export type AuthUser = { id: string; email: string; @@ -364,6 +372,29 @@ export function getErrorMessage(payload: unknown, fallback: string): string { return fallback; } +export function getOrgLimitError(payload: unknown): OrgLimitError | null { + if (!isRecord(payload) || payload.error !== "org_limit_reached") { + return null; + } + + if ( + (payload.limitType !== "members" && payload.limitType !== "workers") || + typeof payload.message !== "string" || + typeof payload.currentCount !== "number" || + typeof payload.limit !== "number" + ) { + return null; + } + + return { + error: "org_limit_reached", + message: payload.message, + limitType: payload.limitType, + currentCount: payload.currentCount, + limit: payload.limit, + }; +} + export function getUser(payload: unknown): AuthUser | null { if (!isRecord(payload) || !isRecord(payload.user)) { return null; diff --git a/ee/apps/den-web/app/(den)/_lib/feedback.ts b/ee/apps/den-web/app/(den)/_lib/feedback.ts new file mode 100644 index 000000000..6b16cbc87 --- /dev/null +++ b/ee/apps/den-web/app/(den)/_lib/feedback.ts @@ -0,0 +1,22 @@ +export const OPENWORK_FEEDBACK_URL = "https://openworklabs.com/feedback"; + +export function buildDenFeedbackUrl(options?: { + pathname?: string; + orgSlug?: string | null; + topic?: string; +}) { + const url = new URL(OPENWORK_FEEDBACK_URL); + url.searchParams.set("source", "openwork-web-app"); + url.searchParams.set("deployment", "web"); + url.searchParams.set("entrypoint", options?.pathname ?? "dashboard"); + + if (options?.orgSlug) { + url.searchParams.set("org", options.orgSlug); + } + + if (options?.topic) { + url.searchParams.set("topic", options.topic); + } + + return url.toString(); +} diff --git a/ee/apps/den-web/app/(den)/_providers/den-flow-provider.tsx b/ee/apps/den-web/app/(den)/_providers/den-flow-provider.tsx index 9ca62a29e..f19e202cd 100644 --- a/ee/apps/den-web/app/(den)/_providers/den-flow-provider.tsx +++ b/ee/apps/den-web/app/(den)/_providers/den-flow-provider.tsx @@ -16,6 +16,7 @@ import { type BillingSummary, type LaunchEvent, type OnboardingIntent, + type OrgLimitError, type RuntimeServiceName, type SocialAuthProvider, type WorkerLaunch, @@ -31,6 +32,7 @@ import { getCheckoutUrl, getEmailDomain, getErrorMessage, + getOrgLimitError, getRuntimeServiceLabel, getSocialCallbackUrl, getSocialProviderLabel, @@ -60,7 +62,7 @@ import { parseOrgListPayload, } from "../_lib/den-org"; -type LaunchWorkerResult = "success" | "checkout" | "error"; +type LaunchWorkerResult = "success" | "checkout" | "limit" | "error"; type AuthNavigationResult = "dashboard" | "checkout" | "join-org" | null; type DenFlowContextValue = { @@ -96,6 +98,8 @@ type DenFlowContextValue = { billingSubscriptionBusy: boolean; billingError: string | null; effectiveCheckoutUrl: string | null; + orgLimitError: OrgLimitError | null; + clearOrgLimitError: () => void; refreshBilling: (options?: { includeCheckout?: boolean; quiet?: boolean }) => Promise; handleSubscriptionCancellation: (cancelAtPeriodEnd: boolean) => Promise; refreshCheckoutReturn: (sessionTokenPresent: boolean) => Promise; @@ -150,23 +154,6 @@ type DenFlowContextValue = { const DenFlowContext = createContext(null); -function readLocalStorage(key: string): T | null { - if (typeof window === "undefined") { - return null; - } - - const raw = window.localStorage.getItem(key); - if (!raw) { - return null; - } - - try { - return JSON.parse(raw) as T; - } catch { - return null; - } -} - function getPendingOrgInvitationId() { if (typeof window === "undefined") { return null; @@ -211,6 +198,7 @@ export function DenFlowProvider({ children }: { children: ReactNode }) { const [billingSubscriptionBusy, setBillingSubscriptionBusy] = useState(false); const [billingError, setBillingError] = useState(null); const [billingLoadedOnce, setBillingLoadedOnce] = useState(false); + const [orgLimitError, setOrgLimitError] = useState(null); const [workerName, setWorkerName] = useState(DEFAULT_WORKER_NAME); const [worker, setWorker] = useState(null); @@ -237,7 +225,7 @@ export function DenFlowProvider({ children }: { children: ReactNode }) { const [runtimeError, setRuntimeError] = useState(null); const [runtimeUpgradeBusy, setRuntimeUpgradeBusy] = useState(false); - const [onboardingIntent, setOnboardingIntent] = useState(() => readLocalStorage(ONBOARDING_INTENT_STORAGE_KEY)); + const [onboardingIntent, setOnboardingIntent] = useState(null); const onboardingAutoLaunchKeyRef = useRef(null); const socialSignupHandledRef = useRef(null); const pendingWorkersRequestRef = useRef | null>(null); @@ -1039,27 +1027,13 @@ export function DenFlowProvider({ children }: { children: ReactNode }) { } } - async function beginSignupOnboarding(authenticatedUser: AuthUser, authMethod: AuthMethod) { + async function beginSignupOnboarding(authenticatedUser: AuthUser, _authMethod: AuthMethod) { const autoName = deriveOnboardingWorkerName(authenticatedUser); setWorkerName(autoName); setLaunchError(null); - setLaunchStatus("Preparing your first worker."); - - const intent: OnboardingIntent = { - version: 1, - workerName: autoName, - shouldLaunch: true, - completed: false, - authMethod - }; - - persistOnboardingIntent(intent); - const summary = await refreshBilling({ includeCheckout: true, quiet: true }); - if (!summary) { - return "checkout" as const; - } - - return !summary.featureGateEnabled || summary.hasActivePlan ? ("dashboard" as const) : ("checkout" as const); + setLaunchStatus("Create a workspace to get started."); + persistOnboardingIntent(null); + return "dashboard" as const; } async function resolveUserLandingRoute() { @@ -1074,19 +1048,11 @@ export function DenFlowProvider({ children }: { children: ReactNode }) { const dashboardRoute = await resolveDashboardRoute(); - if (!onboardingPending) { + if (dashboardRoute) { return dashboardRoute; } - const summary = - billingSummary ?? - (billingBusy || billingCheckoutBusy ? null : await refreshBilling({ includeCheckout: true, quiet: true })); - - if (!summary) { - return "/checkout"; - } - - return !summary.featureGateEnabled || summary.hasActivePlan ? (dashboardRoute ?? "/") : "/checkout"; + return "/organization"; } async function submitAuth(event: FormEvent) { @@ -1254,6 +1220,7 @@ export function DenFlowProvider({ children }: { children: ReactNode }) { setCheckoutUrl(null); setBillingSummary(null); setBillingError(null); + setOrgLimitError(null); setBillingBusy(false); setBillingCheckoutBusy(false); setBillingSubscriptionBusy(false); @@ -1298,6 +1265,7 @@ export function DenFlowProvider({ children }: { children: ReactNode }) { setLaunchBusy(true); setLaunchError(null); + setOrgLimitError(null); setCheckoutUrl(null); setLaunchStatus(options.source === "signup_auto" ? "Creating your first worker..." : "Checking worker billing and launch eligibility..."); appendEvent("info", "Launch requested", resolvedLaunchName); @@ -1316,6 +1284,15 @@ export function DenFlowProvider({ children }: { children: ReactNode }) { 12000 ); + const limitError = getOrgLimitError(payload); + if (limitError) { + setOrgLimitError(limitError); + setLaunchStatus(limitError.message); + setLaunchError(limitError.message); + appendEvent("warning", "Workspace limit reached", limitError.message); + return "limit" as const; + } + if (response.status === 402) { const url = getCheckoutUrl(payload); setCheckoutUrl(url); @@ -1748,7 +1725,7 @@ export function DenFlowProvider({ children }: { children: ReactNode }) { } if (!summary.featureGateEnabled || summary.hasActivePlan) { - return (await resolveDashboardRoute()) ?? "/"; + return (await resolveUserLandingRoute()) ?? "/organization"; } return "/checkout" as const; @@ -2078,6 +2055,8 @@ export function DenFlowProvider({ children }: { children: ReactNode }) { billingSubscriptionBusy, billingError, effectiveCheckoutUrl, + orgLimitError, + clearOrgLimitError: () => setOrgLimitError(null), refreshBilling, handleSubscriptionCancellation, refreshCheckoutReturn, diff --git a/ee/apps/den-web/app/(den)/o/[orgSlug]/dashboard/_components/background-agents-screen.tsx b/ee/apps/den-web/app/(den)/o/[orgSlug]/dashboard/_components/background-agents-screen.tsx index 86aae5943..ce30ab7d3 100644 --- a/ee/apps/den-web/app/(den)/o/[orgSlug]/dashboard/_components/background-agents-screen.tsx +++ b/ee/apps/den-web/app/(den)/o/[orgSlug]/dashboard/_components/background-agents-screen.tsx @@ -21,6 +21,7 @@ import { import { DenInput } from "../../../../_components/ui/input"; import { DashboardPageTemplate } from "../../../../_components/ui/dashboard-page-template"; import { DenButton, buttonVariants } from "../../../../_components/ui/button"; +import { OrgLimitDialog } from "../../../../_components/org-limit-dialog"; import { OPENWORK_APP_CONNECT_BASE_URL, buildOpenworkAppConnectUrl, @@ -31,6 +32,7 @@ import { requestJson, type WorkerListItem, } from "../../../../_lib/den-flow"; +import { buildDenFeedbackUrl } from "../../../../_lib/feedback"; import { useDenFlow } from "../../../../_providers/den-flow-provider"; import { getSharedSetupsRoute } from "../../../../_lib/den-org"; import { useOrgDashboard } from "../_providers/org-dashboard-provider"; @@ -309,9 +311,16 @@ export function BackgroundAgentsScreen() { workersError, launchBusy, launchWorker, + orgLimitError, + clearOrgLimitError, renameWorker, renameBusyWorkerId, } = useDenFlow(); + const feedbackHref = buildDenFeedbackUrl({ + pathname: `/o/${orgSlug}/dashboard/background-agents`, + orgSlug, + topic: "workspace-limits", + }); async function handleAddWorkspace() { const result = await launchWorker({ source: "manual" }); @@ -403,6 +412,19 @@ export function BackgroundAgentsScreen() { description="Keep selected workflows running in the background without asking each teammate to run them locally." colors={["#E9FFE0", "#3E9A1D", "#B3F750", "#51F0A3"]} > + +
-

- {billingSummary?.hasActivePlan - ? `This workspace's plan is currently ${statusLabel.toLowerCase()} and renews on ${nextBillingDate}.` - : "Workers are $50/month each. Purchase a worker to enable hosted launches for your team."} -

+

+ {billingSummary?.hasActivePlan + ? `This workspace's plan is currently ${statusLabel.toLowerCase()} and renews on ${nextBillingDate}.` + : "Workspace plans are $50/month and include up to 5 members plus 1 hosted worker."} +

@@ -144,7 +144,7 @@ export function BillingDashboardScreen() {
{effectiveCheckoutUrl && !billingSummary?.hasActivePlan ? ( - Purchase worker + Purchase plan ) : null} @@ -175,9 +175,9 @@ export function BillingDashboardScreen() {

Free forever · open source

-

Cloud worker

+

Workspace plan

$50/month

-

Per worker · 5 seats included

+

5 members included · 1 hosted worker

Enterprise

diff --git a/ee/apps/den-web/app/(den)/o/[orgSlug]/dashboard/_components/manage-members-screen.tsx b/ee/apps/den-web/app/(den)/o/[orgSlug]/dashboard/_components/manage-members-screen.tsx index 2ca815542..bccac4022 100644 --- a/ee/apps/den-web/app/(den)/o/[orgSlug]/dashboard/_components/manage-members-screen.tsx +++ b/ee/apps/den-web/app/(den)/o/[orgSlug]/dashboard/_components/manage-members-screen.tsx @@ -18,8 +18,12 @@ import { DEN_ROLE_PERMISSION_OPTIONS, formatRoleLabel, getOrgAccessFlags, + getMembersRoute, splitRoleString, } from "../../../../_lib/den-org"; +import { type OrgLimitError, getOrgLimitError } from "../../../../_lib/den-flow"; +import { buildDenFeedbackUrl } from "../../../../_lib/feedback"; +import { OrgLimitDialog } from "../../../../_components/org-limit-dialog"; import { useOrgDashboard } from "../_providers/org-dashboard-provider"; import { UnderlineTabs } from "../../../../_components/ui/tabs"; import { DashboardPageTemplate } from "../../../../_components/ui/dashboard-page-template"; @@ -142,6 +146,7 @@ export function ManageMembersScreen() { const [rolePermissionDraft, setRolePermissionDraft] = useState< Record >({}); + const [limitDialogError, setLimitDialogError] = useState(null); const assignableRoles = useMemo( () => (orgContext?.roles ?? []).filter((role) => !role.protected), @@ -189,6 +194,16 @@ export function ManageMembersScreen() { ); }, [orgContext?.members, orgContext?.teams]); + const feedbackHref = useMemo( + () => + buildDenFeedbackUrl({ + pathname: activeOrg ? getMembersRoute(activeOrg.slug) : "/organization", + orgSlug: activeOrg?.slug ?? null, + topic: "workspace-limits", + }), + [activeOrg], + ); + function resetInviteForm() { setInviteEmail(""); setInviteRole(assignableRoles[0]?.role ?? "member"); @@ -263,6 +278,12 @@ export function ManageMembersScreen() { await inviteMember({ email: inviteEmail, role: inviteRole }); resetInviteForm(); } catch (error) { + const limitError = getOrgLimitError(error); + if (limitError) { + setLimitDialogError(limitError); + return; + } + setPageError( error instanceof Error ? error.message @@ -601,6 +622,19 @@ export function ManageMembersScreen() { description="Invite teammates, adjust roles, and keep access clean." colors={["#F3EEFF", "#4A1D96", "#7C3AED", "#C4B5FD"]} > + setLimitDialogError(null)} + /> + {pageError ? (
{pageError} diff --git a/ee/apps/den-web/app/(den)/o/[orgSlug]/dashboard/_components/shared-setup-data.ts b/ee/apps/den-web/app/(den)/o/[orgSlug]/dashboard/_components/shared-setup-data.ts index e8f35236a..4e3a22fa7 100644 --- a/ee/apps/den-web/app/(den)/o/[orgSlug]/dashboard/_components/shared-setup-data.ts +++ b/ee/apps/den-web/app/(den)/o/[orgSlug]/dashboard/_components/shared-setup-data.ts @@ -2,25 +2,10 @@ import { useEffect, useState } from "react"; import { getErrorMessage, requestJson } from "../../../../_lib/den-flow"; +import { OPENWORK_FEEDBACK_URL, buildDenFeedbackUrl } from "../../../../_lib/feedback"; export const OPENWORK_DOCS_URL = "https://openworklabs.com/docs"; -export const OPENWORK_FEEDBACK_URL = "https://openworklabs.com/feedback"; - -export function buildDenFeedbackUrl(options?: { - pathname?: string; - orgSlug?: string; -}) { - const url = new URL(OPENWORK_FEEDBACK_URL); - url.searchParams.set("source", "openwork-web-app"); - url.searchParams.set("deployment", "web"); - url.searchParams.set("entrypoint", options?.pathname ?? "dashboard"); - - if (options?.orgSlug) { - url.searchParams.set("org", options.orgSlug); - } - - return url.toString(); -} +export { OPENWORK_FEEDBACK_URL, buildDenFeedbackUrl }; export function formatTemplateTimestamp(value: string | null, options?: { includeTime?: boolean }) { if (!value) { diff --git a/ee/apps/den-web/app/(den)/o/[orgSlug]/dashboard/_providers/org-dashboard-provider.tsx b/ee/apps/den-web/app/(den)/o/[orgSlug]/dashboard/_providers/org-dashboard-provider.tsx index 2454647af..a85d5671d 100644 --- a/ee/apps/den-web/app/(den)/o/[orgSlug]/dashboard/_providers/org-dashboard-provider.tsx +++ b/ee/apps/den-web/app/(den)/o/[orgSlug]/dashboard/_providers/org-dashboard-provider.tsx @@ -9,7 +9,7 @@ import { } from "react"; import { useRouter } from "next/navigation"; import { useDenFlow } from "../../../../_providers/den-flow-provider"; -import { getErrorMessage, requestJson } from "../../../../_lib/den-flow"; +import { getErrorMessage, getOrgLimitError, requestJson } from "../../../../_lib/den-flow"; import { type DenOrgContext, type DenOrgSummary, @@ -158,6 +158,10 @@ export function OrgDashboardProvider({ ); if (!response.ok) { + if (response.status === 402) { + router.push("/checkout"); + return; + } throw new Error(getErrorMessage(payload, `Failed to create organization (${response.status}).`)); } @@ -193,6 +197,10 @@ export function OrgDashboardProvider({ ); if (!response.ok) { + const limitError = getOrgLimitError(payload); + if (limitError) { + throw limitError; + } throw new Error(getErrorMessage(payload, `Failed to invite member (${response.status}).`)); } }); diff --git a/ee/packages/den-db/drizzle/0007_organization_metadata_limits.sql b/ee/packages/den-db/drizzle/0007_organization_metadata_limits.sql new file mode 100644 index 000000000..092b35531 --- /dev/null +++ b/ee/packages/den-db/drizzle/0007_organization_metadata_limits.sql @@ -0,0 +1,27 @@ +UPDATE `organization` +SET `metadata` = '{}' +WHERE `metadata` IS NULL + OR TRIM(`metadata`) = '' + OR JSON_VALID(`metadata`) = 0; + +ALTER TABLE `organization` + MODIFY COLUMN `metadata` json NULL; + +UPDATE `organization` +SET `metadata` = JSON_SET( + `metadata`, + '$.limits.members', + COALESCE( + NULLIF(CAST(JSON_UNQUOTE(JSON_EXTRACT(`metadata`, '$.limits.members')) AS SIGNED), 0), + 5 + ), + '$.limits.workers', + COALESCE( + NULLIF(CAST(JSON_UNQUOTE(COALESCE(JSON_EXTRACT(`metadata`, '$.limits.workers'), JSON_EXTRACT(`metadata`, '$.limits.Workers'))) AS SIGNED), 0), + 1 + ) +); + +UPDATE `organization` +SET `metadata` = JSON_REMOVE(`metadata`, '$.limits.Workers') +WHERE JSON_EXTRACT(`metadata`, '$.limits.Workers') IS NOT NULL; diff --git a/ee/packages/den-db/drizzle/meta/0007_snapshot.json b/ee/packages/den-db/drizzle/meta/0007_snapshot.json new file mode 100644 index 000000000..7c53ac8d5 --- /dev/null +++ b/ee/packages/den-db/drizzle/meta/0007_snapshot.json @@ -0,0 +1,2066 @@ +{ + "version": "5", + "dialect": "mysql", + "id": "0c7f8a74-8ffd-40db-9a67-f5ae72db8656", + "prevId": "01bf4a61-490e-449d-b17d-295d8546bd22", + "tables": { + "account": { + "name": "account", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + } + }, + "indexes": { + "account_user_id": { + "name": "account_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "account_id": { + "name": "account_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "session": { + "name": "session", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "active_organization_id": { + "name": "active_organization_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "active_team_id": { + "name": "active_team_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + } + }, + "indexes": { + "session_token": { + "name": "session_token", + "columns": [ + "token" + ], + "isUnique": true + }, + "session_user_id": { + "name": "session_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "session_id": { + "name": "session_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user": { + "name": "user", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + } + }, + "indexes": { + "user_email": { + "name": "user_email", + "columns": [ + "email" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "user_id": { + "name": "user_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "verification": { + "name": "verification", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "identifier": { + "name": "identifier", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + } + }, + "indexes": { + "verification_identifier": { + "name": "verification_identifier", + "columns": [ + "identifier" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "verification_id": { + "name": "verification_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "desktop_handoff_grant": { + "name": "desktop_handoff_grant", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_token": { + "name": "session_token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "consumed_at": { + "name": "consumed_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": { + "desktop_handoff_grant_user_id": { + "name": "desktop_handoff_grant_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "desktop_handoff_grant_expires_at": { + "name": "desktop_handoff_grant_expires_at", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "desktop_handoff_grant_id": { + "name": "desktop_handoff_grant_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "invitation": { + "name": "invitation", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pending'" + }, + "team_id": { + "name": "team_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "inviter_id": { + "name": "inviter_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": { + "invitation_organization_id": { + "name": "invitation_organization_id", + "columns": [ + "organization_id" + ], + "isUnique": false + }, + "invitation_email": { + "name": "invitation_email", + "columns": [ + "email" + ], + "isUnique": false + }, + "invitation_status": { + "name": "invitation_status", + "columns": [ + "status" + ], + "isUnique": false + }, + "invitation_team_id": { + "name": "invitation_team_id", + "columns": [ + "team_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "invitation_id": { + "name": "invitation_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "member": { + "name": "member", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'member'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": { + "member_organization_id": { + "name": "member_organization_id", + "columns": [ + "organization_id" + ], + "isUnique": false + }, + "member_user_id": { + "name": "member_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "member_organization_user": { + "name": "member_organization_user", + "columns": [ + "organization_id", + "user_id" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "member_id": { + "name": "member_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "organization": { + "name": "organization", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "slug": { + "name": "slug", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "logo": { + "name": "logo", + "type": "varchar(2048)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "metadata": { + "name": "metadata", + "type": "json", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + } + }, + "indexes": { + "organization_slug": { + "name": "organization_slug", + "columns": [ + "slug" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "organization_id": { + "name": "organization_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "organization_role": { + "name": "organization_role", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission": { + "name": "permission", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + } + }, + "indexes": { + "organization_role_organization_id": { + "name": "organization_role_organization_id", + "columns": [ + "organization_id" + ], + "isUnique": false + }, + "organization_role_name": { + "name": "organization_role_name", + "columns": [ + "organization_id", + "role" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "organization_role_id": { + "name": "organization_role_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "temp_template_sharing": { + "name": "temp_template_sharing", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "creator_member_id": { + "name": "creator_member_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "creator_user_id": { + "name": "creator_user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "template_json": { + "name": "template_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + } + }, + "indexes": { + "temp_template_sharing_org_id": { + "name": "temp_template_sharing_org_id", + "columns": [ + "organization_id" + ], + "isUnique": false + }, + "temp_template_sharing_creator_member_id": { + "name": "temp_template_sharing_creator_member_id", + "columns": [ + "creator_member_id" + ], + "isUnique": false + }, + "temp_template_sharing_creator_user_id": { + "name": "temp_template_sharing_creator_user_id", + "columns": [ + "creator_user_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "temp_template_sharing_id": { + "name": "temp_template_sharing_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "skill_hub_member": { + "name": "skill_hub_member", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "skill_hub_id": { + "name": "skill_hub_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "org_membership_id": { + "name": "org_membership_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "team_id": { + "name": "team_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": { + "skill_hub_member_skill_hub_id": { + "name": "skill_hub_member_skill_hub_id", + "columns": [ + "skill_hub_id" + ], + "isUnique": false + }, + "skill_hub_member_org_membership_id": { + "name": "skill_hub_member_org_membership_id", + "columns": [ + "org_membership_id" + ], + "isUnique": false + }, + "skill_hub_member_team_id": { + "name": "skill_hub_member_team_id", + "columns": [ + "team_id" + ], + "isUnique": false + }, + "skill_hub_member_hub_org_membership": { + "name": "skill_hub_member_hub_org_membership", + "columns": [ + "skill_hub_id", + "org_membership_id" + ], + "isUnique": true + }, + "skill_hub_member_hub_team": { + "name": "skill_hub_member_hub_team", + "columns": [ + "skill_hub_id", + "team_id" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "skill_hub_member_id": { + "name": "skill_hub_member_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "skill_hub_skill": { + "name": "skill_hub_skill", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "skill_hub_id": { + "name": "skill_hub_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "skill_id": { + "name": "skill_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "org_membership_id": { + "name": "org_membership_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": { + "skill_hub_skill_skill_hub_id": { + "name": "skill_hub_skill_skill_hub_id", + "columns": [ + "skill_hub_id" + ], + "isUnique": false + }, + "skill_hub_skill_skill_id": { + "name": "skill_hub_skill_skill_id", + "columns": [ + "skill_id" + ], + "isUnique": false + }, + "skill_hub_skill_hub_skill": { + "name": "skill_hub_skill_hub_skill", + "columns": [ + "skill_hub_id", + "skill_id" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "skill_hub_skill_id": { + "name": "skill_hub_skill_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "skill_hub": { + "name": "skill_hub", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_by_org_membership_id": { + "name": "created_by_org_membership_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + } + }, + "indexes": { + "skill_hub_organization_id": { + "name": "skill_hub_organization_id", + "columns": [ + "organization_id" + ], + "isUnique": false + }, + "skill_hub_created_by_org_membership_id": { + "name": "skill_hub_created_by_org_membership_id", + "columns": [ + "created_by_org_membership_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "skill_hub_id": { + "name": "skill_hub_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "skill": { + "name": "skill", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_by_org_membership_id": { + "name": "created_by_org_membership_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "skill_text": { + "name": "skill_text", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "shared": { + "name": "shared", + "type": "enum('org','public')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + } + }, + "indexes": { + "skill_organization_id": { + "name": "skill_organization_id", + "columns": [ + "organization_id" + ], + "isUnique": false + }, + "skill_created_by_org_membership_id": { + "name": "skill_created_by_org_membership_id", + "columns": [ + "created_by_org_membership_id" + ], + "isUnique": false + }, + "skill_shared": { + "name": "skill_shared", + "columns": [ + "shared" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "skill_id": { + "name": "skill_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "team_member": { + "name": "team_member", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "team_id": { + "name": "team_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "org_membership_id": { + "name": "org_membership_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": { + "team_member_team_id": { + "name": "team_member_team_id", + "columns": [ + "team_id" + ], + "isUnique": false + }, + "team_member_org_membership_id": { + "name": "team_member_org_membership_id", + "columns": [ + "org_membership_id" + ], + "isUnique": false + }, + "team_member_team_org_membership": { + "name": "team_member_team_org_membership", + "columns": [ + "team_id", + "org_membership_id" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "team_member_id": { + "name": "team_member_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "team": { + "name": "team", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + } + }, + "indexes": { + "team_organization_id": { + "name": "team_organization_id", + "columns": [ + "organization_id" + ], + "isUnique": false + }, + "team_organization_name": { + "name": "team_organization_name", + "columns": [ + "organization_id", + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "team_id": { + "name": "team_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "audit_event": { + "name": "audit_event", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "org_id": { + "name": "org_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "worker_id": { + "name": "worker_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "actor_user_id": { + "name": "actor_user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "action": { + "name": "action", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "payload": { + "name": "payload", + "type": "json", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": { + "audit_event_org_id": { + "name": "audit_event_org_id", + "columns": [ + "org_id" + ], + "isUnique": false + }, + "audit_event_worker_id": { + "name": "audit_event_worker_id", + "columns": [ + "worker_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "audit_event_id": { + "name": "audit_event_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "daytona_sandbox": { + "name": "daytona_sandbox", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "worker_id": { + "name": "worker_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sandbox_id": { + "name": "sandbox_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "workspace_volume_id": { + "name": "workspace_volume_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "data_volume_id": { + "name": "data_volume_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "signed_preview_url": { + "name": "signed_preview_url", + "type": "varchar(2048)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "signed_preview_url_expires_at": { + "name": "signed_preview_url_expires_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "region": { + "name": "region", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + } + }, + "indexes": { + "daytona_sandbox_worker_id": { + "name": "daytona_sandbox_worker_id", + "columns": [ + "worker_id" + ], + "isUnique": true + }, + "daytona_sandbox_sandbox_id": { + "name": "daytona_sandbox_sandbox_id", + "columns": [ + "sandbox_id" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "daytona_sandbox_id": { + "name": "daytona_sandbox_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "worker_bundle": { + "name": "worker_bundle", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "worker_id": { + "name": "worker_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "storage_url": { + "name": "storage_url", + "type": "varchar(2048)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": { + "worker_bundle_worker_id": { + "name": "worker_bundle_worker_id", + "columns": [ + "worker_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "worker_bundle_id": { + "name": "worker_bundle_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "worker_instance": { + "name": "worker_instance", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "worker_id": { + "name": "worker_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "region": { + "name": "region", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "varchar(2048)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "enum('provisioning','healthy','failed','stopped')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + } + }, + "indexes": { + "worker_instance_worker_id": { + "name": "worker_instance_worker_id", + "columns": [ + "worker_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "worker_instance_id": { + "name": "worker_instance_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "worker": { + "name": "worker", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "org_id": { + "name": "org_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "varchar(1024)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "destination": { + "name": "destination", + "type": "enum('local','cloud')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "enum('provisioning','healthy','failed','stopped')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "image_version": { + "name": "image_version", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "workspace_path": { + "name": "workspace_path", + "type": "varchar(1024)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sandbox_backend": { + "name": "sandbox_backend", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_heartbeat_at": { + "name": "last_heartbeat_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_active_at": { + "name": "last_active_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + } + }, + "indexes": { + "worker_org_id": { + "name": "worker_org_id", + "columns": [ + "org_id" + ], + "isUnique": false + }, + "worker_created_by_user_id": { + "name": "worker_created_by_user_id", + "columns": [ + "created_by_user_id" + ], + "isUnique": false + }, + "worker_status": { + "name": "worker_status", + "columns": [ + "status" + ], + "isUnique": false + }, + "worker_last_heartbeat_at": { + "name": "worker_last_heartbeat_at", + "columns": [ + "last_heartbeat_at" + ], + "isUnique": false + }, + "worker_last_active_at": { + "name": "worker_last_active_at", + "columns": [ + "last_active_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "worker_id": { + "name": "worker_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "worker_token": { + "name": "worker_token", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "worker_id": { + "name": "worker_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "enum('client','host','activity')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "worker_token_worker_id": { + "name": "worker_token_worker_id", + "columns": [ + "worker_id" + ], + "isUnique": false + }, + "worker_token_token": { + "name": "worker_token_token", + "columns": [ + "token" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "worker_token_id": { + "name": "worker_token_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "admin_allowlist": { + "name": "admin_allowlist", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "note": { + "name": "note", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + } + }, + "indexes": { + "admin_allowlist_email": { + "name": "admin_allowlist_email", + "columns": [ + "email" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "admin_allowlist_id": { + "name": "admin_allowlist_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "rate_limit": { + "name": "rate_limit", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "varchar(512)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "count": { + "name": "count", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "last_request": { + "name": "last_request", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "rate_limit_key": { + "name": "rate_limit_key", + "columns": [ + "key" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "rate_limit_id": { + "name": "rate_limit_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + } + }, + "views": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "tables": {}, + "indexes": {} + } +} diff --git a/ee/packages/den-db/drizzle/meta/_journal.json b/ee/packages/den-db/drizzle/meta/_journal.json index 9df96f4a2..ff99f0ca4 100644 --- a/ee/packages/den-db/drizzle/meta/_journal.json +++ b/ee/packages/den-db/drizzle/meta/_journal.json @@ -43,6 +43,13 @@ "when": 1775077000006, "tag": "0006_skill_hub", "breakpoints": true + }, + { + "idx": 7, + "version": "5", + "when": 1775170250887, + "tag": "0007_organization_metadata_limits", + "breakpoints": true } ] } diff --git a/ee/packages/den-db/src/schema/org.ts b/ee/packages/den-db/src/schema/org.ts index a87191642..f157fb7e0 100644 --- a/ee/packages/den-db/src/schema/org.ts +++ b/ee/packages/den-db/src/schema/org.ts @@ -1,5 +1,5 @@ import { relations, sql } from "drizzle-orm" -import { index, mysqlTable, text, timestamp, uniqueIndex, varchar } from "drizzle-orm/mysql-core" +import { index, json, mysqlTable, text, timestamp, uniqueIndex, varchar } from "drizzle-orm/mysql-core" import { denTypeIdColumn } from "../columns" export const DesktopHandoffGrantTable = mysqlTable( @@ -25,7 +25,7 @@ export const OrganizationTable = mysqlTable( name: varchar("name", { length: 255 }).notNull(), slug: varchar("slug", { length: 255 }).notNull(), logo: varchar("logo", { length: 2048 }), - metadata: text("metadata"), + metadata: json("metadata").$type | null>(), createdAt: timestamp("created_at", { fsp: 3 }).notNull().defaultNow(), updatedAt: timestamp("updated_at", { fsp: 3 }) .notNull()