diff --git a/packages/api/src/root.ts b/packages/api/src/root.ts index 0d810e54..de13958b 100644 --- a/packages/api/src/root.ts +++ b/packages/api/src/root.ts @@ -9,7 +9,7 @@ import { invitesRouter } from "./routers/invites"; import { projectsRouter } from "./routers/projects"; import { providersRouter } from "./routers/providers"; import { defsRouter } from "./routers/defs"; -import { teamsRouter } from "./routers/teams"; +import { orgsRouter } from "./routers/orgs"; import { tablesRouter } from "./routers/tables"; import { usersRouter } from "./routers/users"; @@ -34,7 +34,7 @@ export function appRouter( return createTRPCRouter({ auth: authRouter(store), projects: projectsRouter(store), - teams: teamsRouter(store, sendInvite), + orgs: orgsRouter(store, sendInvite), defs: defsRouter(store), tables: tablesRouter(store), invites: invitesRouter(store, sendInvite, dataSealPass), diff --git a/packages/api/src/routers/auth.ts b/packages/api/src/routers/auth.ts index 67e8dd4b..4f2ac690 100644 --- a/packages/api/src/routers/auth.ts +++ b/packages/api/src/routers/auth.ts @@ -31,7 +31,7 @@ export function authRouter(store: Store) { // TODO: do we want to verify domain and time here? }); ctx.session.siweFields = fields.data; - const info = await store.auth.userAndPersonalTeamByAddress( + const info = await store.auth.userAndPersonalOrgByAddress( fields.data.address, ); if (info) { @@ -63,7 +63,7 @@ export function authRouter(store: Store) { }); } try { - const auth = await store.auth.createUserAndPersonalTeam( + const auth = await store.auth.createUserAndPersonalOrg( ctx.session.siweFields.address, input.username, input.email, diff --git a/packages/api/src/routers/invites.ts b/packages/api/src/routers/invites.ts index 1da4d1c2..a9dc924a 100644 --- a/packages/api/src/routers/invites.ts +++ b/packages/api/src/routers/invites.ts @@ -6,7 +6,7 @@ import { protectedProcedure, publicProcedure, createTRPCRouter, - teamProcedure, + orgProcedure, } from "../trpc"; import { type SendInviteFunc } from "../utils/sendInvite"; @@ -16,23 +16,23 @@ export function invitesRouter( dataSealPass: string, ) { return createTRPCRouter({ - invitesForTeam: teamProcedure(store).query(async ({ ctx }) => { - const invites = await store.invites.invitesForTeam(ctx.teamId); - return { invites, teamAuthorization: ctx.teamAuthorization }; + invitesForOrg: orgProcedure(store).query(async ({ ctx }) => { + const invites = await store.invites.invitesForOrg(ctx.orgId); + return { invites, orgAuthorization: ctx.orgAuthorization }; }), - inviteEmails: teamProcedure(store) + inviteEmails: orgProcedure(store) .input(z.object({ emails: z.array(z.string().trim().email()) })) .mutation(async ({ ctx, input }) => { - const invites = await store.invites.inviteEmailsToTeam( - ctx.teamId, - ctx.session.auth.user.teamId, + const invites = await store.invites.inviteEmailsToOrg( + ctx.orgId, + ctx.session.auth.user.orgId, input.emails, ); await Promise.all( invites.map(async (invite) => await sendInvite(invite)), ); }), - resendInvite: teamProcedure(store) + resendInvite: orgProcedure(store) .input(z.object({ inviteId: z.string().trim() })) .mutation(async ({ input }) => { const invite = await store.invites.inviteById(input.inviteId); @@ -80,13 +80,13 @@ export function invitesRouter( } // we want to make sure and check for "" since these columns are type text // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - if (invite.claimedAt || invite.claimedByTeamId) { + if (invite.claimedAt || invite.claimedByOrgId) { throw new TRPCError({ code: "PRECONDITION_FAILED", message: "Invite has already been claimed", }); } - await store.invites.acceptInvite(invite, ctx.session.auth.personalTeam); + await store.invites.acceptInvite(invite, ctx.session.auth.personalOrg); return invite; }), ignoreInvite: publicProcedure @@ -104,7 +104,7 @@ export function invitesRouter( } // we want to make sure and check for "" since these columns are type text // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - if (invite.claimedAt || invite.claimedByTeamId) { + if (invite.claimedAt || invite.claimedByOrgId) { throw new TRPCError({ code: "PRECONDITION_FAILED", message: "Invite has already been claimed", @@ -113,7 +113,7 @@ export function invitesRouter( await store.invites.deleteInvite(inviteId); return invite; }), - deleteInvite: teamProcedure(store) + deleteInvite: orgProcedure(store) .input(z.object({ inviteId: z.string().trim() })) .mutation(async ({ input }) => { await store.invites.deleteInvite(input.inviteId); diff --git a/packages/api/src/routers/teams.ts b/packages/api/src/routers/orgs.ts similarity index 50% rename from packages/api/src/routers/teams.ts rename to packages/api/src/routers/orgs.ts index e7f15f34..5aaa7447 100644 --- a/packages/api/src/routers/teams.ts +++ b/packages/api/src/routers/orgs.ts @@ -2,135 +2,135 @@ import { type Store, type schema } from "@tableland/studio-store"; import { TRPCError } from "@trpc/server"; import { z } from "zod"; import { - teamNameAvailableSchema, - newTeamSchema, - updateTeamSchema, + orgNameAvailableSchema, + newOrgSchema, + updateOrgSchema, } from "@tableland/studio-validators"; import { protectedProcedure, publicProcedure, createTRPCRouter, - teamAdminProcedure, + orgAdminProcedure, } from "../trpc"; import { type SendInviteFunc } from "../utils/sendInvite"; import { internalError } from "../utils/internalError"; import { zeroNine } from "../utils/fourHundredError"; -export function teamsRouter(store: Store, sendInvite: SendInviteFunc) { +export function orgsRouter(store: Store, sendInvite: SendInviteFunc) { return createTRPCRouter({ isAuthorized: publicProcedure - .input(z.object({ teamId: z.string().trim() }).or(z.void())) + .input(z.object({ orgId: z.string().trim() }).or(z.void())) .query(async ({ ctx, input }) => { if (!ctx.session.auth) { return false; } // we want to check for null, undefined, and "" // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - const teamId = input?.teamId || ctx.session.auth.user.teamId; - if (!teamId) { + const orgId = input?.orgId || ctx.session.auth.user.orgId; + if (!orgId) { throw new TRPCError({ code: "BAD_REQUEST", - message: "Team ID must be provided as input or session context", + message: "Org ID must be provided as input or session context", }); } - return await store.teams.isAuthorizedForTeam( - ctx.session.auth.user.teamId, - teamId, + return await store.orgs.isAuthorizedForOrg( + ctx.session.auth.user.orgId, + orgId, ); }), nameAvailable: publicProcedure - .input(teamNameAvailableSchema) + .input(orgNameAvailableSchema) .query(async ({ input }) => { - return await store.teams.nameAvailable(input.name, input.teamId); + return await store.orgs.nameAvailable(input.name, input.orgId); }), - getTeam: publicProcedure - .input(z.object({ teamId: z.string().trim() }).or(z.void())) + getOrg: publicProcedure + .input(z.object({ orgId: z.string().trim() }).or(z.void())) .query(async ({ input, ctx }) => { // we want to check for null, undefined, and "" // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - const teamId = input?.teamId || ctx.session.auth?.user.teamId; - if (!teamId) { + const orgId = input?.orgId || ctx.session.auth?.user.orgId; + if (!orgId) { throw new TRPCError({ code: "BAD_REQUEST", - message: "Team ID must be provided as input or session context", + message: "Org ID must be provided as input or session context", }); } - const team = await store.teams.teamById(teamId); - if (!team) { - throw new TRPCError({ code: "NOT_FOUND", message: "Team not found" }); + const org = await store.orgs.orgById(orgId); + if (!org) { + throw new TRPCError({ code: "NOT_FOUND", message: "Org not found" }); } - return team; + return org; }), - teamBySlug: publicProcedure + orgBySlug: publicProcedure .input(z.object({ slug: z.string().trim() })) .query(async ({ input }) => { - const team = await store.teams.teamBySlug(input.slug); - if (!team) { - throw new TRPCError({ code: "NOT_FOUND", message: "Team not found" }); + const org = await store.orgs.orgBySlug(input.slug); + if (!org) { + throw new TRPCError({ code: "NOT_FOUND", message: "Org not found" }); } - return team; + return org; }), - userTeams: publicProcedure - .input(z.object({ userTeamId: z.string().trim().min(1) }).or(z.void())) + userOrgs: publicProcedure + .input(z.object({ userOrgId: z.string().trim().min(1) }).or(z.void())) .query(async ({ input, ctx }) => { // we want to check for null, undefined, and "" // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - const teamId = input?.userTeamId || ctx.session.auth?.user.teamId; - if (!teamId) { + const orgId = input?.userOrgId || ctx.session.auth?.user.orgId; + if (!orgId) { throw new TRPCError({ code: "BAD_REQUEST", - message: "Team ID must be provided as input or session context", + message: "Org ID must be provided as input or session context", }); } - return await store.teams.teamsByMemberId(teamId); + return await store.orgs.orgsByMemberId(orgId); }), - userTeamsFromAddress: publicProcedure + userOrgsFromAddress: publicProcedure .input(z.object({ userAddress: z.string().trim().min(1) })) .query(async ({ input, ctx }) => { - const personalTeam = await store.users.userPersonalTeam( + const personalOrg = await store.users.userPersonalOrg( input?.userAddress, ); - if (!personalTeam) { + if (!personalOrg) { throw new TRPCError({ code: "NOT_FOUND", - message: `No personal team found for ${input?.userAddress}`, + message: `No personal org found for ${input?.userAddress}`, }); } - return await store.teams.teamsByMemberId(personalTeam); + return await store.orgs.orgsByMemberId(personalOrg); }), - newTeam: protectedProcedure - .input(newTeamSchema) + newOrg: protectedProcedure + .input(newOrgSchema) .mutation(async ({ ctx, input }) => { - let team: schema.Team; - let invites: schema.TeamInvite[]; + let org: schema.Org; + let invites: schema.OrgInvite[]; try { - const nameAvailable = await store.teams.nameAvailable(input.name); + const nameAvailable = await store.orgs.nameAvailable(input.name); if (!nameAvailable) { - throw zeroNine(`the team name ${input.name} is not available`); + throw zeroNine(`the org name ${input.name} is not available`); } } catch (err: any) { if (err.status !== 409) { - throw internalError("Error validating team name", err); + throw internalError("Error validating org name", err); } throw err; } try { - const res = await store.teams.createTeamByPersonalTeam( + const res = await store.orgs.createOrgByPersonalOrg( input.name, - ctx.session.auth.user.teamId, + ctx.session.auth.user.orgId, input.emailInvites, ); - team = res.team; + org = res.org; invites = res.invites; } catch (err) { - throw internalError("Error creating team", err); + throw internalError("Error creating org", err); } - // Intentionally swallowing any error here since the team is still + // Intentionally swallowing any error here since the org is still // created and invites can be viewed and resent at any time. try { await Promise.all( @@ -147,56 +147,56 @@ export function teamsRouter(store: Store, sendInvite: SendInviteFunc) { console.error(err); } - return team; + return org; }), - updateTeam: teamAdminProcedure(store) - .input(updateTeamSchema) + updateOrg: orgAdminProcedure(store) + .input(updateOrgSchema) .mutation(async ({ input: { name }, ctx }) => { - let team: schema.Team | undefined; + let org: schema.Org | undefined; try { - team = await store.teams.updateTeam(ctx.teamId, name); + org = await store.orgs.updateOrg(ctx.orgId, name); } catch (err) { - throw internalError("Error updating team", err); + throw internalError("Error updating org", err); } - if (!team) { + if (!org) { throw new TRPCError({ code: "NOT_FOUND", - message: "Team not found", + message: "Org not found", }); } - return team; + return org; }), - deleteTeam: teamAdminProcedure(store).mutation(async ({ ctx }) => { + deleteOrg: orgAdminProcedure(store).mutation(async ({ ctx }) => { try { - await store.teams.deleteTeam(ctx.teamId); + await store.orgs.deleteOrg(ctx.orgId); } catch (err) { - throw internalError("Error deleting team", err); + throw internalError("Error deleting org", err); } }), - usersForTeam: publicProcedure - .input(z.object({ teamId: z.string().trim() }).or(z.void())) + usersForOrg: publicProcedure + .input(z.object({ orgId: z.string().trim() }).or(z.void())) .query(async ({ input, ctx }) => { // we want to check for null, undefined, and "" // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - const teamId = input?.teamId || ctx.session.auth?.user.teamId; - if (!teamId) { + const orgId = input?.orgId || ctx.session.auth?.user.orgId; + if (!orgId) { throw new TRPCError({ code: "BAD_REQUEST", - message: "Team ID must be provided as input or session context", + message: "Org ID must be provided as input or session context", }); } - const people = await store.teams.userTeamsForTeamId(teamId); + const people = await store.orgs.userOrgsForOrgId(orgId); return people; }), - toggleAdmin: teamAdminProcedure(store) + toggleAdmin: orgAdminProcedure(store) .input(z.object({ userId: z.string().trim() })) .mutation(async ({ input, ctx }) => { - await store.teams.toggleAdmin(ctx.teamId, input.userId); + await store.orgs.toggleAdmin(ctx.orgId, input.userId); }), - removeTeamMember: teamAdminProcedure(store) + removeOrgMember: orgAdminProcedure(store) .input(z.object({ userId: z.string().trim() })) .mutation(async ({ input, ctx }) => { - await store.teams.removeTeamMember(ctx.teamId, input.userId); + await store.orgs.removeOrgMember(ctx.orgId, input.userId); }), }); } diff --git a/packages/api/src/routers/projects.ts b/packages/api/src/routers/projects.ts index c01875f8..e8c02967 100644 --- a/packages/api/src/routers/projects.ts +++ b/packages/api/src/routers/projects.ts @@ -9,46 +9,46 @@ import { import { publicProcedure, createTRPCRouter, - teamProcedure, + orgProcedure, projectAdminProcedure, } from "../trpc"; import { internalError } from "../utils/internalError"; export function projectsRouter(store: Store) { return createTRPCRouter({ - teamProjects: publicProcedure - .input(z.object({ teamId: z.string().trim().min(1) }).or(z.void())) + orgProjects: publicProcedure + .input(z.object({ orgId: z.string().trim().min(1) }).or(z.void())) .query(async ({ ctx, input }) => { // we want to check for null, undefined, and "" // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - const teamId = input?.teamId || ctx.session.auth?.user.teamId; - if (!teamId) { + const orgId = input?.orgId || ctx.session.auth?.user.orgId; + if (!orgId) { throw new TRPCError({ code: "BAD_REQUEST", - message: "Team ID must be provided as input or session context", + message: "Org ID must be provided as input or session context", }); } - return await store.projects.projectsByTeamId(teamId); + return await store.projects.projectsByOrgId(orgId); }), projectBySlug: publicProcedure .input( z.object({ - teamId: z.string().trim().optional(), + orgId: z.string().trim().optional(), slug: z.string().trim(), }), ) .query(async ({ input, ctx }) => { // we want to check for null, undefined, and "" // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - const teamId = input?.teamId || ctx.session.auth?.user.teamId; - if (!teamId) { + const orgId = input?.orgId || ctx.session.auth?.user.orgId; + if (!orgId) { throw new TRPCError({ code: "BAD_REQUEST", - message: "Team ID must be provided as input or session context", + message: "Org ID must be provided as input or session context", }); } - const project = await store.projects.projectByTeamIdAndSlug( - teamId, + const project = await store.projects.projectByOrgIdAndSlug( + orgId, input.slug, ); if (!project) { @@ -64,25 +64,25 @@ export function projectsRouter(store: Store) { .query(async ({ input, ctx }) => { // we want to check for null, undefined, and "" // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - const teamId = input?.teamId || ctx.session.auth?.user.teamId; - if (!teamId) { + const orgId = input?.orgId || ctx.session.auth?.user.orgId; + if (!orgId) { throw new TRPCError({ code: "BAD_REQUEST", - message: "Team ID must be provided as input or session context", + message: "Org ID must be provided as input or session context", }); } return await store.projects.nameAvailable( - teamId, + orgId, input.name, input.projectId, ); }), - newProject: teamProcedure(store) + newProject: orgProcedure(store) .input(newProjectSchema) .mutation(async ({ input, ctx }) => { try { const project = await store.projects.createProject( - ctx.teamId, + ctx.orgId, input.name, input.description, input.envNames.map((env) => env.name), diff --git a/packages/api/src/session-data.ts b/packages/api/src/session-data.ts index 11f7d268..ac5d3557 100644 --- a/packages/api/src/session-data.ts +++ b/packages/api/src/session-data.ts @@ -11,7 +11,7 @@ export type SiweFields = Omit< export interface Auth { user: schema.User; - personalTeam: schema.Team; + personalOrg: schema.Org; } export interface SessionData { diff --git a/packages/api/src/trpc.ts b/packages/api/src/trpc.ts index 4b5bd949..619744c7 100644 --- a/packages/api/src/trpc.ts +++ b/packages/api/src/trpc.ts @@ -207,40 +207,40 @@ export const protectedProcedure = publicProcedure.use(async ({ ctx, next }) => { }); }); -export const teamProcedure = (store: Store) => +export const orgProcedure = (store: Store) => protectedProcedure - .input(z.object({ teamId: z.string().trim().min(1).optional() })) + .input(z.object({ orgId: z.string().trim().min(1).optional() })) .use(async ({ ctx, input, next }) => { // we want to check for null, undefined, and "" // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - const teamId = input.teamId || ctx.session.auth.user.teamId; - const membership = await store.teams.isAuthorizedForTeam( - ctx.session.auth.user.teamId, - teamId, + const orgId = input.orgId || ctx.session.auth.user.orgId; + const membership = await store.orgs.isAuthorizedForOrg( + ctx.session.auth.user.orgId, + orgId, ); if (!membership) { throw new TRPCError({ code: "UNAUTHORIZED", - message: "not authorized for team", + message: "not authorized for org", }); } - input.teamId = teamId; + input.orgId = orgId; return await next({ ctx: { ...ctx, session: ctx.session, - teamAuthorization: membership, - teamId, + orgAuthorization: membership, + orgId, }, }); }); -export const teamAdminProcedure = (store: Store) => - teamProcedure(store).use(async ({ ctx, next }) => { - if (!ctx.teamAuthorization.isOwner) { +export const orgAdminProcedure = (store: Store) => + orgProcedure(store).use(async ({ ctx, next }) => { + if (!ctx.orgAuthorization.isOwner) { throw new TRPCError({ code: "UNAUTHORIZED", - message: "not authorized as team admin", + message: "not authorized as org admin", }); } return await next({ ctx }); @@ -250,31 +250,31 @@ export const projectProcedure = (store: Store) => protectedProcedure .input(z.object({ projectId: z.string().trim().min(1) })) .use(async ({ ctx, input, next }) => { - const team = await store.projects.projectTeamByProjectId(input.projectId); - if (!team) { + const org = await store.projects.projectOrgByProjectId(input.projectId); + if (!org) { throw new TRPCError({ code: "NOT_FOUND", - message: "no team for project id found", + message: "no org for project id found", }); } - const membership = await store.teams.isAuthorizedForTeam( - ctx.session.auth.user.teamId, - team.id, + const membership = await store.orgs.isAuthorizedForOrg( + ctx.session.auth.user.orgId, + org.id, ); if (!membership) { throw new TRPCError({ code: "UNAUTHORIZED", - message: "not authorized for team", + message: "not authorized for org", }); } return await next({ - ctx: { ...ctx, session: ctx.session, teamAuthorization: membership }, + ctx: { ...ctx, session: ctx.session, orgAuthorization: membership }, }); }); export const projectAdminProcedure = (store: Store) => projectProcedure(store).use(async ({ ctx, next }) => { - if (!ctx.teamAuthorization.isOwner) { + if (!ctx.orgAuthorization.isOwner) { throw new TRPCError({ code: "UNAUTHORIZED", message: "not authorized as project admin", @@ -287,12 +287,12 @@ export const environmentProcedure = (store: Store) => protectedProcedure .input(z.object({ envId: z.string().uuid() })) .use(async ({ ctx, input, next }) => { - const { team, project } = - (await store.environments.environmentTeamAndProject(input.envId)) ?? {}; - if (!team) { + const { org, project } = + (await store.environments.environmentOrgAndProject(input.envId)) ?? {}; + if (!org) { throw new TRPCError({ code: "NOT_FOUND", - message: "no team for env id found", + message: "no org for env id found", }); } if (!project) { @@ -301,30 +301,30 @@ export const environmentProcedure = (store: Store) => message: "no project for env id found", }); } - const membership = await store.teams.isAuthorizedForTeam( - ctx.session.auth.user.teamId, - team.id, + const membership = await store.orgs.isAuthorizedForOrg( + ctx.session.auth.user.orgId, + org.id, ); if (!membership) { throw new TRPCError({ code: "UNAUTHORIZED", - message: "not authorized for team", + message: "not authorized for org", }); } return await next({ ctx: { ...ctx, session: ctx.session, - team, + org, project, - teamAuthorization: membership, + orgAuthorization: membership, }, }); }); export const environmentAdminProcedure = (store: Store) => environmentProcedure(store).use(async ({ ctx, next }) => { - if (!ctx.teamAuthorization.isOwner) { + if (!ctx.orgAuthorization.isOwner) { throw new TRPCError({ code: "UNAUTHORIZED", message: "not authorized as environment admin", @@ -337,31 +337,31 @@ export const defProcedure = (store: Store) => protectedProcedure .input(z.object({ defId: z.string().trim().uuid() })) .use(async ({ ctx, input, next }) => { - const team = await store.defs.defTeam(input.defId); - if (!team) { + const org = await store.defs.defOrg(input.defId); + if (!org) { throw new TRPCError({ code: "NOT_FOUND", - message: "no team for def id found", + message: "no org for def id found", }); } - const membership = await store.teams.isAuthorizedForTeam( - ctx.session.auth.user.teamId, - team.id, + const membership = await store.orgs.isAuthorizedForOrg( + ctx.session.auth.user.orgId, + org.id, ); if (!membership) { throw new TRPCError({ code: "UNAUTHORIZED", - message: "not authorized for team", + message: "not authorized for org", }); } return await next({ - ctx: { ...ctx, session: ctx.session, teamAuthorization: membership }, + ctx: { ...ctx, session: ctx.session, orgAuthorization: membership }, }); }); export const defAdminProcedure = (store: Store) => defProcedure(store).use(async ({ ctx, next }) => { - if (!ctx.teamAuthorization.isOwner) { + if (!ctx.orgAuthorization.isOwner) { throw new TRPCError({ code: "UNAUTHORIZED", message: "not authorized as def admin", diff --git a/packages/api/src/utils/sendInvite.ts b/packages/api/src/utils/sendInvite.ts index bc8ba814..54cf545d 100644 --- a/packages/api/src/utils/sendInvite.ts +++ b/packages/api/src/utils/sendInvite.ts @@ -9,14 +9,14 @@ export function createSendInvite( createInviteLink: (seal: string) => string, mailApi: MailApi, ) { - return async function (invite: schema.TeamInvite) { - const inviterTeam = await store.teams.teamById(invite.inviterTeamId); - if (!inviterTeam) { - throw new Error("Inviter team not found"); + return async function (invite: schema.OrgInvite) { + const inviterOrg = await store.orgs.orgById(invite.inviterOrgId); + if (!inviterOrg) { + throw new Error("Inviter org not found"); } - const team = await store.teams.teamById(invite.teamId); - if (!team) { - throw new Error("Team not found"); + const org = await store.orgs.orgById(invite.orgId); + if (!org) { + throw new Error("Org not found"); } const seal = await sealData( { inviteId: invite.id }, @@ -30,8 +30,8 @@ export function createSendInvite( return await mailApi.sendInvite( invite.email, inviteImageLink, - inviterTeam.name, - team.name, + inviterOrg.name, + org.name, link, ); }; diff --git a/packages/cli/README.md b/packages/cli/README.md index 2c51f6ff..d51041e8 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -8,7 +8,7 @@ ## Background -The Studio command line lets you interact with your Studio teams, projects, and tables. The full, up-to-date docs can be found: [here](https://docs.tableland.xyz/studio/cli). +The Studio command line lets you interact with your Studio orgs, projects, and tables. The full, up-to-date docs can be found: [here](https://docs.tableland.xyz/studio/cli). ## Install @@ -32,7 +32,7 @@ Commands: studio login create a login session via private key studio logout logout current session - studio team manage studio teams + studio org manage studio orgs studio init create a tablelandrc config file studio project manage studio projects studio deployment manage studio deployments @@ -45,7 +45,7 @@ Commands: optionally with a new name studio use [context] [id] use the given context id for all ensuing commands. context can be one - of (team, project, or api). + of (org, project, or api). studio unuse [context] remove any existing id from the given context diff --git a/packages/cli/src/commands/index.ts b/packages/cli/src/commands/index.ts index 435fbba9..4112484a 100644 --- a/packages/cli/src/commands/index.ts +++ b/packages/cli/src/commands/index.ts @@ -7,7 +7,7 @@ import * as logout from "./logout.js"; import * as project from "./project.js"; import * as query from "./query.js"; import * as status from "./status.js"; -import * as team from "./team.js"; +import * as org from "./org.js"; import * as use from "./use.js"; import * as unuse from "./unuse.js"; @@ -21,7 +21,7 @@ export const commands = [ project, query, status, - team, + org, use, unuse, ]; diff --git a/packages/cli/src/commands/team.ts b/packages/cli/src/commands/org.ts similarity index 76% rename from packages/cli/src/commands/team.ts rename to packages/cli/src/commands/org.ts index 5a5a0fca..fa1a36cf 100644 --- a/packages/cli/src/commands/team.ts +++ b/packages/cli/src/commands/org.ts @@ -15,13 +15,13 @@ import { type Yargs = typeof yargs; -export const command = "team "; -export const desc = "manage studio teams"; +export const command = "org "; +export const desc = "manage studio orgs"; export interface CommandOptions extends GlobalOptions { name?: string; address?: string; - teamId?: string; + orgId?: string; invites?: string; } @@ -29,7 +29,7 @@ export const builder = function (args: Yargs) { return args .command( "ls [public key]", - "Get a list of your teams, or the teams for a public key address", + "Get a list of your orgs, or the orgs for a public key address", function (args) { return args.positional("address", { type: "string", @@ -54,16 +54,16 @@ export const builder = function (args: Yargs) { if (typeof address === "string" && address.trim() !== "") { // if address exists we will query based on address - const teams = await api.teams.userTeamsFromAddress.query({ + const orgs = await api.orgs.userOrgsFromAddress.query({ userAddress: address.trim(), }); - const pretty = JSON.stringify(teams, null, 4); + const pretty = JSON.stringify(orgs, null, 4); return logger.log(pretty); } - const teams = await api.teams.userTeams.query(); - const pretty = JSON.stringify(teams, null, 4); + const orgs = await api.orgs.userOrgs.query(); + const pretty = JSON.stringify(orgs, null, 4); logger.log(pretty); } catch (err) { @@ -73,27 +73,24 @@ export const builder = function (args: Yargs) { ) .command( "create ", - "create a team with the given name", + "create a org with the given name", function (args) { return args .positional("name", { type: "string", description: - "optional team name, if not provided all teams are returned", + "optional org name, if not provided all orgs are returned", }) .option("invites", { type: "string", default: "", description: - "comma separated list of emails to be invited to the team", + "comma separated list of emails to be invited to the org", }) as yargs.Argv; }, async function (argv: CommandOptions) { const { invites } = argv; - const name = helpers.getStringValue( - argv.name, - "must provide team name", - ); + const name = helpers.getStringValue(argv.name, "must provide org name"); const store = helpers.getStringValue( argv.store, ERROR_INVALID_STORE_PATH, @@ -106,7 +103,7 @@ export const builder = function (args: Yargs) { }); const api = helpers.getApi(fileStore, apiUrl); - const result = await api.teams.newTeam.mutate({ + const result = await api.orgs.newOrg.mutate({ name, emailInvites: (invites ?? "") .split(",") @@ -119,16 +116,16 @@ export const builder = function (args: Yargs) { ) .command( "invite ", - "invite a list of emails to a team. team id must either be a command option or available in the context.", + "invite a list of emails to a org. org id must either be a command option or available in the context.", function (args) { return args .positional("emails", { type: "string", - description: "comma separated list of emails to be invited to team", + description: "comma separated list of emails to be invited to org", }) - .option("teamId", { + .option("orgId", { type: "string", - description: "id of team to add to", + description: "id of org to add to", }); }, async function (argv) { @@ -151,9 +148,9 @@ export const builder = function (args: Yargs) { } const fileStore = new FileStore(store); - const team = helpers.getStringValue( - helpers.getTeam({ teamId: argv.teamId, store: fileStore }), - "must provide teamId as arg or context", + const org = helpers.getStringValue( + helpers.getOrg({ orgId: argv.orgId, store: fileStore }), + "must provide orgId as arg or context", ); const apiUrl = helpers.getApiUrl({ @@ -164,8 +161,8 @@ export const builder = function (args: Yargs) { // check if the email address is already invited and resend; else, // invite the email address (to avoid SQLite constraint error) - const { invites } = await api.invites.invitesForTeam.query({ - teamId: team, + const { invites } = await api.invites.invitesForOrg.query({ + orgId: org, }); const pendingEmails = invites .filter((i) => i.invite.claimedAt == null) @@ -173,7 +170,7 @@ export const builder = function (args: Yargs) { if (pendingEmails.length > 0) { for (const i of invites) { await api.invites.resendInvite.mutate({ - teamId: team, + orgId: org, inviteId: i.invite.id, }); } @@ -183,12 +180,12 @@ export const builder = function (args: Yargs) { ); if (newEmails.length > 0) { await api.invites.inviteEmails.mutate({ - teamId: team, + orgId: org, emails: newEmails, }); } - logger.log(JSON.stringify({ emails: emailInvites, teamId: team })); + logger.log(JSON.stringify({ emails: emailInvites, orgId: org })); }, ); }; @@ -197,6 +194,6 @@ export const builder = function (args: Yargs) { export const handler = async ( argv: Arguments, ): Promise => { - // (args: ArgumentsCamelCase & { name: string | undefined; } & { personalTeamId: string; } & { invites: string; } & { team: string | undefined; } & { user: string | undefined; }>) => void + // (args: ArgumentsCamelCase & { name: string | undefined; } & { personalOrgId: string; } & { invites: string; } & { org: string | undefined; } & { user: string | undefined; }>) => void // noop }; diff --git a/packages/cli/src/commands/project.ts b/packages/cli/src/commands/project.ts index bd834111..7554dc5e 100644 --- a/packages/cli/src/commands/project.ts +++ b/packages/cli/src/commands/project.ts @@ -19,29 +19,29 @@ export const command = "project "; export const desc = "manage studio projects"; export interface CommandOptions extends GlobalOptions { - teamId?: string; + orgId?: string; name?: string; description?: string; - team?: string; + org?: string; user?: string; - personalTeamId?: string; + personalOrgId?: string; invites?: string; } export const builder = function (args: Yargs) { return args .command( - "ls [teamId]", - "list the projects for the given team id, or if no id is given, for currently logged in user's personal team", + "ls [orgId]", + "list the projects for the given org id, or if no id is given, for currently logged in user's personal org", function (args) { - return args.positional("teamId", { + return args.positional("orgId", { type: "string", - description: "optional team id", + description: "optional org id", }); }, async function (argv) { try { - const { teamId } = argv; + const { orgId } = argv; const store = helpers.getStringValue( argv.store, @@ -56,10 +56,10 @@ export const builder = function (args: Yargs) { const api = helpers.getApi(fileStore, apiUrl); const query = - typeof teamId === "string" && teamId.trim() !== "" - ? { teamId } + typeof orgId === "string" && orgId.trim() !== "" + ? { orgId } : undefined; - const projects = await api.projects.teamProjects.query(query); + const projects = await api.projects.orgProjects.query(query); const projectsWithDefs = []; @@ -89,9 +89,9 @@ export const builder = function (args: Yargs) { type: "string", description: "the project description", }) - .option("teamId", { + .option("orgId", { type: "string", - description: "the team id associated with the project", + description: "the org id associated with the project", }) as yargs.Argv; }, async function (argv: CommandOptions) { @@ -115,16 +115,16 @@ export const builder = function (args: Yargs) { store: fileStore, }); const api = helpers.getApi(fileStore, apiUrl); - const teamId = helpers.getStringValue( - helpers.getTeam({ + const orgId = helpers.getStringValue( + helpers.getOrg({ store: fileStore, - teamId: argv.teamId, + orgId: argv.orgId, }), - "must provide team for project", + "must provide org for project", ); const result = await api.projects.newProject.mutate({ - teamId, + orgId, name, description, // TODO: Allow user to specify env names @@ -143,6 +143,6 @@ export const builder = function (args: Yargs) { export const handler = async ( argv: Arguments, ): Promise => { - // (args: ArgumentsCamelCase & { name: string | undefined; } & { personalTeamId: string; } & { invites: string; } & { team: string | undefined; } & { user: string | undefined; }>) => void + // (args: ArgumentsCamelCase & { name: string | undefined; } & { personalOrgId: string; } & { invites: string; } & { org: string | undefined; } & { user: string | undefined; }>) => void // noop }; diff --git a/packages/cli/src/commands/status.ts b/packages/cli/src/commands/status.ts index d75b5b20..52257b51 100644 --- a/packages/cli/src/commands/status.ts +++ b/packages/cli/src/commands/status.ts @@ -26,7 +26,7 @@ export const handler = async ( `logged in as: ${JSON.stringify(user, null, 4)} context: ${JSON.stringify( { - team: fileStore.get("teamId") ?? notSet, + org: fileStore.get("orgId") ?? notSet, project: fileStore.get("projectId") ?? notSet, api: fileStore.get("apiUrl") ?? notSet, chain: fileStore.get("chain") ?? notSet, diff --git a/packages/cli/src/commands/unuse.ts b/packages/cli/src/commands/unuse.ts index 13cb0fdf..c72353a2 100644 --- a/packages/cli/src/commands/unuse.ts +++ b/packages/cli/src/commands/unuse.ts @@ -20,8 +20,8 @@ export const handler = async ( const fileStore = new FileStore(store); switch (context) { - case "team": - fileStore.remove("teamId"); + case "org": + fileStore.remove("orgId"); fileStore.save(); break; case "project": diff --git a/packages/cli/src/commands/use.ts b/packages/cli/src/commands/use.ts index 81dd923a..1e098e7d 100644 --- a/packages/cli/src/commands/use.ts +++ b/packages/cli/src/commands/use.ts @@ -10,7 +10,7 @@ import { // note: abnormal spacing is needed to ensure help message is formatted correctly export const command = "use [context] [id]"; export const desc = - "use the given context id for all ensuing commands. context can be one of (api, chain, team, project, or provider). "; + "use the given context id for all ensuing commands. context can be one of (api, chain, org, project, or provider). "; export const handler = async ( argv: Arguments, @@ -22,9 +22,9 @@ export const handler = async ( const fileStore = new FileStore(store); switch (context) { - case "team": - if (!helpers.isUUID(id)) throw new Error("invalid team id"); - fileStore.set("teamId", id); + case "org": + if (!helpers.isUUID(id)) throw new Error("invalid org id"); + fileStore.set("orgId", id); fileStore.save(); break; case "project": diff --git a/packages/cli/src/utils.ts b/packages/cli/src/utils.ts index ab296bfc..518ded90 100644 --- a/packages/cli/src/utils.ts +++ b/packages/cli/src/utils.ts @@ -472,12 +472,12 @@ export const helpers = { getTableIdFromTableName: function (tableName: string) { return getIdFromTableName(tableName, 1); }, - getTeam: function (argv: { store: FileStore; teamId?: unknown }) { - if (typeof argv.teamId === "string" && argv.teamId.trim() !== "") { - return argv.teamId.trim(); + getOrg: function (argv: { store: FileStore; orgId?: unknown }) { + if (typeof argv.orgId === "string" && argv.orgId.trim() !== "") { + return argv.orgId.trim(); } - return argv.store.get("teamId"); + return argv.store.get("orgId"); }, getUUID: function (arg: unknown, errorMessage: string) { if (!this.isUUID(arg)) { diff --git a/packages/cli/test/project.test.ts b/packages/cli/test/project.test.ts index a0650898..c716eb1b 100644 --- a/packages/cli/test/project.test.ts +++ b/packages/cli/test/project.test.ts @@ -12,7 +12,7 @@ import { TEST_TIMEOUT_FACTOR, TEST_API_BASE_URL, TEST_REGISTRY_PORT, - TEST_TEAM_ID, + TEST_ORG_ID, } from "./utils"; const _dirname = path.dirname(fileURLToPath(import.meta.url)); @@ -55,8 +55,8 @@ describe("commands/project", function () { "create", projectName, description, - "--teamId", - TEST_TEAM_ID, + "--orgId", + TEST_ORG_ID, ...defaultArgs, ]) .command(mod) @@ -81,7 +81,7 @@ describe("commands/project", function () { test("can list projects", async function () { const consoleLog = spy(logger, "log"); - await yargs(["project", "ls", TEST_TEAM_ID, ...defaultArgs]) + await yargs(["project", "ls", TEST_ORG_ID, ...defaultArgs]) .command(mod) .parse(); diff --git a/packages/cli/test/setup.ts b/packages/cli/test/setup.ts index 28f356c3..80d4853f 100644 --- a/packages/cli/test/setup.ts +++ b/packages/cli/test/setup.ts @@ -92,7 +92,7 @@ async function deployStudioTables(db: Database) { } async function populateStudioTestData(db: Database) { - // TODO: setup an account, a project, a team, and some tables + // TODO: setup an account, a project, a org, and some tables console.log("inserting test data into studio tables"); const tableSetupFilepath = path.join( diff --git a/packages/cli/test/status.test.ts b/packages/cli/test/status.test.ts index 5664d919..1a08be6a 100644 --- a/packages/cli/test/status.test.ts +++ b/packages/cli/test/status.test.ts @@ -15,7 +15,7 @@ import { TEST_API_BASE_URL, TEST_PROJECT_ID, TEST_REGISTRY_PORT, - TEST_TEAM_ID, + TEST_ORG_ID, TEST_TIMEOUT_FACTOR, } from "./utils"; @@ -75,7 +75,7 @@ describe("commands/status", function () { }); test("status command can list session status", async function () { - await yargs(["use", "team", TEST_TEAM_ID, ...defaultArgs]) + await yargs(["use", "org", TEST_ORG_ID, ...defaultArgs]) .command(modUse) .parse(); @@ -100,18 +100,18 @@ describe("commands/status", function () { ); equal(user.user.address, "0xBcd4042DE499D14e55001CcbB24a551F3b954096"); - equal(user.user.teamId, TEST_TEAM_ID); - equal(user.personalTeam.id, TEST_TEAM_ID); - equal(user.personalTeam.name, "testuser"); - equal(user.personalTeam.slug, "testuser"); - equal(user.personalTeam.personal, 1); + equal(user.user.orgId, TEST_ORG_ID); + equal(user.personalOrg.id, TEST_ORG_ID); + equal(user.personalOrg.name, "testuser"); + equal(user.personalOrg.slug, "testuser"); + equal(user.personalOrg.personal, 1); const context = JSON.parse( // eslint-disable-next-line @typescript-eslint/restrict-plus-operands res.slice(res.indexOf("context: ") + 9).replace(/\n/g, ""), ); - equal(context.team, "a3cd7fac-4528-4765-9ae1-304460555429"); + equal(context.org, "a3cd7fac-4528-4765-9ae1-304460555429"); equal(context.project, "2f403473-de7b-41ba-8d97-12a0344aeccb"); equal(context.api, "http://localhost:2999"); equal(context.chain, "undefined"); diff --git a/packages/cli/test/team.test.ts b/packages/cli/test/team.test.ts index 7bd43825..4a578ac4 100644 --- a/packages/cli/test/team.test.ts +++ b/packages/cli/test/team.test.ts @@ -6,8 +6,8 @@ import { afterEach, before, describe, test } from "mocha"; import { restore, spy } from "sinon"; import yargs from "yargs/yargs"; import { type GlobalOptions } from "../src/cli.js"; -import * as mod from "../src/commands/team.js"; -import type { CommandOptions } from "../src/commands/team.js"; +import * as mod from "../src/commands/org.js"; +import type { CommandOptions } from "../src/commands/org.js"; import * as modLogin from "../src/commands/login.js"; import * as modLogout from "../src/commands/logout.js"; import { logger, wait } from "../src/utils.js"; @@ -15,7 +15,7 @@ import { TEST_TIMEOUT_FACTOR, TEST_API_BASE_URL, TEST_REGISTRY_PORT, -} from "./utils"; +} from "./utils.js"; const _dirname = path.dirname(fileURLToPath(import.meta.url)); const accounts = getAccounts(); @@ -33,7 +33,7 @@ const defaultArgs = [ TEST_API_BASE_URL, ]; -describe("commands/team", function () { +describe("commands/org", function () { this.timeout(15000 * TEST_TIMEOUT_FACTOR); before(async function () { @@ -59,13 +59,13 @@ describe("commands/team", function () { }); // TODO: all the tests depend on previous tests, need to fix this - const teamName = "testuser"; + const orgName = "testuser"; const projectDescription = "testing project create"; const projectName = "projectfoo"; - test("can list authenticated user's teams", async function () { + test("can list authenticated user's orgs", async function () { const consoleLog = spy(logger, "log"); - await yargs(["team", "ls", ...defaultArgs]) + await yargs(["org", "ls", ...defaultArgs]) .command(mod) .parse(); @@ -73,8 +73,8 @@ describe("commands/team", function () { const data = JSON.parse(output); equal(data.length, 1); - const team = data[0]; - const idParts = team.id.split("-"); + const org = data[0]; + const idParts = org.id.split("-"); equal(idParts.length, 5); equal(idParts[0].length, 8); equal(idParts[1].length, 4); @@ -82,19 +82,19 @@ describe("commands/team", function () { equal(idParts[3].length, 4); equal(idParts[4].length, 12); - equal(team.name, teamName); - equal(team.slug, teamName); + equal(org.name, orgName); + equal(org.slug, orgName); - equal(team.projects.length, 2); - const project = team.projects[0]; + equal(org.projects.length, 2); + const project = org.projects[0]; equal(project.name, projectName); equal(project.description, projectDescription); }); - test("can list teams for a specific user", async function () { + test("can list orgs for a specific user", async function () { const consoleLog = spy(logger, "log"); const userAddress = "0xBcd4042DE499D14e55001CcbB24a551F3b954096"; - await yargs(["team", "ls", userAddress, ...defaultArgs]) + await yargs(["org", "ls", userAddress, ...defaultArgs]) .command(mod) .parse(); @@ -102,8 +102,8 @@ describe("commands/team", function () { const data = JSON.parse(output); equal(data.length, 1); - const team = data[0]; - const idParts = team.id.split("-"); + const org = data[0]; + const idParts = org.id.split("-"); equal(idParts.length, 5); equal(idParts[0].length, 8); equal(idParts[1].length, 4); @@ -111,26 +111,26 @@ describe("commands/team", function () { equal(idParts[3].length, 4); equal(idParts[4].length, 12); - equal(team.name, teamName); - equal(team.slug, teamName); + equal(org.name, orgName); + equal(org.slug, orgName); - equal(team.projects.length, 2); - const project = team.projects[0]; + equal(org.projects.length, 2); + const project = org.projects[0]; equal(project.name, projectName); equal(project.description, projectDescription); }); - test("can create a team", async function () { + test("can create a org", async function () { const consoleLog = spy(logger, "log"); - const teamName = "mynewteam"; - await yargs(["team", "create", teamName, ...defaultArgs]) + const orgName = "myneworg"; + await yargs(["org", "create", orgName, ...defaultArgs]) .command(mod) .parse(); const output = consoleLog.getCall(0).firstArg; - const team = JSON.parse(output); + const org = JSON.parse(output); - const idParts = team.id.split("-"); + const idParts = org.id.split("-"); equal(idParts.length, 5); equal(idParts[0].length, 8); equal(idParts[1].length, 4); @@ -138,8 +138,8 @@ describe("commands/team", function () { equal(idParts[3].length, 4); equal(idParts[4].length, 12); - equal(team.name, teamName); - equal(team.slug, teamName); + equal(org.name, orgName); + equal(org.slug, orgName); }); // TODO: fix this test @@ -154,7 +154,7 @@ describe("commands/team", function () { // ``` // But, if you comment out the stub, the test still fails due to an error with // the `mail` package——see `mail/index.ts` for more details. - // test("can invite a user to a team", async function () { + // test("can invite a user to a org", async function () { // const consoleLog = spy(logger, "log"); // // const mutateStub = stub().returns({ message: "spy success" }); // // stub(helpers, "getApi").callsFake(function ( @@ -171,11 +171,11 @@ describe("commands/team", function () { // // }); // await yargs([ - // "team", + // "org", // "invite", // "test@textile.io,test2@textile.io", - // "--teamId", - // TEST_TEAM_ID, + // "--orgId", + // TEST_ORG_ID, // ...defaultArgs, // ]) // .command(mod) @@ -187,7 +187,7 @@ describe("commands/team", function () { // equal(response.message, "spy success"); // // deepStrictEqual(mutateStub.firstCall.args[0], { // // emails: ["test@textile.io", "test2@textile.io"], - // // teamId: TEST_TEAM_ID, + // // orgId: TEST_ORG_ID, // // }); // }); }); diff --git a/packages/cli/test/unuse.test.ts b/packages/cli/test/unuse.test.ts index 80b79e47..d23d9459 100644 --- a/packages/cli/test/unuse.test.ts +++ b/packages/cli/test/unuse.test.ts @@ -94,12 +94,12 @@ describe("commands/unuse", function () { equal(getSession().projectId, undefined); }); - test("unuse command can clear team id", async function () { - const teamId = "01a2d24d-3805-4a14-8059-7041f8b69aac"; + test("unuse command can clear org id", async function () { + const orgId = "01a2d24d-3805-4a14-8059-7041f8b69aac"; await yargs([ "use", - "team", - teamId, + "org", + orgId, ...defaultArgs, "--privateKey", accounts[10].privateKey.slice(2), @@ -107,11 +107,11 @@ describe("commands/unuse", function () { .command(modUse) .parse(); - equal(getSession().teamId, teamId); + equal(getSession().orgId, orgId); await yargs([ "unuse", - "team", + "org", ...defaultArgs, "--privateKey", accounts[10].privateKey.slice(2), @@ -119,7 +119,7 @@ describe("commands/unuse", function () { .command(modUnuse) .parse(); - equal(getSession().teamId, undefined); + equal(getSession().orgId, undefined); }); test("unuse command can clear api url", async function () { diff --git a/packages/cli/test/use.test.ts b/packages/cli/test/use.test.ts index 0a00457b..9f1eda7a 100644 --- a/packages/cli/test/use.test.ts +++ b/packages/cli/test/use.test.ts @@ -11,7 +11,7 @@ import * as modUse from "../src/commands/use.js"; import * as modUnuse from "../src/commands/unuse.js"; import * as modLogin from "../src/commands/login.js"; import * as modLogout from "../src/commands/logout.js"; -import * as modTeam from "../src/commands/team.js"; +import * as modOrg from "../src/commands/org.js"; import { logger, wait } from "../src/utils.js"; import { TEST_TIMEOUT_FACTOR, @@ -77,13 +77,13 @@ describe("commands/use", function () { equal(err.message, "invalid project id"); }); - test("use command throws if team id is not valid", async function () { + test("use command throws if org id is not valid", async function () { const consoleError = spy(logger, "error"); await yargs([ "use", - "team", - "invalidteamid", + "org", + "invalidorgid", ...defaultArgs, "--privateKey", accounts[10].privateKey.slice(2), @@ -92,10 +92,10 @@ describe("commands/use", function () { .parse(); const err = consoleError.getCall(0).firstArg; - equal(err.message, "invalid team id"); + equal(err.message, "invalid org id"); }); - test("use command sets teamId for project command", async function () { + test("use command sets orgId for project command", async function () { await yargs([ "login", ...defaultArgs, @@ -108,24 +108,24 @@ describe("commands/use", function () { const consoleLog = spy(logger, "log"); await yargs([ - "team", + "org", "ls", ...defaultArgs, "--privateKey", accounts[10].privateKey.slice(2), ]) - .command(modTeam) + .command(modOrg) .parse(); - const teamStr = consoleLog.getCall(0).firstArg; - const teamId = JSON.parse(teamStr)[0].id; + const orgStr = consoleLog.getCall(0).firstArg; + const orgId = JSON.parse(orgStr)[0].id; - equal(typeof teamId, "string"); + equal(typeof orgId, "string"); await yargs([ "use", - "team", - teamId, + "org", + orgId, ...defaultArgs, "--privateKey", accounts[10].privateKey.slice(2), @@ -136,11 +136,11 @@ describe("commands/use", function () { equal( consoleLog.getCall(1).firstArg, // typescript linting doesn't honor the assertion of the runtime type here, so we need to cast - `your team context has been set to: ${teamId as string}`, + `your org context has been set to: ${orgId as string}`, ); const session = getSession(); - equal(session.teamId, teamId); + equal(session.orgId, orgId); }); test("use command can set chain", async function () { @@ -197,11 +197,11 @@ describe("commands/use", function () { .command(modLogin) .parse(); - const teamId = "01a2d24d-3805-4a14-8059-7041f8b69aac"; + const orgId = "01a2d24d-3805-4a14-8059-7041f8b69aac"; await yargs([ "use", - "team", - teamId, + "org", + orgId, ...defaultArgs, "--privateKey", accounts[10].privateKey.slice(2), @@ -209,11 +209,11 @@ describe("commands/use", function () { .command(modUse) .parse(); - equal(getSession().teamId, teamId); + equal(getSession().orgId, orgId); await yargs([ "unuse", - "team", + "org", ...defaultArgs, "--privateKey", accounts[10].privateKey.slice(2), @@ -221,6 +221,6 @@ describe("commands/use", function () { .command(modUnuse) .parse(); - equal(getSession().teamId, undefined); + equal(getSession().orgId, undefined); }); }); diff --git a/packages/cli/test/utils.ts b/packages/cli/test/utils.ts index 4327f774..fa1dbc6d 100644 --- a/packages/cli/test/utils.ts +++ b/packages/cli/test/utils.ts @@ -15,7 +15,7 @@ export const TEST_API_BASE_URL = `http://localhost:${TEST_API_PORT}`; export const TEST_REGISTRY_PORT = 8546; export const TEST_VALIDATOR_URL = "http://localhost:8081/api/v1"; -export const TEST_TEAM_ID = "a3cd7fac-4528-4765-9ae1-304460555429"; +export const TEST_ORG_ID = "a3cd7fac-4528-4765-9ae1-304460555429"; export const TEST_PROJECT_ID = "2f403473-de7b-41ba-8d97-12a0344aeccb"; export const isUUID = function (value: string) { diff --git a/packages/client/README.md b/packages/client/README.md index f56acfd8..c03ff4ab 100644 --- a/packages/client/README.md +++ b/packages/client/README.md @@ -37,7 +37,7 @@ const studioRpc = api({ url: "http://localhost:3000" }): -const projects = await studioRpc.projects.teamProjects.query({ teamId: "123abc" }); +const projects = await studioRpc.projects.orgProjects.query({ orgId: "123abc" }); ``` diff --git a/packages/mail/readme.md b/packages/mail/readme.md index e87334a9..17fd7aaa 100644 --- a/packages/mail/readme.md +++ b/packages/mail/readme.md @@ -30,7 +30,7 @@ await mailApi.sendInvite( "email@example.com", // User's email "https://example.com/image", // Link to invite image "inviter-username", // Username of who is inviting - "team-name", // Team name string + "org-name", // Org name string "https://example.com/accept", // Link to accept invite ); ``` diff --git a/packages/mail/src/emails/invite.tsx b/packages/mail/src/emails/invite.tsx index 0fce19fe..c9f385c3 100644 --- a/packages/mail/src/emails/invite.tsx +++ b/packages/mail/src/emails/invite.tsx @@ -19,17 +19,17 @@ const { export interface InviteProps { imageLink: string; inviterUsername: string; - teamName: string; + orgName: string; link: string; } export const InviteUserEmail = ({ imageLink = "https://studio.tableland.xyz/mesa.jpg", inviterUsername = "user1", - teamName = "Team", + orgName = "Org", link = "https://example.com", }: InviteProps) => { - const previewText = `Join ${teamName} on Tableland Studio`; + const previewText = `Join ${orgName} on Tableland Studio`; return ( @@ -48,7 +48,7 @@ export const InviteUserEmail = ({ /> - Join {teamName} on{" "} + Join {orgName} on{" "} Tableland Studio @@ -56,7 +56,7 @@ export const InviteUserEmail = ({ User {inviterUsername} has invited you to the{" "} - {teamName} team on{" "} + {orgName} org on{" "} Tableland Studio.
diff --git a/packages/mail/src/index.ts b/packages/mail/src/index.ts index 455fb938..911e1285 100644 --- a/packages/mail/src/index.ts +++ b/packages/mail/src/index.ts @@ -9,10 +9,10 @@ export function initMailApi(apiKey?: string) { to: string, imageLink: string, inviterUsername: string, - teamName: string, + orgName: string, link: string, ) => { - // TODO: error occurs in `render` or `Invite` function via `team.test.ts`: + // TODO: error occurs in `render` or `Invite` function via `org.test.ts`: // ``` // Element type is invalid: expected a string (for built-in components) // or a class/function (for composite components) but got: undefined. You @@ -20,12 +20,12 @@ export function initMailApi(apiKey?: string) { // or you might have mixed up default and named imports. // ``` const emailHtml = render( - Invite({ imageLink, inviterUsername, teamName, link }), + Invite({ imageLink, inviterUsername, orgName, link }), ); const res = await client.sendEmail({ From: "noreply@tableland.xyz", To: to, - Subject: "You've been invited to a team on Tableland Studio", + Subject: "You've been invited to an org on Tableland Studio", HtmlBody: emailHtml, }); return res; diff --git a/packages/store/src/api/auth.ts b/packages/store/src/api/auth.ts index 67823457..61a5cf52 100644 --- a/packages/store/src/api/auth.ts +++ b/packages/store/src/api/auth.ts @@ -4,7 +4,7 @@ import { eq } from "drizzle-orm"; import { type DrizzleD1Database } from "drizzle-orm/d1"; import { sealData, unsealData } from "iron-session"; import type * as schema from "../schema/index.js"; -import { teamMemberships, teams, users } from "../schema/index.js"; +import { orgMemberships, orgs, users } from "../schema/index.js"; import { slugify } from "../helpers.js"; export function auth( @@ -12,14 +12,14 @@ export function auth( tbl: Database, dataSealPass: string, ) { - async function userAndPersonalTeamByAddress(address: string) { + async function userAndPersonalOrgByAddress(address: string) { const res = await db .select({ user: users, - personalTeam: teams, + personalOrg: orgs, }) .from(users) - .innerJoin(teams, eq(users.teamId, teams.id)) + .innerJoin(orgs, eq(users.orgId, orgs.id)) .where(eq(users.address, address)) .get(); if (!res) { @@ -34,58 +34,58 @@ export function auth( ...rest, email, }, - personalTeam: res.personalTeam, + personalOrg: res.personalOrg, }; } return { - createUserAndPersonalTeam: async function ( + createUserAndPersonalOrg: async function ( address: string, - teamName: string, + orgName: string, email?: string, ) { const now = new Date().toISOString(); - const teamId = randomUUID(); + const orgId = randomUUID(); const sealed = await sealData( { email }, { password: dataSealPass, ttl: 0 }, ); const { sql: usersSql, params: usersParams } = db .insert(users) - .values({ address, teamId, sealed }) + .values({ address, orgId, sealed }) .toSQL(); - const { sql: teamsSql, params: teamsParams } = db - .insert(teams) + const { sql: orgsSql, params: orgsParams } = db + .insert(orgs) .values({ - id: teamId, + id: orgId, personal: 1, - name: teamName, - slug: slugify(teamName), + name: orgName, + slug: slugify(orgName), createdAt: now, updatedAt: now, }) .toSQL(); - const { sql: teamMembershipsSql, params: teamMembershipsParams } = db - .insert(teamMemberships) + const { sql: orgMembershipsSql, params: orgMembershipsParams } = db + .insert(orgMemberships) .values({ - memberTeamId: teamId, - teamId, + memberOrgId: orgId, + orgId, isOwner: 1, - joinedAt: new Date().toISOString(), + joinedAt: now, }) .toSQL(); await tbl.batch([ tbl.prepare(usersSql).bind(usersParams), - tbl.prepare(teamsSql).bind(teamsParams), - tbl.prepare(teamMembershipsSql).bind(teamMembershipsParams), + tbl.prepare(orgsSql).bind(orgsParams), + tbl.prepare(orgMembershipsSql).bind(orgMembershipsParams), ]); - const info = await userAndPersonalTeamByAddress(address); + const info = await userAndPersonalOrgByAddress(address); if (!info) { - throw new Error("Failed to create user and personal team."); + throw new Error("Failed to create user and personal org."); } return info; }, - userAndPersonalTeamByAddress, + userAndPersonalOrgByAddress, }; } diff --git a/packages/store/src/api/defs.ts b/packages/store/src/api/defs.ts index d6ed58fa..b7738eeb 100644 --- a/packages/store/src/api/defs.ts +++ b/packages/store/src/api/defs.ts @@ -9,8 +9,8 @@ import { slugify } from "../helpers.js"; type Def = schema.Def; const projectDefs = schema.projectDefs; const defs = schema.defs; -const teamProjects = schema.teamProjects; -const teams = schema.teams; +const orgProjects = schema.orgProjects; +const orgs = schema.orgs; const deployments = schema.deployments; export function initDefs(db: DrizzleD1Database, tbl: Database) { @@ -148,20 +148,20 @@ export function initDefs(db: DrizzleD1Database, tbl: Database) { return res?.defs; }, - defTeam: async function (defId: string) { + defOrg: async function (defId: string) { const res = await db - .select({ teams }) + .select({ orgs }) .from(defs) .innerJoin(projectDefs, eq(defs.id, projectDefs.defId)) .innerJoin( - teamProjects, - eq(projectDefs.projectId, teamProjects.projectId), + orgProjects, + eq(projectDefs.projectId, orgProjects.projectId), ) - .innerJoin(teams, eq(teamProjects.teamId, teams.id)) + .innerJoin(orgs, eq(orgProjects.orgId, orgs.id)) .where(eq(defs.id, defId)) .orderBy(defs.name) .get(); - return res?.teams; + return res?.orgs; }, }; } diff --git a/packages/store/src/api/deployments.ts b/packages/store/src/api/deployments.ts index 48ff18ec..e86578e6 100644 --- a/packages/store/src/api/deployments.ts +++ b/packages/store/src/api/deployments.ts @@ -7,8 +7,8 @@ const environments = schema.environments; const projectDefs = schema.projectDefs; const projects = schema.projects; const defs = schema.defs; -const teamProjects = schema.teamProjects; -const teams = schema.teams; +const orgProjects = schema.orgProjects; +const orgs = schema.orgs; export function initDeployments(db: DrizzleD1Database) { return { @@ -107,7 +107,7 @@ export function initDeployments(db: DrizzleD1Database) { deploymentReferences: async function (chainId: number, tableId: string) { const res = await db .select({ - team: teams, + org: orgs, project: projects, def: defs, environment: environments, @@ -118,8 +118,8 @@ export function initDeployments(db: DrizzleD1Database) { .innerJoin(defs, eq(projectDefs.defId, defs.id)) .innerJoin(projects, eq(projectDefs.projectId, projects.id)) .innerJoin(environments, eq(deployments.environmentId, environments.id)) - .innerJoin(teamProjects, eq(projects.id, teamProjects.projectId)) - .innerJoin(teams, eq(teamProjects.teamId, teams.id)) + .innerJoin(orgProjects, eq(projects.id, orgProjects.projectId)) + .innerJoin(orgs, eq(orgProjects.orgId, orgs.id)) .where( and( eq(deployments.chainId, chainId), diff --git a/packages/store/src/api/environments.ts b/packages/store/src/api/environments.ts index 1b5d773d..16cbf896 100644 --- a/packages/store/src/api/environments.ts +++ b/packages/store/src/api/environments.ts @@ -9,8 +9,8 @@ type Environment = schema.Environment; const environments = schema.environments; const deployments = schema.deployments; const projects = schema.projects; -const teamProjects = schema.teamProjects; -const teams = schema.teams; +const orgProjects = schema.orgProjects; +const orgs = schema.orgs; export function initEnvironments( db: DrizzleD1Database, @@ -93,13 +93,13 @@ export function initEnvironments( await tbl.batch(batch); }, - environmentTeamAndProject: async function (id: string) { + environmentOrgAndProject: async function (id: string) { const res = await db - .select({ team: teams, project: projects }) + .select({ org: orgs, project: projects }) .from(environments) .innerJoin(projects, eq(environments.projectId, projects.id)) - .innerJoin(teamProjects, eq(projects.id, teamProjects.projectId)) - .innerJoin(teams, eq(teamProjects.teamId, teams.id)) + .innerJoin(orgProjects, eq(projects.id, orgProjects.projectId)) + .innerJoin(orgs, eq(orgProjects.orgId, orgs.id)) .where(eq(environments.id, id)) .get(); return res; diff --git a/packages/store/src/api/index.ts b/packages/store/src/api/index.ts index 4a4bc993..b597d807 100644 --- a/packages/store/src/api/index.ts +++ b/packages/store/src/api/index.ts @@ -7,7 +7,7 @@ import { initEnvironments } from "./environments.js"; import { invites } from "./invites.js"; import { initProjects } from "./projects.js"; import { initDefs } from "./defs.js"; -import { initTeams } from "./teams.js"; +import { initOrgs } from "./orgs.js"; import { initUsers } from "./users.js"; export function init(tbl: Database, dataSealPass: string) { @@ -17,7 +17,7 @@ export function init(tbl: Database, dataSealPass: string) { invites: invites(db, tbl, dataSealPass), projects: initProjects(db, tbl), defs: initDefs(db, tbl), - teams: initTeams(db, tbl, dataSealPass), + orgs: initOrgs(db, tbl, dataSealPass), environments: initEnvironments(db, tbl), deployments: initDeployments(db), users: initUsers(db), diff --git a/packages/store/src/api/invites.ts b/packages/store/src/api/invites.ts index a074d451..d7be192d 100644 --- a/packages/store/src/api/invites.ts +++ b/packages/store/src/api/invites.ts @@ -6,12 +6,12 @@ import { alias } from "drizzle-orm/sqlite-core"; import { sealData, unsealData } from "iron-session"; import * as schema from "../schema/index.js"; -type NewTeamInviteSealed = schema.NewTeamInviteSealed; -type Team = schema.Team; -type TeamInvite = schema.TeamInvite; -const teamInvites = schema.teamInvites; -const teamMemberships = schema.teamMemberships; -const teams = schema.teams; +type NewOrgInviteSealed = schema.NewOrgInviteSealed; +type Org = schema.Org; +type OrgInvite = schema.OrgInvite; +const orgInvites = schema.orgInvites; +const orgMemberships = schema.orgMemberships; +const orgs = schema.orgs; export function invites( db: DrizzleD1Database, @@ -19,25 +19,25 @@ export function invites( dataSealPass: string, ) { return { - inviteEmailsToTeam: async function ( - teamId: string, - inviterTeamId: string, + inviteEmailsToOrg: async function ( + orgId: string, + inviterOrgId: string, emails: string[], ) { - const invites: TeamInvite[] = emails.map((email) => ({ - // Assure we don't allow duplicate invites per team/email. This is necessary + const invites: OrgInvite[] = emails.map((email) => ({ + // Assure we don't allow duplicate invites per org/email. This is necessary // because we don't store the email address in plaintext in the database. id: createHash("sha256") - .update(teamId + email) + .update(orgId + email) .digest("hex"), - teamId, - inviterTeamId, + orgId, + inviterOrgId, email, createdAt: new Date().toISOString(), - claimedByTeamId: null, + claimedByOrgId: null, claimedAt: null, })); - const sealedInvites: NewTeamInviteSealed[] = await Promise.all( + const sealedInvites: NewOrgInviteSealed[] = await Promise.all( invites.map(async ({ email, ...rest }) => ({ ...rest, sealed: await sealData( @@ -49,15 +49,15 @@ export function invites( ), })), ); - await db.insert(teamInvites).values(sealedInvites).run(); + await db.insert(orgInvites).values(sealedInvites).run(); return invites; }, inviteById: async function (id: string) { const invite = await db .select() - .from(teamInvites) - .where(eq(teamInvites.id, id)) + .from(orgInvites) + .where(eq(orgInvites.id, id)) .get(); if (!invite) return undefined; const { sealed, ...rest } = invite; @@ -70,20 +70,20 @@ export function invites( }; }, - acceptInvite: async function (invite: TeamInvite, personalTeam: Team) { + acceptInvite: async function (invite: OrgInvite, personalOrg: Org) { const { sql: invitesSql, params: invitesParams } = db - .update(teamInvites) + .update(orgInvites) .set({ - claimedByTeamId: personalTeam.id, + claimedByOrgId: personalOrg.id, claimedAt: new Date().toISOString(), }) - .where(eq(teamInvites.id, invite.id)) + .where(eq(orgInvites.id, invite.id)) .toSQL(); const { sql: membershipsSql, params: membershipsParams } = db - .insert(teamMemberships) + .insert(orgMemberships) .values({ - teamId: invite.teamId, - memberTeamId: personalTeam.id, + orgId: invite.orgId, + memberOrgId: personalOrg.id, isOwner: 0, joinedAt: new Date().toISOString(), }) @@ -95,25 +95,25 @@ export function invites( }, deleteInvite: async function (id: string) { - await db.delete(teamInvites).where(eq(teamInvites.id, id)).run(); + await db.delete(orgInvites).where(eq(orgInvites.id, id)).run(); }, - invitesForTeam: async function invitesForTeam(teamId: string) { - const claimedByTeams = alias(teams, "claimed_by_teams"); + invitesForOrg: async function invitesForOrg(orgId: string) { + const claimedByOrgs = alias(orgs, "claimed_by_orgs"); const invitesSealed = await db .select({ - inviter: teams, - invite: teamInvites, - claimedBy: claimedByTeams, + inviter: orgs, + invite: orgInvites, + claimedBy: claimedByOrgs, }) - .from(teamInvites) - .innerJoin(teams, eq(teamInvites.inviterTeamId, teams.id)) + .from(orgInvites) + .innerJoin(orgs, eq(orgInvites.inviterOrgId, orgs.id)) .leftJoin( - claimedByTeams, - eq(teamInvites.claimedByTeamId, claimedByTeams.id), + claimedByOrgs, + eq(orgInvites.claimedByOrgId, claimedByOrgs.id), ) - .where(eq(teamInvites.teamId, teamId)) - .orderBy(asc(teamInvites.createdAt)) + .where(eq(orgInvites.orgId, orgId)) + .orderBy(asc(orgInvites.createdAt)) .all(); const invites = await Promise.all( invitesSealed.map( diff --git a/packages/store/src/api/orgs.ts b/packages/store/src/api/orgs.ts new file mode 100644 index 00000000..f5789b42 --- /dev/null +++ b/packages/store/src/api/orgs.ts @@ -0,0 +1,370 @@ +import { randomUUID } from "crypto"; +import { type Database } from "@tableland/sdk"; +import { and, asc, eq, ne, inArray } from "drizzle-orm"; +import { type DrizzleD1Database } from "drizzle-orm/d1"; +import { sealData } from "iron-session"; +import * as schema from "../schema/index.js"; +import { slugify } from "../helpers.js"; + +type NewOrgInviteSealed = schema.NewOrgInviteSealed; +type Org = schema.Org; +type OrgInvite = schema.OrgInvite; +const projects = schema.projects; +const orgInvites = schema.orgInvites; +const orgMemberships = schema.orgMemberships; +const orgProjects = schema.orgProjects; +const orgs = schema.orgs; +const users = schema.users; +const projectDefs = schema.projectDefs; +const defs = schema.defs; +const environments = schema.environments; +const deployments = schema.deployments; + +export function initOrgs( + db: DrizzleD1Database, + tbl: Database, + dataSealPass: string, +) { + return { + nameAvailable: async function (name: string, orgId?: string) { + const res = await db + .select() + .from(orgs) + .where( + and( + eq(orgs.slug, slugify(name)), + orgId ? ne(orgs.id, orgId) : undefined, + ), + ) + .get(); + return !res; + }, + + createOrgByPersonalOrg: async function ( + name: string, + personalOrgId: string, + inviteEmails: string[], + ) { + const orgId = randomUUID(); + const slug = slugify(name); + const now = new Date().toISOString(); + const org: Org = { + id: orgId, + personal: 0, + name, + slug, + createdAt: now, + updatedAt: now, + }; + const { sql: orgsSql, params: orgsParams } = db + .insert(orgs) + .values(org) + .toSQL(); + const { sql: orgMembershipsSql, params: orgMembershipsParams } = db + .insert(orgMemberships) + .values({ + memberOrgId: personalOrgId, + orgId, + isOwner: 1, + joinedAt: now, + }) + .toSQL(); + const invites: OrgInvite[] = inviteEmails.map((email) => ({ + id: randomUUID(), + orgId, + inviterOrgId: personalOrgId, + email, + createdAt: now, + claimedByOrgId: null, + claimedAt: null, + })); + const batch = [ + tbl.prepare(orgsSql).bind(orgsParams), + tbl.prepare(orgMembershipsSql).bind(orgMembershipsParams), + ]; + if (invites.length) { + const sealedInvites: NewOrgInviteSealed[] = await Promise.all( + invites.map(async ({ email, ...rest }) => ({ + ...rest, + sealed: await sealData( + { email }, + { + password: dataSealPass, + ttl: 0, + }, + ), + })), + ); + const { sql: invitesSql, params: invitesParams } = db + .insert(orgInvites) + .values(sealedInvites) + .toSQL(); + batch.push(tbl.prepare(invitesSql).bind(invitesParams)); + } + await tbl.batch(batch); + return { org, invites }; + }, + + updateOrg: async function (orgId: string, name: string) { + const slug = slugify(name); + await db + .update(orgs) + .set({ name, slug, updatedAt: new Date().toISOString() }) + .where(eq(orgs.id, orgId)) + .run(); + return await db.select().from(orgs).where(eq(orgs.id, orgId)).get(); + }, + + deleteOrg: async function (orgId: string) { + // orgs + const { sql: orgsSql, params: orgsParams } = db + .delete(orgs) + .where(eq(orgs.id, orgId)) + .toSQL(); + + // orgMemberships + const { sql: orgMembershipsSql, params: orgMembershipsParams } = db + .delete(orgMemberships) + .where(eq(orgMemberships.orgId, orgId)) + .toSQL(); + + // users + const { sql: usersSql, params: usersParams } = db + .delete(users) + .where(eq(users.orgId, orgId)) + .toSQL(); + + // orgInvites + const { sql: orgInvitesSql, params: orgInvitesParams } = db + .delete(orgInvites) + .where(eq(orgInvites.orgId, orgId)) + .toSQL(); + + // orgProjects + const { sql: orgProjectsSql, params: orgProjectsParams } = db + .delete(orgProjects) + .where(eq(orgProjects.orgId, orgId)) + .toSQL(); + + const batch = [ + tbl.prepare(orgsSql).bind(orgsParams), + tbl.prepare(orgMembershipsSql).bind(orgMembershipsParams), + tbl.prepare(usersSql).bind(usersParams), + tbl.prepare(orgInvitesSql).bind(orgInvitesParams), + tbl.prepare(orgProjectsSql).bind(orgProjectsParams), + ]; + + // Get an array of project IDs for the org + const orgProjectIds = ( + await db + .select({ projectId: orgProjects.projectId }) + .from(orgProjects) + .where(eq(orgProjects.orgId, orgId)) + .all() + ).map((r) => r.projectId); + + // If the org has projects, delete them and all related data + if (orgProjectIds.length) { + // projects + const { sql: projectsSql, params: projectsParams } = db + .delete(projects) + .where(inArray(projects.id, orgProjectIds)) + .toSQL(); + + // environments + const { sql: environmentsSql, params: environmentsParams } = db + .delete(environments) + .where(inArray(environments.projectId, orgProjectIds)) + .toSQL(); + + // projectDefs + const { sql: projectDefsSql, params: projectDefsParams } = db + .delete(projectDefs) + .where(inArray(projectDefs.projectId, orgProjectIds)) + .toSQL(); + + batch.push(tbl.prepare(projectsSql).bind(projectsParams)); + batch.push(tbl.prepare(environmentsSql).bind(environmentsParams)); + batch.push(tbl.prepare(projectDefsSql).bind(projectDefsParams)); + + // Get an array of def IDs for all projects in the org + const defIds = ( + await db + .select({ defId: projectDefs.defId }) + .from(projectDefs) + .where(inArray(projectDefs.projectId, orgProjectIds)) + .all() + ).map((r) => r.defId); + + // If the org's projects have defs, delete them and all related data + if (defIds.length) { + // defs + const { sql: defsSql, params: defsParams } = db + .delete(defs) + .where(inArray(defs.id, defIds)) + .toSQL(); + + // deployments + const { sql: deploymentsSql, params: deploymentsParams } = db + .delete(deployments) + .where(inArray(deployments.defId, defIds)) + .toSQL(); + + batch.push(tbl.prepare(defsSql).bind(defsParams)); + batch.push(tbl.prepare(deploymentsSql).bind(deploymentsParams)); + } + } + + await tbl.batch(batch); + }, + + orgBySlug: async function (slug: string) { + const org = await db.select().from(orgs).where(eq(orgs.slug, slug)).get(); + return org; + }, + + orgById: async function (id: string) { + return await db.select().from(orgs).where(eq(orgs.id, id)).get(); + }, + + orgsByMemberId: async function (memberId: string) { + const res = await db + .select({ org: orgs, project: projects }) + .from(orgMemberships) + .innerJoin(orgs, eq(orgMemberships.orgId, orgs.id)) + .leftJoin(orgProjects, eq(orgProjects.orgId, orgs.id)) + .leftJoin(projects, eq(orgProjects.projectId, projects.id)) + .where(eq(orgMemberships.memberOrgId, memberId)) + .orderBy(asc(orgs.slug), asc(projects.slug)) + .all(); + + const iter = res + .reduce( + (acc, r) => { + if (!acc.has(r.org.id)) { + acc.set(r.org.id, { + ...r.org, + projects: [], + }); + } + if (r.project) { + acc.get(r.org.id)?.projects.push(r.project); + } + return acc; + }, + new Map< + string, + schema.Org & { + projects: schema.Project[]; + } + >(), + ) + .values(); + return Array.from(iter); + + // const res1 = await db.query.orgMemberships.findMany({ + // where: (orgMemberships, { eq }) => + // eq(orgMemberships.memberOrgId, memberId), + // with: { + // org: { + // with: { + // orgProjects: { + // // orderBy: (orgProjects, { asc }) => [asc(orgProjects.isOwner)], + // with: { + // project: true, + // }, + // }, + // }, + // }, + // }, + // }); + // return res1.map((orgMembership) => { + // const { + // org: { orgProjects, ...restOrg }, + // } = orgMembership; + // const projects = orgProjects.map((orgProject) => orgProject.project); + // return { + // ...restOrg, + // projects, + // }; + // }); + }, + + userOrgsForOrgId: async function (orgId: string) { + const res = await db + .select({ orgs, users, orgMemberships }) + .from(users) + .innerJoin(orgMemberships, eq(users.orgId, orgMemberships.memberOrgId)) + .innerJoin(orgs, eq(users.orgId, orgs.id)) + .where(eq(orgMemberships.orgId, orgId)) + .orderBy(orgs.name) + .all(); + return res.map((r) => ({ + address: r.users.address, + personalOrg: r.orgs, + membership: r.orgMemberships, + })); + }, + + isAuthorizedForOrg: async function (memberOrgId: string, orgId: string) { + const membership = await db + .select() + .from(orgMemberships) + .where( + and( + eq(orgMemberships.orgId, orgId), + eq(orgMemberships.memberOrgId, memberOrgId), + ), + ) + .get(); + return membership ?? false; + }, + + toggleAdmin: async function (orgId: string, memberId: string) { + const res = await db + .select({ isOwner: orgMemberships.isOwner }) + .from(orgMemberships) + .where( + and( + eq(orgMemberships.orgId, orgId), + eq(orgMemberships.memberOrgId, memberId), + ), + ) + .get(); + if (!res) { + throw new Error("Org membership not found"); + } + await db + .update(orgMemberships) + .set({ isOwner: res.isOwner ? 0 : 1 }) + .where( + and( + eq(orgMemberships.orgId, orgId), + eq(orgMemberships.memberOrgId, memberId), + ), + ) + .run(); + }, + + removeOrgMember: async function (orgId: string, memberId: string) { + const { sql: membershipsSql, params: membershipsParams } = db + .delete(orgMemberships) + .where( + and( + eq(orgMemberships.orgId, orgId), + eq(orgMemberships.memberOrgId, memberId), + ), + ) + .toSQL(); + const { sql: invitesSql, params: invitesParams } = db + .delete(orgInvites) + .where(eq(orgInvites.claimedByOrgId, memberId)) + .toSQL(); + const batch = [ + tbl.prepare(membershipsSql).bind(membershipsParams), + tbl.prepare(invitesSql).bind(invitesParams), + ]; + await tbl.batch(batch); + }, + }; +} diff --git a/packages/store/src/api/projects.ts b/packages/store/src/api/projects.ts index 6f67b767..2282369b 100644 --- a/packages/store/src/api/projects.ts +++ b/packages/store/src/api/projects.ts @@ -8,8 +8,8 @@ import { slugify } from "../helpers.js"; type Project = schema.Project; const environments = schema.environments; const projects = schema.projects; -const teamProjects = schema.teamProjects; -const teams = schema.teams; +const orgProjects = schema.orgProjects; +const orgs = schema.orgs; const defs = schema.defs; const deployments = schema.deployments; const projectDefs = schema.projectDefs; @@ -20,17 +20,17 @@ export function initProjects( ) { return { nameAvailable: async function ( - teamId: string, + orgId: string, name: string, projectId?: string, ) { const res = await db .select() .from(projects) - .innerJoin(teamProjects, eq(projects.id, teamProjects.projectId)) + .innerJoin(orgProjects, eq(projects.id, orgProjects.projectId)) .where( and( - eq(teamProjects.teamId, teamId), + eq(orgProjects.orgId, orgId), eq(projects.slug, slugify(name)), projectId ? ne(projects.id, projectId) : undefined, ), @@ -40,7 +40,7 @@ export function initProjects( }, createProject: async function ( - teamId: string, + orgId: string, name: string, description: string, envNames: string[], @@ -60,9 +60,9 @@ export function initProjects( .insert(projects) .values(project) .toSQL(); - const { sql: teamProjectsSql, params: teamProjectsParams } = db - .insert(teamProjects) - .values({ projectId, teamId, isOwner: 1 }) + const { sql: orgProjectsSql, params: orgProjectsParams } = db + .insert(orgProjects) + .values({ projectId, orgId, isOwner: 1 }) .toSQL(); const envs: schema.Environment[] = envNames.map((name) => ({ id: randomUUID(), @@ -78,7 +78,7 @@ export function initProjects( .toSQL(); await tbl.batch([ tbl.prepare(projectsSql).bind(projectsParams), - tbl.prepare(teamProjectsSql).bind(teamProjectsParams), + tbl.prepare(orgProjectsSql).bind(orgProjectsParams), tbl.prepare(envsSql).bind(envsParams), ]); return project; @@ -109,10 +109,10 @@ export function initProjects( }, deleteProject: async function (projectId: string) { - // teamProjects - const { sql: teamProjectsSql, params: teamProjectsParams } = db - .delete(teamProjects) - .where(eq(teamProjects.projectId, projectId)) + // orgProjects + const { sql: orgProjectsSql, params: orgProjectsParams } = db + .delete(orgProjects) + .where(eq(orgProjects.projectId, projectId)) .toSQL(); // projects @@ -134,13 +134,13 @@ export function initProjects( .toSQL(); const batch = [ - tbl.prepare(teamProjectsSql).bind(teamProjectsParams), + tbl.prepare(orgProjectsSql).bind(orgProjectsParams), tbl.prepare(projectsSql).bind(projectsParams), tbl.prepare(environmentsSql).bind(environmentsParams), tbl.prepare(projectDefsSql).bind(projectDefsParams), ]; - // Get an array of def IDs for all projects in the team + // Get an array of def IDs for all projects in the org const defIds = ( await db .select({ defId: projectDefs.defId }) @@ -172,105 +172,103 @@ export function initProjects( firstNProjectSlugs: async function (n: number) { const res = await db - .select({ team: teams.slug, project: projects.slug }) + .select({ org: orgs.slug, project: projects.slug }) .from(projects) - .innerJoin(teamProjects, eq(projects.id, teamProjects.projectId)) - .innerJoin(teams, eq(teamProjects.teamId, teams.id)) + .innerJoin(orgProjects, eq(projects.id, orgProjects.projectId)) + .innerJoin(orgs, eq(orgProjects.orgId, orgs.id)) .orderBy(projects.name) .limit(n) .all(); return res; }, - projectByTeamAndProjectSlugs: async function ( - teamSlug: string, + projectByOrgAndProjectSlugs: async function ( + orgSlug: string, projectSlug: string, ) { const res = await db - .select({ team: teams, project: projects }) + .select({ org: orgs, project: projects }) .from(projects) - .innerJoin(teamProjects, eq(projects.id, teamProjects.projectId)) - .innerJoin(teams, eq(teamProjects.teamId, teams.id)) - .where(and(eq(teams.slug, teamSlug), eq(projects.slug, projectSlug))) + .innerJoin(orgProjects, eq(projects.id, orgProjects.projectId)) + .innerJoin(orgs, eq(orgProjects.orgId, orgs.id)) + .where(and(eq(orgs.slug, orgSlug), eq(projects.slug, projectSlug))) .get(); return res; }, latestProjects: async function (offset: number, count: number) { const res = await db - .select({ projects, teams }) + .select({ projects, orgs }) .from(projects) - .innerJoin(teamProjects, eq(projects.id, teamProjects.projectId)) - .innerJoin(teams, eq(teamProjects.teamId, teams.id)) + .innerJoin(orgProjects, eq(projects.id, orgProjects.projectId)) + .innerJoin(orgs, eq(orgProjects.orgId, orgs.id)) .where(isNotNull(projects.createdAt)) .orderBy(desc(projects.createdAt)) .offset(offset) .limit(count) .all(); - return res.map((r) => ({ team: r.teams, project: r.projects })); + return res.map((r) => ({ org: r.orgs, project: r.projects })); }, - projectsByTeamId: async function (teamId: string) { + projectsByOrgId: async function (orgId: string) { const res = await db .select({ project: projects }) // TODO: Figure out why if we don't specify select key, projects key ends up as actual table name. - .from(teamProjects) - .innerJoin(projects, eq(teamProjects.projectId, projects.id)) - .where( - and(eq(teamProjects.teamId, teamId), eq(teamProjects.isOwner, 1)), - ) + .from(orgProjects) + .innerJoin(projects, eq(orgProjects.projectId, projects.id)) + .where(and(eq(orgProjects.orgId, orgId), eq(orgProjects.isOwner, 1))) .orderBy(projects.slug) .all(); const mapped = res.map((r) => r.project); return mapped; }, - projectByTeamIdAndSlug: async function (teamId: string, slug: string) { + projectByOrgIdAndSlug: async function (orgId: string, slug: string) { const res = await db .select({ projects }) - .from(teamProjects) - .innerJoin(projects, eq(teamProjects.projectId, projects.id)) - .where(and(eq(teamProjects.teamId, teamId), eq(projects.slug, slug))) + .from(orgProjects) + .innerJoin(projects, eq(orgProjects.projectId, projects.id)) + .where(and(eq(orgProjects.orgId, orgId), eq(projects.slug, slug))) .get(); return res?.projects; }, // TODO: Where does this belong? - projectTeamByProjectId: async function (projectId: string) { + projectOrgByProjectId: async function (projectId: string) { const res = await db - .select({ teams }) - .from(teamProjects) - .innerJoin(teams, eq(teamProjects.teamId, teams.id)) - .where(eq(teamProjects.projectId, projectId)) - .orderBy(teams.name) + .select({ orgs }) + .from(orgProjects) + .innerJoin(orgs, eq(orgProjects.orgId, orgs.id)) + .where(eq(orgProjects.projectId, projectId)) + .orderBy(orgs.name) .get(); - return res?.teams; + return res?.orgs; }, - isAuthorizedForProject: async function (teamId: string, projectId: string) { + isAuthorizedForProject: async function (orgId: string, projectId: string) { const authorized = await db .select() - .from(teamProjects) + .from(orgProjects) .where( and( - eq(teamProjects.teamId, teamId), - eq(teamProjects.projectId, projectId), + eq(orgProjects.orgId, orgId), + eq(orgProjects.projectId, projectId), ), ) .get(); return !!authorized; }, - projectTeamByEnvironmentId: async function (environmentId: string) { - const projectTeam = await db - .select({ teams }) + projectOrgByEnvironmentId: async function (environmentId: string) { + const projectOrg = await db + .select({ orgs }) .from(environments) .innerJoin(projects, eq(environments.projectId, projects.id)) - .innerJoin(teamProjects, eq(projects.id, teamProjects.projectId)) - .innerJoin(teams, eq(teamProjects.teamId, teams.id)) + .innerJoin(orgProjects, eq(projects.id, orgProjects.projectId)) + .innerJoin(orgs, eq(orgProjects.orgId, orgs.id)) .where(eq(environments.id, environmentId)) .get(); - return projectTeam?.teams; + return projectOrg?.orgs; }, }; } diff --git a/packages/store/src/api/teams.ts b/packages/store/src/api/teams.ts deleted file mode 100644 index b45e23dc..00000000 --- a/packages/store/src/api/teams.ts +++ /dev/null @@ -1,377 +0,0 @@ -import { randomUUID } from "crypto"; -import { type Database } from "@tableland/sdk"; -import { and, asc, eq, ne, inArray } from "drizzle-orm"; -import { type DrizzleD1Database } from "drizzle-orm/d1"; -import { sealData } from "iron-session"; -import * as schema from "../schema/index.js"; -import { slugify } from "../helpers.js"; - -type NewTeamInviteSealed = schema.NewTeamInviteSealed; -type Team = schema.Team; -type TeamInvite = schema.TeamInvite; -const projects = schema.projects; -const teamInvites = schema.teamInvites; -const teamMemberships = schema.teamMemberships; -const teamProjects = schema.teamProjects; -const teams = schema.teams; -const users = schema.users; -const projectDefs = schema.projectDefs; -const defs = schema.defs; -const environments = schema.environments; -const deployments = schema.deployments; - -export function initTeams( - db: DrizzleD1Database, - tbl: Database, - dataSealPass: string, -) { - return { - nameAvailable: async function (name: string, teamId?: string) { - const res = await db - .select() - .from(teams) - .where( - and( - eq(teams.slug, slugify(name)), - teamId ? ne(teams.id, teamId) : undefined, - ), - ) - .get(); - return !res; - }, - - createTeamByPersonalTeam: async function ( - name: string, - personalTeamId: string, - inviteEmails: string[], - ) { - const teamId = randomUUID(); - const slug = slugify(name); - const now = new Date().toISOString(); - const team: Team = { - id: teamId, - personal: 0, - name, - slug, - createdAt: now, - updatedAt: now, - }; - const { sql: teamsSql, params: teamsParams } = db - .insert(teams) - .values(team) - .toSQL(); - const { sql: teamMembershipsSql, params: teamMembershipsParams } = db - .insert(teamMemberships) - .values({ - memberTeamId: personalTeamId, - teamId, - isOwner: 1, - joinedAt: now, - }) - .toSQL(); - const invites: TeamInvite[] = inviteEmails.map((email) => ({ - id: randomUUID(), - teamId, - inviterTeamId: personalTeamId, - email, - createdAt: now, - claimedByTeamId: null, - claimedAt: null, - })); - const batch = [ - tbl.prepare(teamsSql).bind(teamsParams), - tbl.prepare(teamMembershipsSql).bind(teamMembershipsParams), - ]; - if (invites.length) { - const sealedInvites: NewTeamInviteSealed[] = await Promise.all( - invites.map(async ({ email, ...rest }) => ({ - ...rest, - sealed: await sealData( - { email }, - { - password: dataSealPass, - ttl: 0, - }, - ), - })), - ); - const { sql: invitesSql, params: invitesParams } = db - .insert(teamInvites) - .values(sealedInvites) - .toSQL(); - batch.push(tbl.prepare(invitesSql).bind(invitesParams)); - } - await tbl.batch(batch); - return { team, invites }; - }, - - updateTeam: async function (teamId: string, name: string) { - const slug = slugify(name); - await db - .update(teams) - .set({ name, slug, updatedAt: new Date().toISOString() }) - .where(eq(teams.id, teamId)) - .run(); - return await db.select().from(teams).where(eq(teams.id, teamId)).get(); - }, - - deleteTeam: async function (teamId: string) { - // teams - const { sql: teamsSql, params: teamsParams } = db - .delete(teams) - .where(eq(teams.id, teamId)) - .toSQL(); - - // teamMemberships - const { sql: teamMembershipsSql, params: teamMembershipsParams } = db - .delete(teamMemberships) - .where(eq(teamMemberships.teamId, teamId)) - .toSQL(); - - // users - const { sql: usersSql, params: usersParams } = db - .delete(users) - .where(eq(users.teamId, teamId)) - .toSQL(); - - // teamInvites - const { sql: teamInvitesSql, params: teamInvitesParams } = db - .delete(teamInvites) - .where(eq(teamInvites.teamId, teamId)) - .toSQL(); - - // teamProjects - const { sql: teamProjectsSql, params: teamProjectsParams } = db - .delete(teamProjects) - .where(eq(teamProjects.teamId, teamId)) - .toSQL(); - - const batch = [ - tbl.prepare(teamsSql).bind(teamsParams), - tbl.prepare(teamMembershipsSql).bind(teamMembershipsParams), - tbl.prepare(usersSql).bind(usersParams), - tbl.prepare(teamInvitesSql).bind(teamInvitesParams), - tbl.prepare(teamProjectsSql).bind(teamProjectsParams), - ]; - - // Get an array of project IDs for the team - const teamProjectIds = ( - await db - .select({ projectId: teamProjects.projectId }) - .from(teamProjects) - .where(eq(teamProjects.teamId, teamId)) - .all() - ).map((r) => r.projectId); - - // If the team has projects, delete them and all related data - if (teamProjectIds.length) { - // projects - const { sql: projectsSql, params: projectsParams } = db - .delete(projects) - .where(inArray(projects.id, teamProjectIds)) - .toSQL(); - - // environments - const { sql: environmentsSql, params: environmentsParams } = db - .delete(environments) - .where(inArray(environments.projectId, teamProjectIds)) - .toSQL(); - - // projectDefs - const { sql: projectDefsSql, params: projectDefsParams } = db - .delete(projectDefs) - .where(inArray(projectDefs.projectId, teamProjectIds)) - .toSQL(); - - batch.push(tbl.prepare(projectsSql).bind(projectsParams)); - batch.push(tbl.prepare(environmentsSql).bind(environmentsParams)); - batch.push(tbl.prepare(projectDefsSql).bind(projectDefsParams)); - - // Get an array of def IDs for all projects in the team - const defIds = ( - await db - .select({ defId: projectDefs.defId }) - .from(projectDefs) - .where(inArray(projectDefs.projectId, teamProjectIds)) - .all() - ).map((r) => r.defId); - - // If the team's projects have defs, delete them and all related data - if (defIds.length) { - // defs - const { sql: defsSql, params: defsParams } = db - .delete(defs) - .where(inArray(defs.id, defIds)) - .toSQL(); - - // deployments - const { sql: deploymentsSql, params: deploymentsParams } = db - .delete(deployments) - .where(inArray(deployments.defId, defIds)) - .toSQL(); - - batch.push(tbl.prepare(defsSql).bind(defsParams)); - batch.push(tbl.prepare(deploymentsSql).bind(deploymentsParams)); - } - } - - await tbl.batch(batch); - }, - - teamBySlug: async function (slug: string) { - const team = await db - .select() - .from(teams) - .where(eq(teams.slug, slug)) - .get(); - return team; - }, - - teamById: async function (id: string) { - return await db.select().from(teams).where(eq(teams.id, id)).get(); - }, - - teamsByMemberId: async function (memberId: string) { - const res = await db - .select() - .from(teamMemberships) - .innerJoin(teams, eq(teamMemberships.teamId, teams.id)) - .leftJoin(teamProjects, eq(teamProjects.teamId, teams.id)) - .leftJoin(projects, eq(teamProjects.projectId, projects.id)) - .where(eq(teamMemberships.memberTeamId, memberId)) - .orderBy(asc(teams.slug), asc(projects.slug)) - .all(); - - const iter = res - .reduce( - (acc, r) => { - if (!acc.has(r.teams.id)) { - acc.set(r.teams.id, { - ...r.teams, - projects: [], - }); - } - if (r.projects) { - acc.get(r.teams.id)?.projects.push(r.projects); - } - return acc; - }, - new Map< - string, - schema.Team & { - projects: schema.Project[]; - } - >(), - ) - .values(); - return Array.from(iter); - - // const res1 = await db.query.teamMemberships.findMany({ - // where: (teamMemberships, { eq }) => - // eq(teamMemberships.memberTeamId, memberId), - // with: { - // team: { - // with: { - // teamProjects: { - // // orderBy: (teamProjects, { asc }) => [asc(teamProjects.isOwner)], - // with: { - // project: true, - // }, - // }, - // }, - // }, - // }, - // }); - // return res1.map((teamMembership) => { - // const { - // team: { teamProjects, ...restTeam }, - // } = teamMembership; - // const projects = teamProjects.map((teamProject) => teamProject.project); - // return { - // ...restTeam, - // projects, - // }; - // }); - }, - - userTeamsForTeamId: async function (teamId: string) { - const res = await db - .select({ teams, users, teamMemberships }) - .from(users) - .innerJoin( - teamMemberships, - eq(users.teamId, teamMemberships.memberTeamId), - ) - .innerJoin(teams, eq(users.teamId, teams.id)) - .where(eq(teamMemberships.teamId, teamId)) - .orderBy(teams.name) - .all(); - return res.map((r) => ({ - address: r.users.address, - personalTeam: r.teams, - membership: r.teamMemberships, - })); - }, - - isAuthorizedForTeam: async function (memberTeamId: string, teamId: string) { - const membership = await db - .select() - .from(teamMemberships) - .where( - and( - eq(teamMemberships.teamId, teamId), - eq(teamMemberships.memberTeamId, memberTeamId), - ), - ) - .get(); - return membership ?? false; - }, - - toggleAdmin: async function (teamId: string, memberId: string) { - const res = await db - .select({ isOwner: teamMemberships.isOwner }) - .from(teamMemberships) - .where( - and( - eq(teamMemberships.teamId, teamId), - eq(teamMemberships.memberTeamId, memberId), - ), - ) - .get(); - if (!res) { - throw new Error("Team membership not found"); - } - await db - .update(teamMemberships) - .set({ isOwner: res.isOwner ? 0 : 1 }) - .where( - and( - eq(teamMemberships.teamId, teamId), - eq(teamMemberships.memberTeamId, memberId), - ), - ) - .run(); - }, - - removeTeamMember: async function (teamId: string, memberId: string) { - const { sql: membershipsSql, params: membershipsParams } = db - .delete(teamMemberships) - .where( - and( - eq(teamMemberships.teamId, teamId), - eq(teamMemberships.memberTeamId, memberId), - ), - ) - .toSQL(); - const { sql: invitesSql, params: invitesParams } = db - .delete(teamInvites) - .where(eq(teamInvites.claimedByTeamId, memberId)) - .toSQL(); - const batch = [ - tbl.prepare(membershipsSql).bind(membershipsParams), - tbl.prepare(invitesSql).bind(invitesParams), - ]; - await tbl.batch(batch); - }, - }; -} diff --git a/packages/store/src/api/users.ts b/packages/store/src/api/users.ts index 746242a7..1411360d 100644 --- a/packages/store/src/api/users.ts +++ b/packages/store/src/api/users.ts @@ -3,29 +3,29 @@ import { type DrizzleD1Database } from "drizzle-orm/d1"; import * as schema from "../schema/index.js"; const users = schema.users; -const teams = schema.teams; +const orgs = schema.orgs; export function initUsers(db: DrizzleD1Database) { return { - // NOTE: the users table only has the personal team, i.e. this won't return all teams for a user - userPersonalTeam: async function (userAddress: string) { + // NOTE: the users table only has the personal org, i.e. this won't return all orgs for a user + userPersonalOrg: async function (userAddress: string) { const user = await db - .select({ teamId: users.teamId }) + .select({ orgId: users.orgId }) .from(users) .where(eq(users.address, userAddress)) .get(); - return user?.teamId; + return user?.orgId; }, usersForAddresses: async function (addresses: string[]) { const res = await db .select({ user: users, - team: teams, + org: orgs, }) .from(users) - .innerJoin(teams, eq(users.teamId, teams.id)) + .innerJoin(orgs, eq(users.orgId, orgs.id)) .where(inArray(users.address, addresses)) .all(); return res; @@ -35,10 +35,10 @@ export function initUsers(db: DrizzleD1Database) { const res = await db .select({ user: users, - team: teams, + org: orgs, }) .from(users) - .innerJoin(teams, eq(users.teamId, teams.id)) + .innerJoin(orgs, eq(users.orgId, orgs.id)) .where(eq(users.address, address)) .get(); return res; diff --git a/packages/store/src/schema/index.ts b/packages/store/src/schema/index.ts index 51bab166..badb1aba 100644 --- a/packages/store/src/schema/index.ts +++ b/packages/store/src/schema/index.ts @@ -12,15 +12,15 @@ export const users = sqliteTable( "users", { address: text("address").primaryKey(), - teamId: text("team_id").notNull(), + orgId: text("team_id").notNull(), sealed: text("sealed").notNull(), }, (users) => ({ - teamIdIdx: uniqueIndex("teamIdIdx").on(users.teamId), + orgIdIdx: uniqueIndex("teamIdIdx").on(users.orgId), }), ); -export const teams = sqliteTable( +export const orgs = sqliteTable( "teams", { id: text("id").primaryKey(), @@ -30,25 +30,25 @@ export const teams = sqliteTable( createdAt: text("created_at"), updatedAt: text("updated_at"), }, - (teams) => ({ - nameIdx: uniqueIndex("nameIdx").on(teams.name), - slugIdx: uniqueIndex("slugIdx").on(teams.slug), + (orgs) => ({ + nameIdx: uniqueIndex("nameIdx").on(orgs.name), + slugIdx: uniqueIndex("slugIdx").on(orgs.slug), }), ); -export const teamMemberships = sqliteTable( +export const orgMemberships = sqliteTable( "team_memberships", { - memberTeamId: text("member_team_id").notNull(), - teamId: text("team_id").notNull(), + memberOrgId: text("member_team_id").notNull(), + orgId: text("team_id").notNull(), isOwner: integer("is_owner").notNull(), joinedAt: text("joined_at").notNull(), }, - (userTeams) => { + (orgMemberships) => { return { - memberTeamIdx: uniqueIndex("memberTeamIdx").on( - userTeams.memberTeamId, - userTeams.teamId, + memberOrgIdx: uniqueIndex("memberTeamIdx").on( + orgMemberships.memberOrgId, + orgMemberships.orgId, ), }; }, @@ -63,18 +63,18 @@ export const projects = sqliteTable("projects", { updatedAt: text("updated_at"), }); -export const teamProjects = sqliteTable( +export const orgProjects = sqliteTable( "team_projects", { - teamId: text("team_id").notNull(), + orgId: text("team_id").notNull(), projectId: text("project_id").notNull(), isOwner: integer("is_owner").notNull(), }, - (teamProjects) => { + (orgProjects) => { return { - teamProjectIdx: uniqueIndex("teamProjectIdx").on( - teamProjects.teamId, - teamProjects.projectId, + orgProjectIdx: uniqueIndex("teamProjectIdx").on( + orgProjects.orgId, + orgProjects.projectId, ), }; }, @@ -161,13 +161,13 @@ export const deployments = sqliteTable( // mutation: text("mutation").notNull() // }); -export const teamInvites = sqliteTable("team_invites", { +export const orgInvites = sqliteTable("team_invites", { id: text("id").primaryKey(), - teamId: text("team_id").notNull(), + orgId: text("team_id").notNull(), sealed: text("sealed").notNull(), - inviterTeamId: text("inviter_team_id").notNull(), + inviterOrgId: text("inviter_team_id").notNull(), createdAt: text("created_at").notNull(), - claimedByTeamId: text("claimed_by_team_id"), + claimedByOrgId: text("claimed_by_team_id"), claimedAt: text("claimed_at"), }); @@ -182,11 +182,11 @@ export type NewUser = Omit & { email?: string; }; -export type Team = InferSelectModel; -export type NewTeam = InferInsertModel; +export type Org = InferSelectModel; +export type NewOrg = InferInsertModel; -export type TeamMembership = InferSelectModel; -export type NewTeamMembership = InferInsertModel; +export type OrgMembership = InferSelectModel; +export type NewOrgMembership = InferInsertModel; export type Project = InferSelectModel; export type NewProject = InferInsertModel; @@ -203,12 +203,12 @@ export type NewDeployment = InferInsertModel; // export type MigrationLog = InferSelectModel; // export type NewMigrationLog = InferInsertModel; -export type TeamProject = InferSelectModel; -export type NewTeamProject = InferInsertModel; +export type OrgProject = InferSelectModel; +export type NewOrgProject = InferInsertModel; -export type TeamInviteSealed = InferSelectModel; -export type NewTeamInviteSealed = InferInsertModel; -export type TeamInvite = Omit & { email: string }; -export type NewTeamInvite = Omit & { +export type OrgInviteSealed = InferSelectModel; +export type NewOrgInviteSealed = InferInsertModel; +export type OrgInvite = Omit & { email: string }; +export type NewOrgInvite = Omit & { email: string; }; diff --git a/packages/validators/src/common.ts b/packages/validators/src/common.ts index d7e3e7a8..174aa01c 100644 --- a/packages/validators/src/common.ts +++ b/packages/validators/src/common.ts @@ -1,14 +1,14 @@ import { z } from "zod"; import { helpers } from "@tableland/sdk"; import { slugify } from "@tableland/studio-store"; -import { restrictedTeamSlugs, restrictedDefSlugs } from "./restricted-slugs.js"; +import { restrictedOrgSlugs, restrictedDefSlugs } from "./restricted-slugs.js"; -export const teamNameSchema = z +export const orgNameSchema = z .string() .trim() .min(3) - .refine((name) => !restrictedTeamSlugs.includes(slugify(name)), { - message: "You can't use a restricted word as a team name.", + .refine((name) => !restrictedOrgSlugs.includes(slugify(name)), { + message: "You can't use a restricted word as a org name.", }); export const defNameSchema = z diff --git a/packages/validators/src/index.ts b/packages/validators/src/index.ts index ad02ff19..7201db6b 100644 --- a/packages/validators/src/index.ts +++ b/packages/validators/src/index.ts @@ -1,6 +1,6 @@ export * from "./validators/defs.js"; export * from "./validators/envs.js"; -export * from "./validators/teams.js"; +export * from "./validators/orgs.js"; export * from "./validators/projects.js"; export * from "./validators/auth.js"; export * from "./validators/tables.js"; diff --git a/packages/validators/src/restricted-slugs.ts b/packages/validators/src/restricted-slugs.ts index 0163d549..f894bc59 100644 --- a/packages/validators/src/restricted-slugs.ts +++ b/packages/validators/src/restricted-slugs.ts @@ -1,4 +1,4 @@ -export const restrictedTeamSlugs = [ +export const restrictedOrgSlugs = [ "api", "invite", "sql-log", @@ -15,7 +15,7 @@ export const restrictedDefSlugs = ["settings", "tables", "console"]; export const restrictedEnvSlugs = ["settings"]; export const allRestrictedSlugs = [ - ...restrictedTeamSlugs, + ...restrictedOrgSlugs, ...restrictedProjectSlugs, ...restrictedDefSlugs, ...restrictedEnvSlugs, diff --git a/packages/validators/src/validators/auth.ts b/packages/validators/src/validators/auth.ts index adfda619..bec29792 100644 --- a/packages/validators/src/validators/auth.ts +++ b/packages/validators/src/validators/auth.ts @@ -1,7 +1,7 @@ import { z } from "zod"; -import { teamNameSchema } from "../common.js"; +import { orgNameSchema } from "../common.js"; export const registerSchema = z.object({ - username: teamNameSchema, + username: orgNameSchema, email: z.string().trim().email().or(z.literal("")), }); diff --git a/packages/validators/src/validators/orgs.ts b/packages/validators/src/validators/orgs.ts new file mode 100644 index 00000000..cd321bc2 --- /dev/null +++ b/packages/validators/src/validators/orgs.ts @@ -0,0 +1,14 @@ +import { z } from "zod"; +import { orgNameSchema } from "../common.js"; + +export const orgNameAvailableSchema = z.object({ + orgId: z.string().trim().min(1).optional(), + name: orgNameSchema, +}); + +export const newOrgSchema = z.object({ + name: orgNameSchema, + emailInvites: z.array(z.string().trim().email()), +}); + +export const updateOrgSchema = z.object({ name: orgNameSchema }); diff --git a/packages/validators/src/validators/projects.ts b/packages/validators/src/validators/projects.ts index de84be34..cbe5a2b7 100644 --- a/packages/validators/src/validators/projects.ts +++ b/packages/validators/src/validators/projects.ts @@ -14,7 +14,7 @@ const projectNameSchema = z const projectDescriptionSchema = z.string().trim().min(1).max(1024); export const projectNameAvailableSchema = z.object({ - teamId: z.string().trim().optional(), + orgId: z.string().trim().optional(), projectId: z.string().trim().optional(), name: projectNameSchema, }); diff --git a/packages/validators/src/validators/teams.ts b/packages/validators/src/validators/teams.ts deleted file mode 100644 index e6c56740..00000000 --- a/packages/validators/src/validators/teams.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { z } from "zod"; -import { teamNameSchema } from "../common.js"; - -export const teamNameAvailableSchema = z.object({ - teamId: z.string().trim().min(1).optional(), - name: teamNameSchema, -}); - -export const newTeamSchema = z.object({ - name: teamNameSchema, - emailInvites: z.array(z.string().trim().email()), -}); - -export const updateTeamSchema = z.object({ name: teamNameSchema }); diff --git a/packages/web/app/[team]/(team)/_components/new-project-button.tsx b/packages/web/app/[org]/(org)/_components/new-project-button.tsx similarity index 81% rename from packages/web/app/[team]/(team)/_components/new-project-button.tsx rename to packages/web/app/[org]/(org)/_components/new-project-button.tsx index be095df5..a1de7deb 100644 --- a/packages/web/app/[team]/(team)/_components/new-project-button.tsx +++ b/packages/web/app/[org]/(org)/_components/new-project-button.tsx @@ -7,14 +7,14 @@ import NewProjectForm from "@/components/new-project-form"; import { Button } from "@/components/ui/button"; export default function NewProjectButton({ - team, + org, ...props -}: { team: schema.Team } & React.ComponentProps) { +}: { org: schema.Org } & React.ComponentProps) { const router = useRouter(); return ( @@ -23,7 +23,7 @@ export default function NewProjectButton({ } onSuccess={(project) => { router.refresh(); - router.push(`/${team.slug}/${project.slug}`); + router.push(`/${org.slug}/${project.slug}`); }} > ); diff --git a/packages/web/app/[team]/(team)/_components/sidebar.tsx b/packages/web/app/[org]/(org)/_components/sidebar.tsx similarity index 69% rename from packages/web/app/[team]/(team)/_components/sidebar.tsx rename to packages/web/app/[org]/(org)/_components/sidebar.tsx index bc54df95..401a182c 100644 --- a/packages/web/app/[team]/(team)/_components/sidebar.tsx +++ b/packages/web/app/[org]/(org)/_components/sidebar.tsx @@ -8,18 +8,18 @@ import { SidebarContainer, SidebarSection } from "@/components/sidebar"; import SidebarLink from "@/components/sidebar-link"; export function Sidebar() { - const { team: teamSlug } = useParams<{ - team: string; + const { org: orgSlug } = useParams<{ + org: string; }>(); const selectedLayoutSegment = useSelectedLayoutSegment(); - const teamQuery = api.teams.teamBySlug.useQuery({ slug: teamSlug }); + const orgQuery = api.orgs.orgBySlug.useQuery({ slug: orgSlug }); - const isAuthorizedQuery = api.teams.isAuthorized.useQuery( - teamQuery.data ? { teamId: teamQuery.data.id } : skipToken, + const isAuthorizedQuery = api.orgs.isAuthorized.useQuery( + orgQuery.data ? { orgId: orgQuery.data.id } : skipToken, ); - if (!teamQuery.data) { + if (!orgQuery.data) { return null; } @@ -27,14 +27,14 @@ export function Sidebar() { - {!teamQuery.data.personal && ( + {!orgQuery.data.personal && (
-

{team.name} projects

- {authorized && } +

{org.name} projects

+ {authorized && }
{!projects.length && (

- Team {team.name} doesn't have any projects yet. + Org {org.name} doesn't have any projects yet.

)} @@ -56,7 +56,7 @@ export default async function Projects({ return (
diff --git a/packages/web/app/[team]/(team)/people/_components/info.tsx b/packages/web/app/[org]/(org)/people/_components/info.tsx similarity index 94% rename from packages/web/app/[team]/(team)/people/_components/info.tsx rename to packages/web/app/[org]/(org)/people/_components/info.tsx index aec00f7c..924cefe5 100644 --- a/packages/web/app/[team]/(team)/people/_components/info.tsx +++ b/packages/web/app/[org]/(org)/people/_components/info.tsx @@ -17,12 +17,12 @@ import { TimeSince } from "@/components/time"; import { cn } from "@/lib/utils"; type Props = LucideProps & { - user?: schema.Team; - userMembership?: schema.TeamMembership; - team: schema.Team; - inviter?: schema.Team; - invite?: schema.TeamInvite; - membership?: schema.TeamMembership; + user?: schema.Org; + userMembership?: schema.OrgMembership; + org: schema.Org; + inviter?: schema.Org; + invite?: schema.OrgInvite; + membership?: schema.OrgMembership; }; export default function Info({ @@ -40,7 +40,7 @@ export default function Info({
-

Founded team

+

Founded org

diff --git a/packages/web/app/[team]/(team)/people/_components/invite-actions.tsx b/packages/web/app/[org]/(org)/people/_components/invite-actions.tsx similarity index 90% rename from packages/web/app/[team]/(team)/people/_components/invite-actions.tsx rename to packages/web/app/[org]/(org)/people/_components/invite-actions.tsx index 20f083f3..1f4b8262 100644 --- a/packages/web/app/[team]/(team)/people/_components/invite-actions.tsx +++ b/packages/web/app/[org]/(org)/people/_components/invite-actions.tsx @@ -18,10 +18,10 @@ import { cn } from "@/lib/utils"; import { api } from "@/trpc/react"; type Props = DropdownMenuTriggerProps & { - invite: schema.TeamInvite; - inviter: schema.Team; - user: schema.Team; - membership: schema.TeamMembership; + invite: schema.OrgInvite; + inviter: schema.Org; + user: schema.Org; + membership: schema.OrgMembership; }; export default function InviteActions({ @@ -74,7 +74,7 @@ export default function InviteActions({ - resendInvite.mutate({ teamId: invite.teamId, inviteId: invite.id }) + resendInvite.mutate({ orgId: invite.orgId, inviteId: invite.id }) } > Re-send invite @@ -83,7 +83,7 @@ export default function InviteActions({ deleteInvite.mutate({ - teamId: invite.teamId, + orgId: invite.orgId, inviteId: invite.id, }) } diff --git a/packages/web/app/[team]/(team)/people/_components/new-invite.tsx b/packages/web/app/[org]/(org)/people/_components/new-invite.tsx similarity index 96% rename from packages/web/app/[team]/(team)/people/_components/new-invite.tsx rename to packages/web/app/[org]/(org)/people/_components/new-invite.tsx index 8a3be9f3..8bdac28d 100644 --- a/packages/web/app/[team]/(team)/people/_components/new-invite.tsx +++ b/packages/web/app/[org]/(org)/people/_components/new-invite.tsx @@ -23,7 +23,7 @@ const formSchema = z.object({ email: z.string().trim().email(), }); -export default function NewInvite({ team }: { team: schema.Team }) { +export default function NewInvite({ org }: { org: schema.Org }) { const router = useRouter(); const [showForm, setShowForm] = useState(false); const inviteEmails = api.invites.inviteEmails.useMutation({ @@ -48,7 +48,7 @@ export default function NewInvite({ team }: { team: schema.Team }) { } function onSubmit(values: z.infer) { - inviteEmails.mutate({ teamId: team.id, emails: [values.email] }); + inviteEmails.mutate({ orgId: org.id, emails: [values.email] }); } function onCancel() { diff --git a/packages/web/app/[team]/(team)/people/_components/user-actions.tsx b/packages/web/app/[org]/(org)/people/_components/user-actions.tsx similarity index 83% rename from packages/web/app/[team]/(team)/people/_components/user-actions.tsx rename to packages/web/app/[org]/(org)/people/_components/user-actions.tsx index 9ab2883e..17151487 100644 --- a/packages/web/app/[team]/(team)/people/_components/user-actions.tsx +++ b/packages/web/app/[org]/(org)/people/_components/user-actions.tsx @@ -18,17 +18,17 @@ import { cn } from "@/lib/utils"; import { api } from "@/trpc/react"; type Props = DropdownMenuTriggerProps & { - team: schema.Team; - user: schema.Team; - userMembership: schema.TeamMembership; - member: schema.Team; - memberMembership: schema.TeamMembership; + org: schema.Org; + user: schema.Org; + userMembership: schema.OrgMembership; + member: schema.Org; + memberMembership: schema.OrgMembership; claimedInviteId?: string; }; export default function UserActions({ className, - team, + org, user, userMembership, member, @@ -39,7 +39,7 @@ export default function UserActions({ const router = useRouter(); const { toast } = useToast(); - const toggleAdmin = api.teams.toggleAdmin.useMutation({ + const toggleAdmin = api.orgs.toggleAdmin.useMutation({ onSuccess: () => { router.refresh(); toast({ @@ -54,7 +54,7 @@ export default function UserActions({ }, }); - const removeUser = api.teams.removeTeamMember.useMutation({ + const removeUser = api.orgs.removeOrgMember.useMutation({ onSuccess: () => { router.refresh(); toast({ @@ -63,7 +63,7 @@ export default function UserActions({

{member.name} has been removed from{" "} - {team.name}. + {org.name}.

), }); @@ -98,17 +98,17 @@ export default function UserActions({ <> - toggleAdmin.mutate({ teamId: team.id, userId: member.id }) + toggleAdmin.mutate({ orgId: org.id, userId: member.id }) } > {memberMembership.isOwner ? "Remove" : "Make"} admin - removeUser.mutate({ teamId: team.id, userId: member.id }) + removeUser.mutate({ orgId: org.id, userId: member.id }) } > - Remove from team + Remove from org )} diff --git a/packages/web/app/[team]/(team)/people/page.tsx b/packages/web/app/[org]/(org)/people/page.tsx similarity index 67% rename from packages/web/app/[team]/(team)/people/page.tsx rename to packages/web/app/[org]/(org)/people/page.tsx index a02423eb..9dab2086 100644 --- a/packages/web/app/[team]/(team)/people/page.tsx +++ b/packages/web/app/[org]/(org)/people/page.tsx @@ -10,27 +10,27 @@ import HashDisplay from "@/components/hash-display"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { cn } from "@/lib/utils"; import { api } from "@/trpc/server"; -import { teamBySlug } from "@/lib/api-helpers"; +import { orgBySlug } from "@/lib/api-helpers"; -export default async function People({ params }: { params: { team: string } }) { +export default async function People({ params }: { params: { org: string } }) { const session = await getSession({ cookies: cookies(), headers: headers() }); const { auth } = session; - const team = await teamBySlug(params.team); - const people = await cache(api.teams.usersForTeam)({ - teamId: team.id, + const org = await orgBySlug(params.org); + const people = await cache(api.orgs.usersForOrg)({ + orgId: org.id, }); - let invites: RouterOutputs["invites"]["invitesForTeam"]["invites"] = []; - let teamAuthorization: - | RouterOutputs["invites"]["invitesForTeam"]["teamAuthorization"] + let invites: RouterOutputs["invites"]["invitesForOrg"]["invites"] = []; + let orgAuthorization: + | RouterOutputs["invites"]["invitesForOrg"]["orgAuthorization"] | undefined; if (auth) { try { - const { invites: invitesRes, teamAuthorization: teamAuthorizationRes } = - await cache(api.invites.invitesForTeam)({ teamId: team.id }); + const { invites: invitesRes, orgAuthorization: orgAuthorizationRes } = + await cache(api.invites.invitesForOrg)({ orgId: org.id }); invites = invitesRes; - teamAuthorization = teamAuthorizationRes; + orgAuthorization = orgAuthorizationRes; } catch {} } @@ -50,10 +50,10 @@ export default async function People({ params }: { params: { team: string } }) { ); const peopleAugmented = people.map((person) => { - if (binnedInvites.claimed.has(person.personalTeam.id)) { + if (binnedInvites.claimed.has(person.personalOrg.id)) { return { ...person, - claimedInvite: binnedInvites.claimed.get(person.personalTeam.id), + claimedInvite: binnedInvites.claimed.get(person.personalOrg.id), }; } return { @@ -67,20 +67,20 @@ export default async function People({ params }: { params: { team: string } }) {
{peopleAugmented.map((person) => { return ( -
+
- {person.personalTeam.name.charAt(0)} + {person.personalOrg.name.charAt(0)}

- {person.personalTeam.name} - {person.personalTeam.id === auth?.personalTeam.id && " (You)"} + {person.personalOrg.name} + {person.personalOrg.id === auth?.personalOrg.id && " (You)"}

- {teamAuthorization && ( + {orgAuthorization && (
{person.membership.isOwner ? "Admin" : "Member"}
)} - {auth && teamAuthorization && ( + {auth && orgAuthorization && ( @@ -116,7 +116,7 @@ export default async function People({ params }: { params: { team: string } }) {
); })} - {teamAuthorization && + {orgAuthorization && auth && binnedInvites.pending.map((i) => { return ( @@ -133,9 +133,9 @@ export default async function People({ params }: { params: { team: string } }) {
@@ -143,13 +143,13 @@ export default async function People({ params }: { params: { team: string } }) { className="ml-2" invite={i.invite} inviter={i.inviter} - user={auth.personalTeam} - membership={teamAuthorization} + user={auth.personalOrg} + membership={orgAuthorization} />
); })} - {teamAuthorization && } + {orgAuthorization && }
); diff --git a/packages/web/app/[team]/(team)/settings/_components/delete-button.tsx b/packages/web/app/[org]/(org)/settings/_components/delete-button.tsx similarity index 74% rename from packages/web/app/[team]/(team)/settings/_components/delete-button.tsx rename to packages/web/app/[org]/(org)/settings/_components/delete-button.tsx index 5b92a94c..8ec8ec81 100644 --- a/packages/web/app/[team]/(team)/settings/_components/delete-button.tsx +++ b/packages/web/app/[org]/(org)/settings/_components/delete-button.tsx @@ -19,17 +19,17 @@ import { Button, type ButtonProps } from "@/components/ui/button"; import { authAtom } from "@/store/auth"; export default function DeleteButton({ - team, + org, ...props -}: Omit & { team: schema.Team }) { +}: Omit & { org: schema.Org }) { const router = useRouter(); const logout = api.auth.logout.useMutation(); const setAuth = useSetAtom(authAtom); - const deleteTeam = api.teams.deleteTeam.useMutation({ + const deleteOrg = api.orgs.deleteOrg.useMutation({ onSuccess: async () => { router.replace("/"); - if (team.personal) { + if (org.personal) { await logout.mutateAsync(); setAuth(undefined); } @@ -38,33 +38,33 @@ export default function DeleteButton({ }); const handleClick = () => { - deleteTeam.mutate({ teamId: team.id }); + deleteOrg.mutate({ orgId: org.id }); }; return ( e.preventDefault() : undefined + deleteOrg.isPending ? (e) => e.preventDefault() : undefined } onEscapeKeyDown={ - deleteTeam.isPending ? (e) => e.preventDefault() : undefined + deleteOrg.isPending ? (e) => e.preventDefault() : undefined } > Are you absolutely sure? - This action cannot be undone. All data related to this team will be + This action cannot be undone. All data related to this org will be deleted from Studio's database. This includes all projects, table definitions, and table metadata. Any tables that were created - on Tableland by this team will continue to exist, but Studio will - not be aware of them. + on Tableland by this org will continue to exist, but Studio will not + be aware of them. @@ -72,7 +72,7 @@ export default function DeleteButton({ @@ -80,12 +80,12 @@ export default function DeleteButton({ diff --git a/packages/web/app/[team]/(team)/settings/_components/edit-team.tsx b/packages/web/app/[org]/(org)/settings/_components/edit-org.tsx similarity index 69% rename from packages/web/app/[team]/(team)/settings/_components/edit-team.tsx rename to packages/web/app/[org]/(org)/settings/_components/edit-org.tsx index 10c7f3a7..94f446fb 100644 --- a/packages/web/app/[team]/(team)/settings/_components/edit-team.tsx +++ b/packages/web/app/[org]/(org)/settings/_components/edit-org.tsx @@ -2,7 +2,7 @@ import { zodResolver } from "@hookform/resolvers/zod"; import { type schema } from "@tableland/studio-store"; -import { updateTeamSchema } from "@tableland/studio-validators"; +import { updateOrgSchema } from "@tableland/studio-validators"; import { useForm } from "react-hook-form"; import { type z } from "zod"; import { useEffect, useState } from "react"; @@ -22,43 +22,43 @@ import { import InputWithCheck from "@/components/input-with-check"; import { api } from "@/trpc/react"; -export default function EditTeam({ - team, +export default function EditOrg({ + org, disabled = false, }: { - team: schema.Team; + org: schema.Org; disabled?: boolean; }) { const router = useRouter(); - const [query, setQuery] = useState(team.name); - const form = useForm>({ - resolver: zodResolver(updateTeamSchema), + const [query, setQuery] = useState(org.name); + const form = useForm>({ + resolver: zodResolver(updateOrgSchema), defaultValues: { - name: team.name, + name: org.name, }, }); useEffect(() => { - form.reset({ name: team.name }); - }, [team, form]); + form.reset({ name: org.name }); + }, [org, form]); - const nameAvailable = api.teams.nameAvailable.useQuery( - query !== team.name ? { teamId: team.id, name: query } : skipToken, + const nameAvailable = api.orgs.nameAvailable.useQuery( + query !== org.name ? { orgId: org.id, name: query } : skipToken, { retry: false }, ); - const updateTeam = api.teams.updateTeam.useMutation({ - onSuccess: (team) => { + const updateOrg = api.orgs.updateOrg.useMutation({ + onSuccess: (org) => { router.refresh(); - router.replace(`/${team.slug}/settings`); + router.replace(`/${org.slug}/settings`); }, }); - const onSubmit = (values: z.infer) => { - updateTeam.mutate({ teamId: team.id, ...values }); + const onSubmit = (values: z.infer) => { + updateOrg.mutate({ orgId: org.id, ...values }); }; const onReset = () => { - setQuery(team.name); + setQuery(org.name); form.reset(); }; @@ -78,15 +78,15 @@ export default function EditTeam({ Name - The team name. It must be unique across all Studio teams. + The org name. It must be unique across all Studio orgs. @@ -97,7 +97,7 @@ export default function EditTeam({ variant="outline" type="reset" onClick={onReset} - disabled={!form.formState.isDirty || updateTeam.isPending} + disabled={!form.formState.isDirty || updateOrg.isPending} > Reset @@ -107,10 +107,10 @@ export default function EditTeam({ disabled || !form.formState.isDirty || (!!form.formState.dirtyFields.name && !nameAvailable.data) || - updateTeam.isPending + updateOrg.isPending } > - {updateTeam.isPending && ( + {updateOrg.isPending && ( )} Submit diff --git a/packages/web/app/[team]/(team)/settings/page.tsx b/packages/web/app/[org]/(org)/settings/page.tsx similarity index 64% rename from packages/web/app/[team]/(team)/settings/page.tsx rename to packages/web/app/[org]/(org)/settings/page.tsx index f86c80aa..59316e73 100644 --- a/packages/web/app/[team]/(team)/settings/page.tsx +++ b/packages/web/app/[org]/(org)/settings/page.tsx @@ -2,7 +2,7 @@ import { cache } from "react"; import { notFound } from "next/navigation"; import { OctagonAlert } from "lucide-react"; import DeleteButton from "./_components/delete-button"; -import EditTeam from "./_components/edit-team"; +import EditOrg from "./_components/edit-org"; import { api } from "@/trpc/server"; import { Card, @@ -13,16 +13,16 @@ import { } from "@/components/ui/card"; import { cn } from "@/lib/utils"; import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; -import { teamBySlug } from "@/lib/api-helpers"; +import { orgBySlug } from "@/lib/api-helpers"; -export default async function TeamSettings({ +export default async function OrgSettings({ params, }: { - params: { team: string }; + params: { org: string }; }) { - const team = await teamBySlug(params.team); - const authorization = await cache(api.teams.isAuthorized)({ - teamId: team.id, + const org = await orgBySlug(params.org); + const authorization = await cache(api.orgs.isAuthorized)({ + orgId: org.id, }); if (!authorization) { @@ -38,20 +38,20 @@ export default async function TeamSettings({ Hold on - You need to be an admin to access team settings. An existing admin + You need to be an admin to access org settings. An existing admin can make this happen. )} - Team info + Org info - Update general information about the {team.name} team. + Update general information about the {org.name} org. - + @@ -64,11 +64,11 @@ export default async function TeamSettings({

- {team.personal - ? `You can delete your personal team, ${team.name}, if you choose. This effectively deletes your Studio account:` - : `You can delete the ${team.name} team if you choose:`} + {org.personal + ? `You can delete your personal org, ${org.name}, if you choose. This effectively deletes your Studio account:` + : `You can delete the ${org.name} org if you choose:`}

- +
diff --git a/packages/web/app/[team]/[project]/[env]/[table]/_components/deploy-button.tsx b/packages/web/app/[org]/[project]/[env]/[table]/_components/deploy-button.tsx similarity index 100% rename from packages/web/app/[team]/[project]/[env]/[table]/_components/deploy-button.tsx rename to packages/web/app/[org]/[project]/[env]/[table]/_components/deploy-button.tsx diff --git a/packages/web/app/[team]/[project]/[env]/[table]/_components/needs-deploy.tsx b/packages/web/app/[org]/[project]/[env]/[table]/_components/needs-deploy.tsx similarity index 94% rename from packages/web/app/[team]/[project]/[env]/[table]/_components/needs-deploy.tsx rename to packages/web/app/[org]/[project]/[env]/[table]/_components/needs-deploy.tsx index 84544a51..1b4811c9 100644 --- a/packages/web/app/[team]/[project]/[env]/[table]/_components/needs-deploy.tsx +++ b/packages/web/app/[org]/[project]/[env]/[table]/_components/needs-deploy.tsx @@ -17,7 +17,7 @@ export default async function NeedsDeploy({ }: { def: Pick; env: schema.Environment; - isAuthorized?: RouterOutputs["teams"]["isAuthorized"]; + isAuthorized?: RouterOutputs["orgs"]["isAuthorized"]; }) { return ( diff --git a/packages/web/app/[team]/[project]/[env]/[table]/page.tsx b/packages/web/app/[org]/[project]/[env]/[table]/page.tsx similarity index 89% rename from packages/web/app/[team]/[project]/[env]/[table]/page.tsx rename to packages/web/app/[org]/[project]/[env]/[table]/page.tsx index 380ad018..3fdef0c0 100644 --- a/packages/web/app/[team]/[project]/[env]/[table]/page.tsx +++ b/packages/web/app/[org]/[project]/[env]/[table]/page.tsx @@ -10,7 +10,7 @@ import { defBySlug, environmentBySlug, projectBySlug, - teamBySlug, + orgBySlug, } from "@/lib/api-helpers"; import DefDetails from "@/components/def-details"; import TableWrapper from "@/components/table-wrapper"; @@ -22,11 +22,11 @@ import { export default async function Deployments({ params, }: { - params: { team: string; project: string; env: string; table: string }; + params: { org: string; project: string; env: string; table: string }; }) { const session = await getSession({ headers: headers(), cookies: cookies() }); - const team = await teamBySlug(params.team); - const project = await projectBySlug(params.project, team.id); + const org = await orgBySlug(params.org); + const project = await projectBySlug(params.project, org.id); const env = await environmentBySlug(project.id, params.env); const def = await defBySlug(project.id, params.table); let deployment: schema.Deployment | undefined; @@ -49,12 +49,12 @@ export default async function Deployments({ ); } - const isAuthorized = await cache(api.teams.isAuthorized)({ teamId: team.id }); + const isAuthorized = await cache(api.orgs.isAuthorized)({ orgId: org.id }); return (
{ @@ -110,14 +108,14 @@ export function Sidebar() { .refetch() .then(() => { if (env) { - router.push(`/${team.slug}/${project.slug}/${env.slug}/${def.slug}`); + router.push(`/${org.slug}/${project.slug}/${env.slug}/${def.slug}`); } }) .catch(() => {}); }; const onImportTableSuccess = ( - team: schema.Team, + org: schema.Org, project: schema.Project, def: schema.Def, env: schema.Environment, @@ -125,12 +123,12 @@ export function Sidebar() { defsQuery .refetch() .then(() => { - router.push(`/${team.slug}/${project.slug}/${env.slug}/${def.slug}`); + router.push(`/${org.slug}/${project.slug}/${env.slug}/${def.slug}`); }) .catch(() => {}); }; - if (!teamQuery.data || !projectQuery.data || !linkEnv) { + if (!orgQuery.data || !projectQuery.data || !linkEnv) { return null; } @@ -140,7 +138,7 @@ export function Sidebar() { @@ -186,7 +184,7 @@ export function Sidebar() { key={def.id} icon={Table2} title={def.name} - href={`/${teamQuery.data.slug}/${projectQuery.data.slug}/${linkEnv.slug}/${def.slug}`} + href={`/${orgQuery.data.slug}/${projectQuery.data.slug}/${linkEnv.slug}/${def.slug}`} selected={def.id === defQuery.data?.id} showIndicator={!!env && !deployment && !!isAuthorizedQuery.data} /> @@ -202,7 +200,7 @@ export function Sidebar() {
@@ -211,14 +209,14 @@ export function Sidebar() { ; }) { const table = typeof searchParams.table === "string" ? searchParams.table : undefined; - const team = await teamBySlug(params.team); - const project = await projectBySlug(params.project, team.id); + const org = await orgBySlug(params.org); + const project = await projectBySlug(params.project, org.id); const env = await api.environments.environmentPreferenceForProject({ projectId: project.id, }); const path = table - ? `/${params.team}/${params.project}/${env.slug}/${table}` - : `/${params.team}/${params.project}/${env.slug}`; + ? `/${params.org}/${params.project}/${env.slug}/${table}` + : `/${params.org}/${params.project}/${env.slug}`; redirect(path, RedirectType.replace); } diff --git a/packages/web/app/[team]/[project]/settings/_components/delete-button.tsx b/packages/web/app/[org]/[project]/settings/_components/delete-button.tsx similarity index 97% rename from packages/web/app/[team]/[project]/settings/_components/delete-button.tsx rename to packages/web/app/[org]/[project]/settings/_components/delete-button.tsx index 1ef40057..9f1af504 100644 --- a/packages/web/app/[team]/[project]/settings/_components/delete-button.tsx +++ b/packages/web/app/[org]/[project]/settings/_components/delete-button.tsx @@ -17,18 +17,18 @@ import { api } from "@/trpc/react"; import { Button, type ButtonProps } from "@/components/ui/button"; export default function DeleteButton({ - team, + org, project, ...props }: Omit & { - team: schema.Team; + org: schema.Org; project: schema.Project; }) { const router = useRouter(); const deleteProject = api.projects.deleteProject.useMutation({ onSuccess: async () => { - router.replace(`/${team.slug}`); + router.replace(`/${org.slug}`); router.refresh(); }, }); diff --git a/packages/web/app/[team]/[project]/settings/_components/edit-project.tsx b/packages/web/app/[org]/[project]/settings/_components/edit-project.tsx similarity index 96% rename from packages/web/app/[team]/[project]/settings/_components/edit-project.tsx rename to packages/web/app/[org]/[project]/settings/_components/edit-project.tsx index 47ab7191..12d7f208 100644 --- a/packages/web/app/[team]/[project]/settings/_components/edit-project.tsx +++ b/packages/web/app/[org]/[project]/settings/_components/edit-project.tsx @@ -24,11 +24,11 @@ import { api } from "@/trpc/react"; import { Textarea } from "@/components/ui/textarea"; export default function EditProject({ - team, + org, project, disabled = false, }: { - team: schema.Team; + org: schema.Org; project: schema.Project; disabled?: boolean; }) { @@ -48,14 +48,14 @@ export default function EditProject({ const nameAvailable = api.projects.nameAvailable.useQuery( query !== project.name - ? { teamId: team.id, projectId: project.id, name: query } + ? { orgId: org.id, projectId: project.id, name: query } : skipToken, { retry: false }, ); const updateProject = api.projects.updateProject.useMutation({ onSuccess: (project) => { router.refresh(); - router.replace(`/${team.slug}/${project.slug}/settings`); + router.replace(`/${org.slug}/${project.slug}/settings`); }, }); @@ -98,7 +98,7 @@ export default function EditProject({ /> - The project name. It must be unique within your Studio team. + The project name. It must be unique within your Studio org. diff --git a/packages/web/app/[team]/[project]/settings/_components/env.tsx b/packages/web/app/[org]/[project]/settings/_components/env.tsx similarity index 100% rename from packages/web/app/[team]/[project]/settings/_components/env.tsx rename to packages/web/app/[org]/[project]/settings/_components/env.tsx diff --git a/packages/web/app/[team]/[project]/settings/_components/envs.tsx b/packages/web/app/[org]/[project]/settings/_components/envs.tsx similarity index 100% rename from packages/web/app/[team]/[project]/settings/_components/envs.tsx rename to packages/web/app/[org]/[project]/settings/_components/envs.tsx diff --git a/packages/web/app/[team]/[project]/settings/_components/new-env.tsx b/packages/web/app/[org]/[project]/settings/_components/new-env.tsx similarity index 100% rename from packages/web/app/[team]/[project]/settings/_components/new-env.tsx rename to packages/web/app/[org]/[project]/settings/_components/new-env.tsx diff --git a/packages/web/app/[team]/[project]/settings/page.tsx b/packages/web/app/[org]/[project]/settings/page.tsx similarity index 88% rename from packages/web/app/[team]/[project]/settings/page.tsx rename to packages/web/app/[org]/[project]/settings/page.tsx index 7f99f371..385e6065 100644 --- a/packages/web/app/[team]/[project]/settings/page.tsx +++ b/packages/web/app/[org]/[project]/settings/page.tsx @@ -5,7 +5,7 @@ import EditProject from "./_components/edit-project"; import DeleteButton from "./_components/delete-button"; import NewEnv from "./_components/new-env"; import Envs from "./_components/envs"; -import { projectBySlug, teamBySlug } from "@/lib/api-helpers"; +import { projectBySlug, orgBySlug } from "@/lib/api-helpers"; import { Card, CardContent, @@ -20,12 +20,12 @@ import { api } from "@/trpc/server"; export default async function ProjectSettings({ params, }: { - params: { team: string; project: string }; + params: { org: string; project: string }; }) { - const team = await teamBySlug(params.team); - const project = await projectBySlug(params.project, team.id); - const authorization = await cache(api.teams.isAuthorized)({ - teamId: team.id, + const org = await orgBySlug(params.org); + const project = await projectBySlug(params.project, org.id); + const authorization = await cache(api.orgs.isAuthorized)({ + orgId: org.id, }); const envs = await cache(api.environments.projectEnvironments)({ projectId: project.id, @@ -59,7 +59,7 @@ export default async function ProjectSettings({ @@ -94,7 +94,7 @@ export default async function ProjectSettings({

You can delete the {project.name} project if you choose:

- +
diff --git a/packages/web/app/_components/latest-projects.tsx b/packages/web/app/_components/latest-projects.tsx index 9aefe050..39ed41f5 100644 --- a/packages/web/app/_components/latest-projects.tsx +++ b/packages/web/app/_components/latest-projects.tsx @@ -6,7 +6,7 @@ import { TimeSince } from "@/components/time"; import { Paginator } from "@/components/paginator"; import { TypographyH3 } from "@/components/typography-h3"; import { type store } from "@/lib/store"; -import TeamAvatar from "@/components/team-avatar"; +import OrgAvatar from "@/components/org-avatar"; export type Projects = Awaited< ReturnType @@ -25,15 +25,15 @@ export function LatestProjects({ projects }: { projects: Projects }) { {projects.slice(offset, offset + pageSize).map((item) => (
- +
- {item.team.name}/{item.project.name} + {item.org.name}/{item.project.name}
{item.project.createdAt && ( diff --git a/packages/web/app/_components/new-team-form.tsx b/packages/web/app/_components/new-org-form.tsx similarity index 78% rename from packages/web/app/_components/new-team-form.tsx rename to packages/web/app/_components/new-org-form.tsx index 400348ca..77fd19e5 100644 --- a/packages/web/app/_components/new-team-form.tsx +++ b/packages/web/app/_components/new-org-form.tsx @@ -5,7 +5,7 @@ import { useForm } from "react-hook-form"; import { type z } from "zod"; import { skipToken } from "@tanstack/react-query"; import { type schema } from "@tableland/studio-store"; -import { newTeamSchema } from "@tableland/studio-validators"; +import { newOrgSchema } from "@tableland/studio-validators"; import { FormRootMessage } from "@/components/form"; import InputWithCheck from "@/components/input-with-check"; import TagInput from "@/components/tag-input"; @@ -28,25 +28,25 @@ import { } from "@/components/ui/form"; import { api } from "@/trpc/react"; -export interface NewTeamFormProps { +export interface NewOrgFormProps { open?: boolean; onOpenChange?: (open: boolean) => void; trigger?: React.ReactNode; - onSuccess: (team: schema.Team) => void; + onSuccess: (org: schema.Org) => void; } -export default function NewTeamForm({ +export default function NewOrgForm({ open, onOpenChange, trigger, onSuccess, -}: NewTeamFormProps) { +}: NewOrgFormProps) { const [openSheet, setOpenSheet] = useState(open ?? false); - const [teamName, setTeamName] = useState(""); + const [orgName, setOrgName] = useState(""); const [pendingEmail, setPendingEmail] = useState(""); - const form = useForm>({ - resolver: zodResolver(newTeamSchema), + const form = useForm>({ + resolver: zodResolver(newOrgSchema), defaultValues: { name: "", emailInvites: [], @@ -55,7 +55,7 @@ export default function NewTeamForm({ useEffect(() => { if (!openSheet) { - setTeamName(""); + setOrgName(""); form.reset(); } onOpenChange?.(openSheet); @@ -65,15 +65,15 @@ export default function NewTeamForm({ setOpenSheet(open ?? false); }, [open]); - const nameAvailableQuery = api.teams.nameAvailable.useQuery( - teamName ? { name: teamName } : skipToken, + const nameAvailableQuery = api.orgs.nameAvailable.useQuery( + orgName ? { name: orgName } : skipToken, { retry: false }, ); - const newTeam = api.teams.newTeam.useMutation({ - onSuccess: (team) => { + const newOrg = api.orgs.newOrg.useMutation({ + onSuccess: (org) => { setOpenSheet(false); - onSuccess(team); + onSuccess(org); }, onError: (err) => { setError("root", { message: err.message }); @@ -82,11 +82,11 @@ export default function NewTeamForm({ const { setError } = form; - function onSubmit(values: z.infer) { + function onSubmit(values: z.infer) { if (pendingEmail) { values.emailInvites = [...values.emailInvites, pendingEmail]; } - newTeam.mutate(values); + newOrg.mutate(values); } return ( @@ -94,12 +94,12 @@ export default function NewTeamForm({ {trigger && {trigger}} e.preventDefault() : undefined + newOrg.isPending ? (e) => e.preventDefault() : undefined } onEscapeKeyDown={ - newTeam.isPending ? (e) => e.preventDefault() : undefined + newOrg.isPending ? (e) => e.preventDefault() : undefined } >
@@ -111,7 +111,7 @@ export default function NewTeamForm({ className="mx-auto max-w-lg space-y-8" > - New Team + New Org {/* This action cannot be undone. This will permanently delete your account and remove your data from our servers. @@ -125,13 +125,13 @@ export default function NewTeamForm({ Name - Team name must be unique. + Org name must be unique. )} @@ -158,7 +158,7 @@ export default function NewTeamForm({ Optionally enter a comma-separated list of email addresses - to invite others to your new Team. + to invite others to your new Org. @@ -167,9 +167,9 @@ export default function NewTeamForm({
diff --git a/packages/web/app/layout.tsx b/packages/web/app/layout.tsx index 30588db6..c9419522 100644 --- a/packages/web/app/layout.tsx +++ b/packages/web/app/layout.tsx @@ -47,14 +47,14 @@ export default async function RootLayout({ children: React.ReactNode; }) { const session = await getSession({ cookies: cookies(), headers: headers() }); - let teams: RouterOutputs["teams"]["userTeams"] | undefined; + let orgs: RouterOutputs["orgs"]["userOrgs"] | undefined; if (session.auth) { try { - teams = await cache(api.teams.userTeams)({ - userTeamId: session.auth.user.teamId, + orgs = await cache(api.orgs.userOrgs)({ + userOrgId: session.auth.user.orgId, }); } catch { - // This is fine, we just don't have any teams if the user + // This is fine, we just don't have any orgs if the user // is unauthorized or some other error happens. } } @@ -71,7 +71,7 @@ export default async function RootLayout({
- +
diff --git a/packages/web/app/page.tsx b/packages/web/app/page.tsx index 2d91e307..a49fb4b6 100644 --- a/packages/web/app/page.tsx +++ b/packages/web/app/page.tsx @@ -19,7 +19,7 @@ import { TypographyH3 } from "@/components/typography-h3"; import { TypographyP } from "@/components/typography-p"; import { featuredProjectSlugs } from "@/lib/featured-projects"; import { store } from "@/lib/store"; -import TeamAvatar from "@/components/team-avatar"; +import OrgAvatar from "@/components/org-avatar"; import { getLatestTables, getPopularTables } from "@/lib/validator-queries"; import { api } from "@/trpc/server"; @@ -29,14 +29,14 @@ export default async function Page() { cookies: cookies(), }); - let teams: RouterOutputs["teams"]["userTeams"] = []; + let orgs: RouterOutputs["orgs"]["userOrgs"] = []; if (session.auth) { try { - teams = await cache(api.teams.userTeams)({ - userTeamId: session.auth.user.teamId, + orgs = await cache(api.orgs.userOrgs)({ + userOrgId: session.auth.user.orgId, }); } catch (e) { - console.log("Failed to fetch user teams:", e); + console.log("Failed to fetch user orgs:", e); } } @@ -45,15 +45,15 @@ export default async function Page() { await Promise.all( projectSlugs.map( async (slugs) => - await store.projects.projectByTeamAndProjectSlugs( - slugs.team, + await store.projects.projectByOrgAndProjectSlugs( + slugs.org, slugs.project, ), ), ) ).filter((p) => !!p) as Array< NonNullable< - Awaited> + Awaited> > >; const latestProjects = await store.projects.latestProjects(0, 1000); @@ -76,7 +76,7 @@ export default async function Page() {

- Get started by selecting one of your teams or by exploring + Get started by selecting one of your orgs or by exploring Studio Projects and Tableland tables below.

@@ -87,9 +87,9 @@ export default async function Page() {

The Tableland Studio makes it easy to design and deploy tables - on Tableland, collaborate with teammates on projects, - integrate your project with the Studio CLI, and discover what - other users are building on Tableland. + on Tableland, collaborate with orgmates on projects, integrate + your project with the Studio CLI, and discover what other + users are building on Tableland.

@@ -114,7 +114,7 @@ export default async function Page() { Then, log into the Studio using the button in the upper right corner of the screen. You'll be prompted to choose a Studio username, and then you'll be redirected to the - Studio page for your personal Team. + Studio page for your personal Org.

@@ -123,19 +123,19 @@ export default async function Page() {
{session.auth && (
- Your Teams + Your Orgs
- {teams.map((team) => ( + {orgs.map((org) => (
- +
-
{team.name}
+
{org.name}
@@ -163,15 +163,15 @@ export default async function Page() { {featuredProjects.map((item) => (
- +
- {item.team.name}/{item.project.name} + {item.org.name}/{item.project.name}
diff --git a/packages/web/components/acl.tsx b/packages/web/components/acl.tsx index 6157416f..4c0bee39 100644 --- a/packages/web/components/acl.tsx +++ b/packages/web/components/acl.tsx @@ -46,7 +46,7 @@ export default function ACL({ acl, authorizedStudioUsers, owner }: Props) { cell: AddressCell, }, { - accessorFn: (row) => row.team?.name ?? "", + accessorFn: (row) => row.org?.name ?? "", header: "Studio User", cell: UserCell, }, @@ -122,8 +122,8 @@ function UserCell({ setValue(initialValue); }, [initialValue]); - return row.original.team ? ( - {value} + return row.original.org ? ( + {value} ) : ( {value} ); diff --git a/packages/web/components/env-switcher.tsx b/packages/web/components/env-switcher.tsx index eeb58a77..0974ff9e 100644 --- a/packages/web/components/env-switcher.tsx +++ b/packages/web/components/env-switcher.tsx @@ -22,7 +22,7 @@ import { Button } from "@/components/ui/button"; type EnvSwitcherProps = { variant?: "navigation" | "select"; - team?: schema.Team; + org?: schema.Org; project?: schema.Project; selectedEnv?: schema.Environment; envs?: schema.Environment[]; @@ -34,7 +34,7 @@ type EnvSwitcherProps = { export default function EnvSwitcher({ className, variant = "navigation", - team, + org, project, selectedEnv, envs, @@ -47,9 +47,9 @@ export default function EnvSwitcher({ return (
- {variant === "navigation" && team && project && selectedEnv && ( + {variant === "navigation" && org && project && selectedEnv && ( {selectedEnv.name} diff --git a/packages/web/components/import-table-form.tsx b/packages/web/components/import-table-form.tsx index 077a1e5b..28eee214 100644 --- a/packages/web/components/import-table-form.tsx +++ b/packages/web/components/import-table-form.tsx @@ -14,7 +14,7 @@ import { SheetTitle, SheetTrigger, } from "./ui/sheet"; -import TeamSwitcher from "./team-switcher"; +import OrgSwitcher from "./org-switcher"; import ProjectSwitcher from "./project-switcher"; import EnvSwitcher from "./env-switcher"; import ChainSelector from "@/components/chain-selector"; @@ -36,7 +36,7 @@ import { api } from "@/trpc/react"; import { tablePrefix } from "@/lib/table-prefix"; export interface ImportTableFormProps { - teamPreset?: schema.Team; + orgPreset?: schema.Org; projectPreset?: schema.Project; envPreset?: schema.Environment; chainIdPreset?: number; @@ -47,7 +47,7 @@ export interface ImportTableFormProps { onOpenChange?: (open: boolean) => void; trigger?: React.ReactNode; onSuccess: ( - team: schema.Team, + org: schema.Org, project: schema.Project, def: schema.Def, environment: schema.Environment, @@ -56,7 +56,7 @@ export interface ImportTableFormProps { } export default function ImportTableForm({ - teamPreset, + orgPreset, projectPreset, envPreset, chainIdPreset, @@ -68,18 +68,18 @@ export default function ImportTableForm({ trigger, onSuccess, }: ImportTableFormProps) { - const [team, setTeam] = useState(teamPreset); + const [org, setOrg] = useState(orgPreset); const [project, setProject] = useState( projectPreset, ); const [env, setEnv] = useState(envPreset); const [defName, setDefName] = useState(""); - const { data: teams } = api.teams.userTeams.useQuery( - !teamPreset ? undefined : skipToken, + const { data: orgs } = api.orgs.userOrgs.useQuery( + !orgPreset ? undefined : skipToken, ); - const { data: projects } = api.projects.teamProjects.useQuery( - !projectPreset && team ? { teamId: team.id } : skipToken, + const { data: projects } = api.projects.orgProjects.useQuery( + !projectPreset && org ? { orgId: org.id } : skipToken, ); const { data: envs } = api.environments.projectEnvironments.useQuery( !envPreset && project ? { projectId: project.id } : skipToken, @@ -114,22 +114,22 @@ export default function ImportTableForm({ ]); useEffect(() => { - setTeam(teamPreset); + setOrg(orgPreset); setProject(projectPreset); setEnv(envPreset); - }, [envPreset, projectPreset, teamPreset]); + }, [envPreset, projectPreset, orgPreset]); const chainId = watch("chainId"); const tableId = watch("tableId"); useEffect(() => { if (!open) { - setTeam(teamPreset); + setOrg(orgPreset); setProject(projectPreset); setEnv(envPreset); form.reset(); } - }, [open, teamPreset, projectPreset, envPreset, form]); + }, [open, orgPreset, projectPreset, envPreset, form]); useEffect(() => { if (defId) return; @@ -162,14 +162,14 @@ export default function ImportTableForm({ const importTable = api.tables.importTable.useMutation({ onSuccess: ({ def, deployment }) => { - if (!team || !project || !env) return; - onSuccess(team, project, def, env, deployment); + if (!org || !project || !env) return; + onSuccess(org, project, def, env, deployment); onOpenChange?.(false); }, }); - const handleTeamSelected = (team: schema.Team) => { - setTeam(team); + const handleOrgSelected = (org: schema.Org) => { + setOrg(org); setProject(undefined); }; @@ -216,14 +216,14 @@ export default function ImportTableForm({ account and remove your data from our servers. */} - {!teamPreset && ( + {!orgPreset && (
- Team - Org +
)} @@ -232,11 +232,11 @@ export default function ImportTableForm({ Project
)} @@ -245,7 +245,7 @@ export default function ImportTableForm({ Environment { - router.push(`/${targetTeam.slug}`); + router.push(`/${targetOrg.slug}`); router.refresh(); }, }); diff --git a/packages/web/components/nav-primary.tsx b/packages/web/components/nav-primary.tsx index 893828ed..4eb82d6e 100644 --- a/packages/web/components/nav-primary.tsx +++ b/packages/web/components/nav-primary.tsx @@ -7,7 +7,7 @@ import { usePathname } from "next/navigation"; import { authAtom } from "@/store/auth"; import { cn } from "@/lib/utils"; -function links(team?: schema.Team): Array<{ +function links(org?: schema.Org): Array<{ label: string; href: string; isActive: (pathname: string) => boolean; @@ -24,11 +24,11 @@ function links(team?: schema.Team): Array<{ isActive: (_: string) => false, }, ]; - if (team) { + if (org) { links.unshift({ label: "Home", - href: `/${team?.slug}`, - isActive: (pathname: string) => pathname === `/${team.slug}`, + href: `/${org?.slug}`, + isActive: (pathname: string) => pathname === `/${org.slug}`, }); } return links; @@ -46,7 +46,7 @@ export function NavPrimary({ className={cn("flex items-center space-x-4 lg:space-x-6", className)} {...props} > - {links(auth?.personalTeam).map((link) => ( + {links(auth?.personalOrg).map((link) => ( void; trigger?: React.ReactNode; onSuccess: ( - team: schema.Team, + org: schema.Org, project: schema.Project, def: schema.Def, ) => void; } export default function NewDefForm({ - teamPreset, + orgPreset, projectPreset, schemaPreset, open, @@ -62,17 +62,17 @@ export default function NewDefForm({ onSuccess, }: NewDefFormProps) { const [openSheet, setOpenSheet] = useState(open ?? false); - const [team, setTeam] = useState(teamPreset); + const [org, setOrg] = useState(orgPreset); const [project, setProject] = useState( projectPreset, ); const [defName, setDefName] = useState(""); - const { data: teams } = api.teams.userTeams.useQuery( - !teamPreset ? undefined : skipToken, + const { data: orgs } = api.orgs.userOrgs.useQuery( + !orgPreset ? undefined : skipToken, ); - const { data: projects } = api.projects.teamProjects.useQuery( - !projectPreset && team ? { teamId: team.id } : skipToken, + const { data: projects } = api.projects.orgProjects.useQuery( + !projectPreset && org ? { orgId: org.id } : skipToken, ); const form = useForm>({ @@ -116,12 +116,12 @@ export default function NewDefForm({ useEffect(() => { if (!openSheet) { - setTeam(teamPreset); + setOrg(orgPreset); setProject(projectPreset); form.reset(); } onOpenChange?.(openSheet); - }, [openSheet, teamPreset, projectPreset, onOpenChange, form]); + }, [openSheet, orgPreset, projectPreset, onOpenChange, form]); useEffect(() => { setOpenSheet(open ?? false); @@ -134,8 +134,8 @@ export default function NewDefForm({ const newDef = api.defs.newDef.useMutation({ onSuccess: (def) => { - if (!team || !project) return; - onSuccess(team, project, def); + if (!org || !project) return; + onSuccess(org, project, def); setOpenSheet(false); }, onError: (err: any) => { @@ -151,8 +151,8 @@ export default function NewDefForm({ name: "columns", }); - const handleTeamSelected = (team: schema.Team) => { - setTeam(team); + const handleOrgSelected = (org: schema.Org) => { + setOrg(org); setProject(undefined); }; @@ -213,14 +213,14 @@ export default function NewDefForm({ account and remove your data from our servers. */} - {!teamPreset && ( + {!orgPreset && (
- Team - Org +
)} @@ -229,11 +229,11 @@ export default function NewDefForm({ Project
)} diff --git a/packages/web/components/new-project-form.tsx b/packages/web/components/new-project-form.tsx index 7aa17832..328ef42c 100644 --- a/packages/web/components/new-project-form.tsx +++ b/packages/web/components/new-project-form.tsx @@ -31,7 +31,7 @@ import { FormRootMessage } from "@/components/form"; import { cn } from "@/lib/utils"; export interface NewProjectFormProps { - team: schema.Team; + org: schema.Org; open?: boolean; onOpenChange?: (open: boolean) => void; trigger?: React.ReactNode; @@ -39,7 +39,7 @@ export interface NewProjectFormProps { } export default function NewProjectForm({ - team, + org, open, onOpenChange, trigger, @@ -49,7 +49,7 @@ export default function NewProjectForm({ const [projectName, setProjectName] = useState(""); const nameAvailableQuery = api.projects.nameAvailable.useQuery( - projectName ? { teamId: team.id, name: projectName } : skipToken, + projectName ? { orgId: org.id, name: projectName } : skipToken, { retry: false }, ); @@ -99,7 +99,7 @@ export default function NewProjectForm({ function onSubmit(values: z.infer) { newProject.mutate({ - teamId: team.id, + orgId: org.id, ...values, }); } @@ -147,7 +147,7 @@ export default function NewProjectForm({ /> - Project name must be unique within your team and at least + Project name must be unique within your org and at least three characters long. diff --git a/packages/web/components/org-avatar.tsx b/packages/web/components/org-avatar.tsx new file mode 100644 index 00000000..979fd9fb --- /dev/null +++ b/packages/web/components/org-avatar.tsx @@ -0,0 +1,14 @@ +import { type schema } from "@tableland/studio-store"; +import { Avatar, AvatarFallback, AvatarImage } from "./ui/avatar"; + +export default function OrgAvatar({ org }: { org: schema.Org }) { + return ( + + + {org.name.charAt(0)} + + ); +} diff --git a/packages/web/components/team-switcher.tsx b/packages/web/components/org-switcher.tsx similarity index 68% rename from packages/web/components/team-switcher.tsx rename to packages/web/components/org-switcher.tsx index 696fbba5..2548aa76 100644 --- a/packages/web/components/team-switcher.tsx +++ b/packages/web/components/org-switcher.tsx @@ -20,56 +20,56 @@ import { } from "@/components/ui/popover"; import { cn } from "@/lib/utils"; -type TeamSwitcherProps = { +type OrgSwitcherProps = { variant?: "navigation" | "select"; - selectedTeam?: schema.Team; - teams?: schema.Team[]; - onTeamSelected?: (team: schema.Team) => void; - onNewTeamSelected?: () => void; + selectedOrg?: schema.Org; + orgs?: schema.Org[]; + onOrgSelected?: (org: schema.Org) => void; + onNewOrgSelected?: () => void; disabled?: boolean; } & React.HTMLAttributes; -export default function TeamSwitcher({ +export default function OrgSwitcher({ className, variant = "navigation", - selectedTeam, - teams, - onTeamSelected, - onNewTeamSelected, + selectedOrg, + orgs, + onOrgSelected, + onNewOrgSelected, disabled, ...rest -}: TeamSwitcherProps) { +}: OrgSwitcherProps) { const [open, setOpen] = React.useState(false); - const teamGroups: Array<{ label: string; teams: schema.Team[] }> = [ + const orgGroups: Array<{ label: string; orgs: schema.Org[] }> = [ { - label: "Personal Team", - teams: [], + label: "Personal Org", + orgs: [], }, { - label: "Teams", - teams: [], + label: "Orgs", + orgs: [], }, ]; - teams?.forEach((team) => { - if (team.personal) { - teamGroups[0].teams.push(team); + orgs?.forEach((org) => { + if (org.personal) { + orgGroups[0].orgs.push(org); } else { - teamGroups[1].teams.push(team); + orgGroups[1].orgs.push(org); } }); return (
- {variant === "navigation" && selectedTeam && ( + {variant === "navigation" && selectedOrg && ( - {selectedTeam.name} + {selectedOrg.name} )} - {teams && ( + {orgs && ( - {/* */} - No team found. - {teamGroups.map((group) => { - if (!group.teams.length) { + {/* */} + No org found. + {orgGroups.map((group) => { + if (!group.orgs.length) { return undefined; } return ( - {group.teams.map((groupTeam) => ( + {group.orgs.map((groupOrg) => ( { - onTeamSelected?.(groupTeam); + onOrgSelected?.(groupOrg); setOpen(false); }} className="text-sm" > - {groupTeam.name} + {groupOrg.name} - {onNewTeamSelected && ( + {onNewOrgSelected && ( <> @@ -131,11 +130,11 @@ export default function TeamSwitcher({ { setOpen(false); - onNewTeamSelected(); + onNewOrgSelected(); }} > - New Team + New Org diff --git a/packages/web/components/profile.tsx b/packages/web/components/profile.tsx index 9a155cbf..d36d0bd7 100644 --- a/packages/web/components/profile.tsx +++ b/packages/web/components/profile.tsx @@ -92,7 +92,7 @@ export default function Profile({ useEffect(() => { if (isConnected && authenticated.data) { - router.push(`/${authenticated.data.personalTeam.slug}`); + router.push(`/${authenticated.data.personalOrg.slug}`); } // Not incliding authenticated.data in deps because we only // want to trigger this when isConnected changes. @@ -104,7 +104,7 @@ export default function Profile({ router.refresh(); setAuth(auth); if (!dontRedirect) { - router.push(`/${auth.personalTeam.slug}`); + router.push(`/${auth.personalOrg.slug}`); } } else { setShowRegisterDialog(true); @@ -120,7 +120,7 @@ export default function Profile({ setAuth(auth); setShowRegisterDialog(false); if (!dontRedirect) { - router.push(`/${auth.personalTeam.slug}`); + router.push(`/${auth.personalOrg.slug}`); } }; @@ -141,11 +141,11 @@ export default function Profile({ > - {auth.personalTeam.name.charAt(0)} + {auth.personalOrg.name.charAt(0)} @@ -154,7 +154,7 @@ export default function Profile({

- {auth.personalTeam.name} + {auth.personalOrg.name}

diff --git a/packages/web/components/project-switcher.tsx b/packages/web/components/project-switcher.tsx index 374106dc..0af70407 100644 --- a/packages/web/components/project-switcher.tsx +++ b/packages/web/components/project-switcher.tsx @@ -22,7 +22,7 @@ import { Button } from "@/components/ui/button"; type ProjectSwitcherProps = { variant?: "navigation" | "select"; - team?: schema.Team; + org?: schema.Org; selectedProject?: schema.Project; projects?: schema.Project[]; onProjectSelected?: (project: schema.Project) => void; @@ -33,7 +33,7 @@ type ProjectSwitcherProps = { export default function ProjectSwitcher({ className, variant = "navigation", - team, + org, selectedProject, projects, onProjectSelected, @@ -45,9 +45,9 @@ export default function ProjectSwitcher({ return (
- {variant === "navigation" && team && selectedProject && ( + {variant === "navigation" && org && selectedProject && ( {selectedProject.name} diff --git a/packages/web/components/projects-referencing-table.tsx b/packages/web/components/projects-referencing-table.tsx index c136a5a9..4f646e84 100644 --- a/packages/web/components/projects-referencing-table.tsx +++ b/packages/web/components/projects-referencing-table.tsx @@ -19,10 +19,10 @@ export default function ProjectsReferencingTable({ {references.map((p) => (
  • - {p.team.name}/{p.project.name} + {p.org.name}/{p.project.name}
  • ))} diff --git a/packages/web/components/registration-dialog.tsx b/packages/web/components/registration-dialog.tsx index d1139893..fbfb2ea0 100644 --- a/packages/web/components/registration-dialog.tsx +++ b/packages/web/components/registration-dialog.tsx @@ -39,7 +39,7 @@ export default function RegistrationDialog({ onOpenChange: (open: boolean) => void; onSuccess: (auth: Auth) => void; }) { - const [teamName, setTeamName] = useState(""); + const [orgName, setOrgName] = useState(""); const form = useForm>({ resolver: zodResolver(registerSchema), @@ -51,8 +51,8 @@ export default function RegistrationDialog({ const { setError } = form; - const nameAvailableQuery = api.teams.nameAvailable.useQuery( - teamName ? { name: teamName } : skipToken, + const nameAvailableQuery = api.orgs.nameAvailable.useQuery( + orgName ? { name: orgName } : skipToken, { retry: false }, ); @@ -103,7 +103,7 @@ export default function RegistrationDialog({ diff --git a/packages/web/components/table-menu.tsx b/packages/web/components/table-menu.tsx index ff87c89f..493f564d 100644 --- a/packages/web/components/table-menu.tsx +++ b/packages/web/components/table-menu.tsx @@ -28,18 +28,18 @@ export interface TableMenuProps { schema: Schema; chainId?: number; tableId?: string; - team?: schema.Team; + org?: schema.Org; project?: schema.Project; env?: schema.Environment; def?: { id: string; name: string; description: string; slug: string }; - isAuthorized?: RouterOutputs["teams"]["isAuthorized"]; + isAuthorized?: RouterOutputs["orgs"]["isAuthorized"]; } export default function TableMenu({ schema, chainId, tableId, - team, + org, project, env, def, @@ -67,7 +67,7 @@ export default function TableMenu({ const onEditDefSuccess = (updatedDef: schema.Def) => { if (def?.slug !== updatedDef.slug) { router.replace( - `/${team!.slug}/${project!.slug}/${env!.slug}/${updatedDef.slug}`, + `/${org!.slug}/${project!.slug}/${env!.slug}/${updatedDef.slug}`, ); } router.refresh(); @@ -88,9 +88,9 @@ export default function TableMenu({ const onDeleteTableSuccess = () => { setDeleteTableOpen(false); void defsQuery.refetch(); - if (!team || !project || !env) return; + if (!org || !project || !env) return; router.refresh(); - router.replace(`/${team.slug}/${project.slug}/${env.slug}`); + router.replace(`/${org.slug}/${project.slug}/${env.slug}`); }; const onUndeployTableSuccess = () => { @@ -100,7 +100,7 @@ export default function TableMenu({ }; const displaySettings = - !!isAuthorized && !!def && !!team && !!project && !!env; + !!isAuthorized && !!def && !!org && !!project && !!env; const displayDeploy = !!isAuthorized && !chainId && !tableId && !!def && !!env; const displayImportToStudio = @@ -153,9 +153,9 @@ export default function TableMenu({ {...importTableFormProps} open={!!importTableFormProps} onOpenChange={(open) => !open && setImportTableFormProps(undefined)} - onSuccess={(team, project, def, env) => { + onSuccess={(org, project, def, env) => { router.refresh(); - const newPathname = `/${team.slug}/${project.slug}/${env.slug}/${def.slug}`; + const newPathname = `/${org.slug}/${project.slug}/${env.slug}/${def.slug}`; if (pathname !== newPathname) { router.push(newPathname); } else { @@ -167,10 +167,10 @@ export default function TableMenu({ schemaPreset={schema} open={newDefFormOpen} onOpenChange={setNewDefFormOpen} - onSuccess={(selectedTeam, selectedProject, def) => { + onSuccess={(selectedOrg, selectedProject, def) => { router.refresh(); router.push( - `/${selectedTeam.slug}/${selectedProject.slug}${env ? `/${env.slug}/${def.slug}` : `?table=${def.slug}`}`, + `/${selectedOrg.slug}/${selectedProject.slug}${env ? `/${env.slug}/${def.slug}` : `?table=${def.slug}`}`, ); if (selectedProject.id === project?.id) { void defsQuery.refetch(); @@ -211,7 +211,7 @@ export default function TableMenu({ setImportTableFormProps({ - teamPreset: team, + orgPreset: org, projectPreset: project, envPreset: env, defId: def?.id, diff --git a/packages/web/components/table.tsx b/packages/web/components/table.tsx index 6124d6e0..15315a65 100644 --- a/packages/web/components/table.tsx +++ b/packages/web/components/table.tsx @@ -62,7 +62,7 @@ interface Props { environment?: schema.Environment; defData?: DefData; deploymentData?: DeploymentData; - isAuthorized?: RouterOutputs["teams"]["isAuthorized"]; + isAuthorized?: RouterOutputs["orgs"]["isAuthorized"]; } export default async function Table({ @@ -197,8 +197,8 @@ export default async function Table({ {ownerStudioUser && ( - Studio user {ownerStudioUser.team.name} - {ownerStudioUser.user.teamId === session.auth?.user.teamId + Studio user {ownerStudioUser.org.name} + {ownerStudioUser.user.orgId === session.auth?.user.orgId ? " (you)" : ""} diff --git a/packages/web/components/team-avatar.tsx b/packages/web/components/team-avatar.tsx deleted file mode 100644 index 3cefe09e..00000000 --- a/packages/web/components/team-avatar.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { type schema } from "@tableland/studio-store"; -import { Avatar, AvatarFallback, AvatarImage } from "./ui/avatar"; - -export default function TeamAvatar({ team }: { team: schema.Team }) { - return ( - - - {team.name.charAt(0)} - - ); -} diff --git a/packages/web/components/wallet-status.tsx b/packages/web/components/wallet-status.tsx index a54ef97d..b5131d8b 100644 --- a/packages/web/components/wallet-status.tsx +++ b/packages/web/components/wallet-status.tsx @@ -56,8 +56,8 @@ export default function WalletStatus({ - Studio user {authorizedStudioUser.team.name} - {authorizedStudioUser.user.teamId === auth?.user.teamId + Studio user {authorizedStudioUser.org.name} + {authorizedStudioUser.user.orgId === auth?.user.orgId ? " (you)" : ""} diff --git a/packages/web/lib/api-helpers.ts b/packages/web/lib/api-helpers.ts index b8a2d471..bf18e36e 100644 --- a/packages/web/lib/api-helpers.ts +++ b/packages/web/lib/api-helpers.ts @@ -3,15 +3,13 @@ import { notFound } from "next/navigation"; import { cache } from "react"; import { api } from "@/trpc/server"; -export async function teamBySlug(slug: string) { - return await catch404( - async () => await cache(api.teams.teamBySlug)({ slug }), - ); +export async function orgBySlug(slug: string) { + return await catch404(async () => await cache(api.orgs.orgBySlug)({ slug })); } -export async function projectBySlug(slug: string, teamId?: string) { +export async function projectBySlug(slug: string, orgId?: string) { return await catch404( - async () => await cache(api.projects.projectBySlug)({ teamId, slug }), + async () => await cache(api.projects.projectBySlug)({ orgId, slug }), ); } diff --git a/packages/web/lib/featured-projects.ts b/packages/web/lib/featured-projects.ts index 9bf36705..67818b4e 100644 --- a/packages/web/lib/featured-projects.ts +++ b/packages/web/lib/featured-projects.ts @@ -9,32 +9,32 @@ export async function featuredProjectSlugs() { } else if (baseUrl === "https://studio.tableland.xyz") { return [ { - team: "partners", + org: "partners", project: "dimo", // Only a sample of non-active testnet tables (over 140 total) }, { - team: "partners", + org: "partners", project: "hideout-labs", // Non-active testnets tables }, // { - // team: "partners", + // org: "partners", // project: "drvrs", // No tables // }, { - team: "tableland", + org: "tableland", project: "studio", // Active Tableland Studio tables }, { - team: "tableland", + org: "tableland", project: "rigs", // Active Tableland Rigs tables }, ]; } else { // It's a preview deployment. return [ - { team: "aaron", project: "four-project" }, - { team: "joe", project: "students" }, - { team: "aaron", project: "with-timestamp" }, + { org: "aaron", project: "four-project" }, + { org: "joe", project: "students" }, + { org: "aaron", project: "with-timestamp" }, ]; } }