Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 48 additions & 1 deletion ee/apps/den-api/src/orgs.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { and, asc, eq } from "@openwork-ee/den-db/drizzle"
import { and, asc, eq, inArray } from "@openwork-ee/den-db/drizzle"
import {
AuthSessionTable,
AuthUserTable,
Expand Down Expand Up @@ -98,6 +98,13 @@ export type OrganizationContext = {
createdAt: Date | null
updatedAt: Date | null
}>
teams: Array<{
id: typeof TeamTable.$inferSelect.id
name: string
createdAt: Date
updatedAt: Date
memberIds: MemberId[]
}>
}

export type MemberTeamSummary = {
Expand Down Expand Up @@ -611,6 +618,8 @@ export async function getOrganizationContextForUser(input: {
.where(eq(OrganizationRoleTable.organizationId, organization.id))
.orderBy(asc(OrganizationRoleTable.createdAt))

const teams = await listOrganizationTeams(organization.id)

const builtInDynamicRoleNames = new Set(Object.keys(denDefaultDynamicOrganizationRoles))

return {
Expand Down Expand Up @@ -655,9 +664,47 @@ export async function getOrganizationContextForUser(input: {
updatedAt: role.updatedAt,
})),
],
teams,
} satisfies OrganizationContext
}

async function listOrganizationTeams(organizationId: OrgId) {
const teams = await db
.select({
id: TeamTable.id,
name: TeamTable.name,
createdAt: TeamTable.createdAt,
updatedAt: TeamTable.updatedAt,
})
.from(TeamTable)
.where(eq(TeamTable.organizationId, organizationId))
.orderBy(asc(TeamTable.createdAt))

if (teams.length === 0) {
return []
}

const memberships = await db
.select({
teamId: TeamMemberTable.teamId,
orgMembershipId: TeamMemberTable.orgMembershipId,
})
.from(TeamMemberTable)
.where(inArray(TeamMemberTable.teamId, teams.map((team) => team.id)))

const memberIdsByTeamId = new Map<typeof TeamTable.$inferSelect.id, MemberId[]>()
for (const membership of memberships) {
const existing = memberIdsByTeamId.get(membership.teamId) ?? []
existing.push(membership.orgMembershipId)
memberIdsByTeamId.set(membership.teamId, existing)
}

return teams.map((team) => ({
...team,
memberIds: memberIdsByTeamId.get(team.id) ?? [],
}))
}

export async function listTeamsForMember(input: {
organizationId: OrgId
memberId: MemberRow["id"]
Expand Down
2 changes: 2 additions & 0 deletions ee/apps/den-api/src/routes/org/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { registerOrgInvitationRoutes } from "./invitations.js"
import { registerOrgMemberRoutes } from "./members.js"
import { registerOrgRoleRoutes } from "./roles.js"
import { registerOrgSkillRoutes } from "./skills.js"
import { registerOrgTeamRoutes } from "./teams.js"
import { registerOrgTemplateRoutes } from "./templates.js"

export function registerOrgRoutes<T extends { Variables: OrgRouteVariables }>(app: Hono<T>) {
Expand All @@ -13,5 +14,6 @@ export function registerOrgRoutes<T extends { Variables: OrgRouteVariables }>(ap
registerOrgMemberRoutes(app)
registerOrgRoleRoutes(app)
registerOrgSkillRoutes(app)
registerOrgTeamRoutes(app)
registerOrgTemplateRoutes(app)
}
24 changes: 24 additions & 0 deletions ee/apps/den-api/src/routes/org/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,30 @@ export function ensureInviteManager(c: { get: (key: "organizationContext") => Or
}
}

export function ensureTeamManager(c: { get: (key: "organizationContext") => OrgRouteVariables["organizationContext"] }) {
const payload = c.get("organizationContext")
if (!payload) {
return {
ok: false as const,
response: {
error: "organization_not_found",
},
}
}

if (payload.currentMember.isOwner || memberHasRole(payload.currentMember.role, "admin")) {
return { ok: true as const }
}

return {
ok: false as const,
response: {
error: "forbidden",
message: "Only organization owners and admins can manage teams.",
},
}
}

export function createInvitationId() {
return createDenTypeId("invitation")
}
Expand Down
75 changes: 75 additions & 0 deletions ee/apps/den-api/src/routes/org/skills.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,19 @@ const createSkillSchema = z.object({
shared: z.enum(["org", "public"]).nullable().optional(),
})

const updateSkillSchema = z.object({
skillText: z.string().trim().min(1).optional(),
shared: z.enum(["org", "public"]).nullable().optional(),
}).superRefine((value, ctx) => {
if (value.skillText === undefined && value.shared === undefined) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["skillText"],
message: "Provide at least one field to update.",
})
}
})

const createSkillHubSchema = z.object({
name: z.string().trim().min(1).max(255),
description: z.string().trim().max(65535).nullish().transform((value) => value || null),
Expand Down Expand Up @@ -310,6 +323,68 @@ export function registerOrgSkillRoutes<T extends { Variables: OrgRouteVariables
},
)

app.patch(
"/v1/orgs/:orgId/skills/:skillId",
requireUserMiddleware,
paramValidator(orgSkillParamsSchema),
resolveOrganizationContextMiddleware,
jsonValidator(updateSkillSchema),
async (c) => {
const payload = c.get("organizationContext")
const params = c.req.valid("param")
const input = c.req.valid("json")

let skillId: SkillId
try {
skillId = parseSkillId(params.skillId)
} catch {
return c.json({ error: "skill_not_found" }, 404)
}

const skillRows = await db
.select()
.from(SkillTable)
.where(and(eq(SkillTable.id, skillId), eq(SkillTable.organizationId, payload.organization.id)))
.limit(1)

const skill = skillRows[0]
if (!skill) {
return c.json({ error: "skill_not_found" }, 404)
}

if (!canManageSkill(payload, skill)) {
return c.json({ error: "forbidden", message: "Only the skill creator or an org admin can update skills." }, 403)
}

const nextSkillText = input.skillText ?? skill.skillText
const metadata = parseSkillMetadata(nextSkillText)
const updatedAt = new Date()
const nextShared = input.shared === undefined ? skill.shared : input.shared

await db
.update(SkillTable)
.set({
title: metadata.title,
description: metadata.description,
skillText: nextSkillText,
shared: nextShared,
updatedAt,
})
.where(eq(SkillTable.id, skill.id))

return c.json({
skill: {
...skill,
title: metadata.title,
description: metadata.description,
skillText: nextSkillText,
shared: nextShared,
updatedAt,
},
})
},
)

app.post(
"/v1/orgs/:orgId/skill-hubs",
requireUserMiddleware,
Expand Down
Loading
Loading