diff --git a/ee/apps/den-api/src/middleware/member-teams.ts b/ee/apps/den-api/src/middleware/member-teams.ts index 003a8f38..d14b716c 100644 --- a/ee/apps/den-api/src/middleware/member-teams.ts +++ b/ee/apps/den-api/src/middleware/member-teams.ts @@ -17,7 +17,7 @@ export const resolveMemberTeamsMiddleware: MiddlewareHandler<{ const memberTeams = await listTeamsForMember({ organizationId: context.organization.id, - userId: context.currentMember.userId, + memberId: context.currentMember.id, }) c.set("memberTeams", memberTeams) diff --git a/ee/apps/den-api/src/orgs.ts b/ee/apps/den-api/src/orgs.ts index 42de70f2..b4de442a 100644 --- a/ee/apps/den-api/src/orgs.ts +++ b/ee/apps/den-api/src/orgs.ts @@ -17,6 +17,7 @@ type UserId = typeof AuthUserTable.$inferSelect.id type SessionId = typeof AuthSessionTable.$inferSelect.id type OrgId = typeof OrganizationTable.$inferSelect.id type MemberRow = typeof MemberTable.$inferSelect +type MemberId = MemberRow["id"] type InvitationRow = typeof InvitationTable.$inferSelect export type InvitationStatus = "pending" | "accepted" | "canceled" | "expired" @@ -61,14 +62,14 @@ export type OrganizationContext = { updatedAt: Date } currentMember: { - id: string + id: MemberId userId: UserId role: string createdAt: Date isOwner: boolean } members: Array<{ - id: string + id: MemberId userId: UserId role: string createdAt: Date @@ -295,14 +296,14 @@ async function acceptInvitation(invitation: InvitationRow, userId: UserId) { const existingTeamMember = await db .select({ id: TeamMemberTable.id }) .from(TeamMemberTable) - .where(and(eq(TeamMemberTable.teamId, invitation.teamId), eq(TeamMemberTable.userId, userId))) + .where(and(eq(TeamMemberTable.teamId, invitation.teamId), eq(TeamMemberTable.orgMembershipId, member.id))) .limit(1) if (!existingTeamMember[0]) { await db.insert(TeamMemberTable).values({ id: createDenTypeId("teamMember"), teamId: invitation.teamId, - userId, + orgMembershipId: member.id, }) } } @@ -659,7 +660,7 @@ export async function getOrganizationContextForUser(input: { export async function listTeamsForMember(input: { organizationId: OrgId - userId: UserId + memberId: MemberRow["id"] }) { return db .select({ @@ -671,7 +672,7 @@ export async function listTeamsForMember(input: { }) .from(TeamMemberTable) .innerJoin(TeamTable, eq(TeamMemberTable.teamId, TeamTable.id)) - .where(and(eq(TeamTable.organizationId, input.organizationId), eq(TeamMemberTable.userId, input.userId))) + .where(and(eq(TeamTable.organizationId, input.organizationId), eq(TeamMemberTable.orgMembershipId, input.memberId))) .orderBy(asc(TeamTable.createdAt)) } @@ -699,7 +700,7 @@ export async function removeOrganizationMember(input: { for (const team of teams) { await tx .delete(TeamMemberTable) - .where(and(eq(TeamMemberTable.teamId, team.id), eq(TeamMemberTable.userId, member.userId))) + .where(and(eq(TeamMemberTable.teamId, team.id), eq(TeamMemberTable.orgMembershipId, member.id))) } await tx.delete(MemberTable).where(eq(MemberTable.id, member.id)) diff --git a/ee/apps/den-api/src/routes/org/index.ts b/ee/apps/den-api/src/routes/org/index.ts index d9e90795..08a20ed0 100644 --- a/ee/apps/den-api/src/routes/org/index.ts +++ b/ee/apps/den-api/src/routes/org/index.ts @@ -4,6 +4,7 @@ import { registerOrgCoreRoutes } from "./core.js" import { registerOrgInvitationRoutes } from "./invitations.js" import { registerOrgMemberRoutes } from "./members.js" import { registerOrgRoleRoutes } from "./roles.js" +import { registerOrgSkillRoutes } from "./skills.js" import { registerOrgTemplateRoutes } from "./templates.js" export function registerOrgRoutes(app: Hono) { @@ -11,5 +12,6 @@ export function registerOrgRoutes(ap registerOrgInvitationRoutes(app) registerOrgMemberRoutes(app) registerOrgRoleRoutes(app) + registerOrgSkillRoutes(app) registerOrgTemplateRoutes(app) } diff --git a/ee/apps/den-api/src/routes/org/skills.ts b/ee/apps/den-api/src/routes/org/skills.ts new file mode 100644 index 00000000..eebf2afc --- /dev/null +++ b/ee/apps/den-api/src/routes/org/skills.ts @@ -0,0 +1,875 @@ +import { and, desc, eq, inArray, isNotNull, or } from "@openwork-ee/den-db/drizzle" +import { + AuthUserTable, + MemberTable, + SkillHubMemberTable, + SkillHubSkillTable, + SkillHubTable, + SkillTable, + TeamTable, +} from "@openwork-ee/den-db/schema" +import { createDenTypeId, normalizeDenTypeId } from "@openwork-ee/utils/typeid" +import type { Hono } from "hono" +import { z } from "zod" +import { db } from "../../db.js" +import { + jsonValidator, + paramValidator, + requireUserMiddleware, + resolveMemberTeamsMiddleware, + resolveOrganizationContextMiddleware, +} from "../../middleware/index.js" +import type { MemberTeamsContext } from "../../middleware/member-teams.js" +import type { OrgRouteVariables } from "./shared.js" +import { idParamSchema, memberHasRole, orgIdParamSchema } from "./shared.js" + +const createSkillSchema = z.object({ + skillText: z.string().trim().min(1), + shared: z.enum(["org", "public"]).nullable().optional(), +}) + +const createSkillHubSchema = z.object({ + name: z.string().trim().min(1).max(255), + description: z.string().trim().max(65535).nullish().transform((value) => value || null), +}) + +const updateSkillHubSchema = z.object({ + name: z.string().trim().min(1).max(255).optional(), + description: z.string().trim().max(65535).nullable().optional(), +}).superRefine((value, ctx) => { + if (value.name === undefined && value.description === undefined) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ["name"], + message: "Provide at least one field to update.", + }) + } +}) + +const addSkillToHubSchema = z.object({ + skillId: z.string().trim().min(1), +}) + +const addSkillHubAccessSchema = z.object({ + orgMembershipId: z.string().trim().min(1).optional(), + teamId: z.string().trim().min(1).optional(), +}).superRefine((value, ctx) => { + const count = Number(Boolean(value.orgMembershipId)) + Number(Boolean(value.teamId)) + if (count !== 1) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ["orgMembershipId"], + message: "Provide exactly one of orgMembershipId or teamId.", + }) + } +}) + +type SkillId = typeof SkillTable.$inferSelect.id +type SkillHubId = typeof SkillHubTable.$inferSelect.id +type SkillHubMemberId = typeof SkillHubMemberTable.$inferSelect.id +type TeamId = typeof TeamTable.$inferSelect.id +type MemberId = typeof MemberTable.$inferSelect.id +type SkillRow = typeof SkillTable.$inferSelect +type SkillHubRow = typeof SkillHubTable.$inferSelect + +const orgSkillHubParamsSchema = orgIdParamSchema.extend(idParamSchema("skillHubId").shape) +const orgSkillParamsSchema = orgIdParamSchema.extend(idParamSchema("skillId").shape) +const orgSkillHubSkillParamsSchema = orgSkillHubParamsSchema.extend(idParamSchema("skillId").shape) +const orgSkillHubAccessParamsSchema = orgSkillHubParamsSchema.extend(idParamSchema("accessId").shape) + +function parseSkillId(value: string) { + return normalizeDenTypeId("skill", value) +} + +function parseSkillHubId(value: string) { + return normalizeDenTypeId("skillHub", value) +} + +function parseSkillHubMemberId(value: string) { + return normalizeDenTypeId("skillHubMember", value) +} + +function parseMemberId(value: string) { + return normalizeDenTypeId("member", value) +} + +function parseTeamId(value: string) { + return normalizeDenTypeId("team", value) +} + +function parseSkillMetadata(skillText: string) { + const lines = skillText + .split(/\r?\n/g) + .map((line) => line.trim()) + .filter(Boolean) + + const cleanup = (value: string) => value + .replace(/^#{1,6}\s+/, "") + .replace(/^[-*+]\s+/, "") + .replace(/^title\s*:\s*/i, "") + .replace(/^description\s*:\s*/i, "") + .trim() + + const title = cleanup(lines[0] ?? "") || "Untitled skill" + const description = lines.slice(1).map(cleanup).find(Boolean) ?? null + + return { + title: title.slice(0, 255), + description: description ? description.slice(0, 65535) : null, + } +} + +function isOrganizationAdmin(payload: { currentMember: { isOwner: boolean; role: string } }) { + return payload.currentMember.isOwner || memberHasRole(payload.currentMember.role, "admin") +} + +function canManageSkill(payload: { currentMember: { id: MemberId; isOwner: boolean; role: string } }, skill: SkillRow) { + return isOrganizationAdmin(payload) || skill.createdByOrgMembershipId === payload.currentMember.id +} + +function canManageHub(payload: { currentMember: { id: MemberId; isOwner: boolean; role: string } }, skillHub: SkillHubRow) { + return isOrganizationAdmin(payload) || skillHub.createdByOrgMembershipId === payload.currentMember.id +} + +async function listAccessibleHubMemberships(input: { + organizationId: typeof SkillHubTable.$inferSelect.organizationId + currentMemberId: MemberId + memberTeams: Array<{ id: TeamId }> +}) { + const teamIds = input.memberTeams.map((team) => team.id) + const accessWhere = teamIds.length > 0 + ? and( + eq(SkillHubTable.organizationId, input.organizationId), + or( + eq(SkillHubMemberTable.orgMembershipId, input.currentMemberId), + inArray(SkillHubMemberTable.teamId, teamIds), + ), + ) + : and( + eq(SkillHubTable.organizationId, input.organizationId), + eq(SkillHubMemberTable.orgMembershipId, input.currentMemberId), + ) + + return db + .select({ + id: SkillHubMemberTable.id, + skillHubId: SkillHubMemberTable.skillHubId, + orgMembershipId: SkillHubMemberTable.orgMembershipId, + teamId: SkillHubMemberTable.teamId, + createdAt: SkillHubMemberTable.createdAt, + }) + .from(SkillHubMemberTable) + .innerJoin(SkillHubTable, eq(SkillHubMemberTable.skillHubId, SkillHubTable.id)) + .where(accessWhere) +} + +async function listAccessibleSkillIds(input: { + organizationId: typeof SkillHubTable.$inferSelect.organizationId + currentMemberId: MemberId + memberTeams: Array<{ id: TeamId }> +}) { + const memberships = await listAccessibleHubMemberships(input) + const hubIds = [...new Set(memberships.map((membership) => membership.skillHubId))] + if (hubIds.length === 0) { + return new Set() + } + + const rows = await db + .select({ skillId: SkillHubSkillTable.skillId }) + .from(SkillHubSkillTable) + .where(inArray(SkillHubSkillTable.skillHubId, hubIds)) + + return new Set(rows.map((row) => row.skillId)) +} + +function canViewSkill(input: { + currentMemberId: MemberId + skill: SkillRow + accessibleSkillIds: Set +}) { + return input.skill.createdByOrgMembershipId === input.currentMemberId + || input.skill.shared !== null + || input.accessibleSkillIds.has(input.skill.id) +} + +export function registerOrgSkillRoutes }>(app: Hono) { + app.post( + "/v1/orgs/:orgId/skills", + requireUserMiddleware, + paramValidator(orgIdParamSchema), + resolveOrganizationContextMiddleware, + jsonValidator(createSkillSchema), + async (c) => { + const payload = c.get("organizationContext") + const input = c.req.valid("json") + const now = new Date() + const skillId = createDenTypeId("skill") + const metadata = parseSkillMetadata(input.skillText) + + await db.insert(SkillTable).values({ + id: skillId, + organizationId: payload.organization.id, + createdByOrgMembershipId: payload.currentMember.id, + title: metadata.title, + description: metadata.description, + skillText: input.skillText, + shared: input.shared ?? null, + createdAt: now, + updatedAt: now, + }) + + return c.json({ + skill: { + id: skillId, + organizationId: payload.organization.id, + createdByOrgMembershipId: payload.currentMember.id, + title: metadata.title, + description: metadata.description, + skillText: input.skillText, + shared: input.shared ?? null, + createdAt: now, + updatedAt: now, + }, + }, 201) + }, + ) + + app.get( + "/v1/orgs/:orgId/skills", + requireUserMiddleware, + paramValidator(orgIdParamSchema), + resolveOrganizationContextMiddleware, + resolveMemberTeamsMiddleware, + async (c) => { + const payload = c.get("organizationContext") + const memberTeams = c.get("memberTeams") ?? [] + const accessibleSkillIds = await listAccessibleSkillIds({ + organizationId: payload.organization.id, + currentMemberId: payload.currentMember.id, + memberTeams, + }) + + const skills = await db + .select() + .from(SkillTable) + .where(eq(SkillTable.organizationId, payload.organization.id)) + .orderBy(desc(SkillTable.updatedAt)) + + return c.json({ + skills: skills + .filter((skill) => canViewSkill({ + currentMemberId: payload.currentMember.id, + skill, + accessibleSkillIds, + })) + .map((skill) => ({ + ...skill, + canManage: canManageSkill(payload, skill), + })), + }) + }, + ) + + app.delete( + "/v1/orgs/:orgId/skills/:skillId", + requireUserMiddleware, + paramValidator(orgSkillParamsSchema), + resolveOrganizationContextMiddleware, + async (c) => { + const payload = c.get("organizationContext") + const params = c.req.valid("param") + + 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 delete skills." }, 403) + } + + await db.transaction(async (tx) => { + await tx.delete(SkillHubSkillTable).where(eq(SkillHubSkillTable.skillId, skill.id)) + await tx.delete(SkillTable).where(eq(SkillTable.id, skill.id)) + }) + + return c.body(null, 204) + }, + ) + + app.post( + "/v1/orgs/:orgId/skill-hubs", + requireUserMiddleware, + paramValidator(orgIdParamSchema), + resolveOrganizationContextMiddleware, + jsonValidator(createSkillHubSchema), + async (c) => { + const payload = c.get("organizationContext") + const input = c.req.valid("json") + const now = new Date() + const skillHubId = createDenTypeId("skillHub") + + await db.transaction(async (tx) => { + await tx.insert(SkillHubTable).values({ + id: skillHubId, + organizationId: payload.organization.id, + createdByOrgMembershipId: payload.currentMember.id, + name: input.name, + description: input.description, + createdAt: now, + updatedAt: now, + }) + + await tx.insert(SkillHubMemberTable).values({ + id: createDenTypeId("skillHubMember"), + skillHubId, + orgMembershipId: payload.currentMember.id, + teamId: null, + createdAt: now, + }) + }) + + return c.json({ + skillHub: { + id: skillHubId, + organizationId: payload.organization.id, + createdByOrgMembershipId: payload.currentMember.id, + name: input.name, + description: input.description, + createdAt: now, + updatedAt: now, + }, + }, 201) + }, + ) + + app.get( + "/v1/orgs/:orgId/skill-hubs", + requireUserMiddleware, + paramValidator(orgIdParamSchema), + resolveOrganizationContextMiddleware, + resolveMemberTeamsMiddleware, + async (c) => { + const payload = c.get("organizationContext") + const memberTeams = c.get("memberTeams") ?? [] + const accessibleMemberships = await listAccessibleHubMemberships({ + organizationId: payload.organization.id, + currentMemberId: payload.currentMember.id, + memberTeams, + }) + const skillHubIds = [...new Set(accessibleMemberships.map((membership) => membership.skillHubId))] + + if (skillHubIds.length === 0) { + return c.json({ skillHubs: [] }) + } + + const skillHubs = await db + .select() + .from(SkillHubTable) + .where(and(eq(SkillHubTable.organizationId, payload.organization.id), inArray(SkillHubTable.id, skillHubIds))) + .orderBy(desc(SkillHubTable.updatedAt)) + + const skillLinks = await db + .select({ skillHubId: SkillHubSkillTable.skillHubId, skillId: SkillHubSkillTable.skillId }) + .from(SkillHubSkillTable) + .where(inArray(SkillHubSkillTable.skillHubId, skillHubIds)) + + const skillIds = [...new Set(skillLinks.map((link) => link.skillId))] + const skills = skillIds.length === 0 + ? [] + : await db + .select() + .from(SkillTable) + .where(and(eq(SkillTable.organizationId, payload.organization.id), inArray(SkillTable.id, skillIds))) + + const memberAccessRows = await db + .select({ + access: { + id: SkillHubMemberTable.id, + skillHubId: SkillHubMemberTable.skillHubId, + createdAt: SkillHubMemberTable.createdAt, + }, + member: { + id: MemberTable.id, + role: MemberTable.role, + }, + user: { + id: AuthUserTable.id, + name: AuthUserTable.name, + email: AuthUserTable.email, + image: AuthUserTable.image, + }, + }) + .from(SkillHubMemberTable) + .innerJoin(MemberTable, eq(SkillHubMemberTable.orgMembershipId, MemberTable.id)) + .innerJoin(AuthUserTable, eq(MemberTable.userId, AuthUserTable.id)) + .where(and(inArray(SkillHubMemberTable.skillHubId, skillHubIds), isNotNull(SkillHubMemberTable.orgMembershipId))) + + const teamAccessRows = await db + .select({ + access: { + id: SkillHubMemberTable.id, + skillHubId: SkillHubMemberTable.skillHubId, + createdAt: SkillHubMemberTable.createdAt, + }, + team: { + id: TeamTable.id, + name: TeamTable.name, + createdAt: TeamTable.createdAt, + updatedAt: TeamTable.updatedAt, + }, + }) + .from(SkillHubMemberTable) + .innerJoin(TeamTable, eq(SkillHubMemberTable.teamId, TeamTable.id)) + .where(and(inArray(SkillHubMemberTable.skillHubId, skillHubIds), isNotNull(SkillHubMemberTable.teamId))) + + const skillsById = new Map(skills.map((skill) => [skill.id, skill])) + const skillsByHubId = new Map() + for (const link of skillLinks) { + const skill = skillsById.get(link.skillId) + if (!skill) { + continue + } + + const existing = skillsByHubId.get(link.skillHubId) ?? [] + existing.push(skill) + skillsByHubId.set(link.skillHubId, existing) + } + + const memberAccessByHubId = new Map() + for (const row of memberAccessRows) { + const existing = memberAccessByHubId.get(row.access.skillHubId) ?? [] + existing.push(row) + memberAccessByHubId.set(row.access.skillHubId, existing) + } + + const teamAccessByHubId = new Map() + for (const row of teamAccessRows) { + const existing = teamAccessByHubId.get(row.access.skillHubId) ?? [] + existing.push(row) + teamAccessByHubId.set(row.access.skillHubId, existing) + } + + const accessibleViaByHubId = new Map() + for (const row of accessibleMemberships) { + const existing = accessibleViaByHubId.get(row.skillHubId) ?? { orgMembershipIds: [], teamIds: [] } + if (row.orgMembershipId && !existing.orgMembershipIds.includes(row.orgMembershipId)) { + existing.orgMembershipIds.push(row.orgMembershipId) + } + if (row.teamId && !existing.teamIds.includes(row.teamId)) { + existing.teamIds.push(row.teamId) + } + accessibleViaByHubId.set(row.skillHubId, existing) + } + + return c.json({ + skillHubs: skillHubs.map((skillHub) => ({ + ...skillHub, + canManage: canManageHub(payload, skillHub), + accessibleVia: accessibleViaByHubId.get(skillHub.id) ?? { orgMembershipIds: [], teamIds: [] }, + skills: skillsByHubId.get(skillHub.id) ?? [], + access: { + members: (memberAccessByHubId.get(skillHub.id) ?? []).map((row) => ({ + id: row.access.id, + orgMembershipId: row.member.id, + role: row.member.role, + user: row.user, + createdAt: row.access.createdAt, + })), + teams: (teamAccessByHubId.get(skillHub.id) ?? []).map((row) => ({ + id: row.access.id, + teamId: row.team.id, + name: row.team.name, + createdAt: row.team.createdAt, + updatedAt: row.team.updatedAt, + })), + }, + })), + }) + }, + ) + + app.patch( + "/v1/orgs/:orgId/skill-hubs/:skillHubId", + requireUserMiddleware, + paramValidator(orgSkillHubParamsSchema), + resolveOrganizationContextMiddleware, + jsonValidator(updateSkillHubSchema), + async (c) => { + const payload = c.get("organizationContext") + const params = c.req.valid("param") + const input = c.req.valid("json") + + let skillHubId: SkillHubId + try { + skillHubId = parseSkillHubId(params.skillHubId) + } catch { + return c.json({ error: "skill_hub_not_found" }, 404) + } + + const skillHubRows = await db + .select() + .from(SkillHubTable) + .where(and(eq(SkillHubTable.id, skillHubId), eq(SkillHubTable.organizationId, payload.organization.id))) + .limit(1) + + const skillHub = skillHubRows[0] + if (!skillHub) { + return c.json({ error: "skill_hub_not_found" }, 404) + } + + if (!canManageHub(payload, skillHub)) { + return c.json({ error: "forbidden", message: "Only the hub creator or an org admin can update hubs." }, 403) + } + + const updatedAt = new Date() + const nextName = input.name ?? skillHub.name + const nextDescription = input.description === undefined ? skillHub.description : input.description + + await db + .update(SkillHubTable) + .set({ + name: nextName, + description: nextDescription, + updatedAt, + }) + .where(eq(SkillHubTable.id, skillHub.id)) + + return c.json({ + skillHub: { + ...skillHub, + name: nextName, + description: nextDescription, + updatedAt, + }, + }) + }, + ) + + app.delete( + "/v1/orgs/:orgId/skill-hubs/:skillHubId", + requireUserMiddleware, + paramValidator(orgSkillHubParamsSchema), + resolveOrganizationContextMiddleware, + async (c) => { + const payload = c.get("organizationContext") + const params = c.req.valid("param") + + let skillHubId: SkillHubId + try { + skillHubId = parseSkillHubId(params.skillHubId) + } catch { + return c.json({ error: "skill_hub_not_found" }, 404) + } + + const skillHubRows = await db + .select() + .from(SkillHubTable) + .where(and(eq(SkillHubTable.id, skillHubId), eq(SkillHubTable.organizationId, payload.organization.id))) + .limit(1) + + const skillHub = skillHubRows[0] + if (!skillHub) { + return c.json({ error: "skill_hub_not_found" }, 404) + } + + if (!canManageHub(payload, skillHub)) { + return c.json({ error: "forbidden", message: "Only the hub creator or an org admin can delete hubs." }, 403) + } + + await db.transaction(async (tx) => { + await tx.delete(SkillHubMemberTable).where(eq(SkillHubMemberTable.skillHubId, skillHub.id)) + await tx.delete(SkillHubSkillTable).where(eq(SkillHubSkillTable.skillHubId, skillHub.id)) + await tx.delete(SkillHubTable).where(eq(SkillHubTable.id, skillHub.id)) + }) + + return c.body(null, 204) + }, + ) + + app.post( + "/v1/orgs/:orgId/skill-hubs/:skillHubId/skills", + requireUserMiddleware, + paramValidator(orgSkillHubParamsSchema), + resolveOrganizationContextMiddleware, + jsonValidator(addSkillToHubSchema), + async (c) => { + const payload = c.get("organizationContext") + const params = c.req.valid("param") + const input = c.req.valid("json") + + let skillHubId: SkillHubId + let skillId: SkillId + try { + skillHubId = parseSkillHubId(params.skillHubId) + skillId = parseSkillId(input.skillId) + } catch { + return c.json({ error: "not_found" }, 404) + } + + const skillHubRows = await db + .select() + .from(SkillHubTable) + .where(and(eq(SkillHubTable.id, skillHubId), eq(SkillHubTable.organizationId, payload.organization.id))) + .limit(1) + + const skillHub = skillHubRows[0] + if (!skillHub) { + return c.json({ error: "skill_hub_not_found" }, 404) + } + + if (!canManageHub(payload, skillHub)) { + return c.json({ error: "forbidden", message: "Only the hub creator or an org admin can manage hub skills." }, 403) + } + + 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) && skill.shared === null) { + return c.json({ + error: "forbidden", + message: "Private skills can only be added to hubs by their creator or an org admin.", + }, 403) + } + + const existing = await db + .select({ id: SkillHubSkillTable.id }) + .from(SkillHubSkillTable) + .where(and(eq(SkillHubSkillTable.skillHubId, skillHubId), eq(SkillHubSkillTable.skillId, skill.id))) + .limit(1) + + if (existing[0]) { + return c.json({ error: "skill_hub_skill_exists" }, 409) + } + + await db.insert(SkillHubSkillTable).values({ + id: createDenTypeId("skillHubSkill"), + skillHubId, + skillId: skill.id, + addedByOrgMembershipId: payload.currentMember.id, + createdAt: new Date(), + }) + + return c.json({ success: true }, 201) + }, + ) + + app.delete( + "/v1/orgs/:orgId/skill-hubs/:skillHubId/skills/:skillId", + requireUserMiddleware, + paramValidator(orgSkillHubSkillParamsSchema), + resolveOrganizationContextMiddleware, + async (c) => { + const payload = c.get("organizationContext") + const params = c.req.valid("param") + + let skillHubId: SkillHubId + let skillId: SkillId + try { + skillHubId = parseSkillHubId(params.skillHubId) + skillId = parseSkillId(params.skillId) + } catch { + return c.json({ error: "not_found" }, 404) + } + + const skillHubRows = await db + .select() + .from(SkillHubTable) + .where(and(eq(SkillHubTable.id, skillHubId), eq(SkillHubTable.organizationId, payload.organization.id))) + .limit(1) + + const skillHub = skillHubRows[0] + if (!skillHub) { + return c.json({ error: "skill_hub_not_found" }, 404) + } + + if (!canManageHub(payload, skillHub)) { + return c.json({ error: "forbidden", message: "Only the hub creator or an org admin can manage hub skills." }, 403) + } + + const existing = await db + .select({ id: SkillHubSkillTable.id }) + .from(SkillHubSkillTable) + .where(and(eq(SkillHubSkillTable.skillHubId, skillHubId), eq(SkillHubSkillTable.skillId, skillId))) + .limit(1) + + if (!existing[0]) { + return c.json({ error: "skill_hub_skill_not_found" }, 404) + } + + await db + .delete(SkillHubSkillTable) + .where(and(eq(SkillHubSkillTable.skillHubId, skillHubId), eq(SkillHubSkillTable.skillId, skillId))) + + return c.body(null, 204) + }, + ) + + app.post( + "/v1/orgs/:orgId/skill-hubs/:skillHubId/access", + requireUserMiddleware, + paramValidator(orgSkillHubParamsSchema), + resolveOrganizationContextMiddleware, + jsonValidator(addSkillHubAccessSchema), + async (c) => { + const payload = c.get("organizationContext") + const params = c.req.valid("param") + const input = c.req.valid("json") + + let skillHubId: SkillHubId + let orgMembershipId: MemberId | null = null + let teamId: TeamId | null = null + try { + skillHubId = parseSkillHubId(params.skillHubId) + orgMembershipId = input.orgMembershipId ? parseMemberId(input.orgMembershipId) : null + teamId = input.teamId ? parseTeamId(input.teamId) : null + } catch { + return c.json({ error: "access_target_not_found" }, 404) + } + + const skillHubRows = await db + .select() + .from(SkillHubTable) + .where(and(eq(SkillHubTable.id, skillHubId), eq(SkillHubTable.organizationId, payload.organization.id))) + .limit(1) + + const skillHub = skillHubRows[0] + if (!skillHub) { + return c.json({ error: "skill_hub_not_found" }, 404) + } + + if (!canManageHub(payload, skillHub)) { + return c.json({ error: "forbidden", message: "Only the hub creator or an org admin can manage access." }, 403) + } + + if (orgMembershipId) { + const memberRows = await db + .select({ id: MemberTable.id }) + .from(MemberTable) + .where(and(eq(MemberTable.id, orgMembershipId), eq(MemberTable.organizationId, payload.organization.id))) + .limit(1) + + if (!memberRows[0]) { + return c.json({ error: "member_not_found" }, 404) + } + } + + if (teamId) { + const teamRows = await db + .select({ id: TeamTable.id }) + .from(TeamTable) + .where(and(eq(TeamTable.id, teamId), eq(TeamTable.organizationId, payload.organization.id))) + .limit(1) + + if (!teamRows[0]) { + return c.json({ error: "team_not_found" }, 404) + } + } + + const existing = await db + .select({ id: SkillHubMemberTable.id }) + .from(SkillHubMemberTable) + .where( + orgMembershipId + ? and(eq(SkillHubMemberTable.skillHubId, skillHubId), eq(SkillHubMemberTable.orgMembershipId, orgMembershipId)) + : and(eq(SkillHubMemberTable.skillHubId, skillHubId), eq(SkillHubMemberTable.teamId, teamId as TeamId)), + ) + .limit(1) + + if (existing[0]) { + return c.json({ error: "skill_hub_access_exists" }, 409) + } + + const accessId = createDenTypeId("skillHubMember") + const createdAt = new Date() + + await db.insert(SkillHubMemberTable).values({ + id: accessId, + skillHubId, + orgMembershipId, + teamId, + createdAt, + }) + + return c.json({ + access: { + id: accessId, + skillHubId, + orgMembershipId, + teamId, + createdAt, + }, + }, 201) + }, + ) + + app.delete( + "/v1/orgs/:orgId/skill-hubs/:skillHubId/access/:accessId", + requireUserMiddleware, + paramValidator(orgSkillHubAccessParamsSchema), + resolveOrganizationContextMiddleware, + async (c) => { + const payload = c.get("organizationContext") + const params = c.req.valid("param") + + let skillHubId: SkillHubId + let accessId: SkillHubMemberId + try { + skillHubId = parseSkillHubId(params.skillHubId) + accessId = parseSkillHubMemberId(params.accessId) + } catch { + return c.json({ error: "not_found" }, 404) + } + + const skillHubRows = await db + .select() + .from(SkillHubTable) + .where(and(eq(SkillHubTable.id, skillHubId), eq(SkillHubTable.organizationId, payload.organization.id))) + .limit(1) + + const skillHub = skillHubRows[0] + if (!skillHub) { + return c.json({ error: "skill_hub_not_found" }, 404) + } + + if (!canManageHub(payload, skillHub)) { + return c.json({ error: "forbidden", message: "Only the hub creator or an org admin can manage access." }, 403) + } + + const accessRows = await db + .select() + .from(SkillHubMemberTable) + .where(and(eq(SkillHubMemberTable.id, accessId), eq(SkillHubMemberTable.skillHubId, skillHubId))) + .limit(1) + + const access = accessRows[0] + if (!access) { + return c.json({ error: "skill_hub_access_not_found" }, 404) + } + + await db.delete(SkillHubMemberTable).where(eq(SkillHubMemberTable.id, access.id)) + return c.body(null, 204) + }, + ) +} diff --git a/ee/apps/den-api/src/routes/workers/shared.ts b/ee/apps/den-api/src/routes/workers/shared.ts index 7f0fb6e9..996104c5 100644 --- a/ee/apps/den-api/src/routes/workers/shared.ts +++ b/ee/apps/den-api/src/routes/workers/shared.ts @@ -4,7 +4,7 @@ import { AuditEventTable, AuthUserTable, DaytonaSandboxTable, - OrgMembershipTable, + MemberTable, WorkerBundleTable, WorkerInstanceTable, WorkerTable, @@ -63,7 +63,7 @@ export type WorkerRouteVariables = AuthContextVariables & Partial randomBytes(32).toString("hex") diff --git a/ee/packages/den-db/README.md b/ee/packages/den-db/README.md new file mode 100644 index 00000000..9b2b6c7e --- /dev/null +++ b/ee/packages/den-db/README.md @@ -0,0 +1,36 @@ +# den-db + +`@openwork-ee/den-db` owns the Den database schema and migration history. + +## Canonical workflow + +- Keep schema changes in `src/schema/**`. +- Keep generated SQL migrations in `drizzle/`. +- Always generate new migrations with Drizzle from this package. +- Do not create migrations from `den-api`, `den-controller`, or other apps. + +## Commands + +Generate a migration after editing the schema: + +```bash +pnpm --dir ee/packages/den-db db:generate +``` + +Apply schema directly to a development database: + +```bash +pnpm --dir ee/packages/den-db db:push +``` + +Run Drizzle migrations against a configured database: + +```bash +pnpm --dir ee/packages/den-db db:migrate +``` + +## Notes + +- `db:generate` is the default path for new migration files. +- `drizzle/meta/` must stay in sync with the SQL migration history so future generation stays incremental. +- Only repair `drizzle/meta/` manually when recovering broken Drizzle history. diff --git a/ee/packages/den-db/drizzle.config.ts b/ee/packages/den-db/drizzle.config.ts index ce9d5789..ea72da42 100644 --- a/ee/packages/den-db/drizzle.config.ts +++ b/ee/packages/den-db/drizzle.config.ts @@ -1,13 +1,13 @@ import "./src/load-env.ts" -import path from "node:path" -import { fileURLToPath } from "node:url" import { defineConfig } from "drizzle-kit" import { parseMySqlConnectionConfig } from "./src/mysql-config.ts" -const currentDir = path.dirname(fileURLToPath(import.meta.url)) - const databaseUrl = process.env.DATABASE_URL?.trim() +function isGenerateCommand() { + return process.argv.some((arg) => arg === "generate") +} + function resolveDrizzleDbCredentials() { if (databaseUrl) { return parseMySqlConnectionConfig(databaseUrl) @@ -18,6 +18,14 @@ function resolveDrizzleDbCredentials() { const password = process.env.DATABASE_PASSWORD ?? "" if (!host || !user) { + if (isGenerateCommand()) { + return { + host: "127.0.0.1", + user: "root", + password: "", + } + } + throw new Error("Provide DATABASE_URL for mysql or DATABASE_HOST/DATABASE_USERNAME/DATABASE_PASSWORD for planetscale") } @@ -30,7 +38,7 @@ function resolveDrizzleDbCredentials() { export default defineConfig({ dialect: "mysql", - schema: path.join(currentDir, "src", "schema.ts"), - out: path.join(currentDir, "..", "..", "apps", "den-controller", "drizzle"), + schema: "./src/schema.ts", + out: "./drizzle", dbCredentials: resolveDrizzleDbCredentials(), }) diff --git a/ee/packages/den-db/drizzle/0001_desktop_handoff_grants.sql b/ee/packages/den-db/drizzle/0001_desktop_handoff_grants.sql new file mode 100644 index 00000000..dc6f1204 --- /dev/null +++ b/ee/packages/den-db/drizzle/0001_desktop_handoff_grants.sql @@ -0,0 +1,13 @@ +CREATE TABLE `desktop_handoff_grant` ( + `id` varchar(64) NOT NULL, + `user_id` varchar(64) NOT NULL, + `session_token` text NOT NULL, + `expires_at` timestamp(3) NOT NULL, + `consumed_at` timestamp(3), + `created_at` timestamp(3) NOT NULL DEFAULT (now(3)), + CONSTRAINT `desktop_handoff_grant_id` PRIMARY KEY(`id`) +); +--> statement-breakpoint +CREATE INDEX `desktop_handoff_grant_user_id` ON `desktop_handoff_grant` (`user_id`); +--> statement-breakpoint +CREATE INDEX `desktop_handoff_grant_expires_at` ON `desktop_handoff_grant` (`expires_at`); diff --git a/ee/packages/den-db/drizzle/0002_worker_activity_heartbeat.sql b/ee/packages/den-db/drizzle/0002_worker_activity_heartbeat.sql new file mode 100644 index 00000000..88a85187 --- /dev/null +++ b/ee/packages/den-db/drizzle/0002_worker_activity_heartbeat.sql @@ -0,0 +1,12 @@ +ALTER TABLE `worker` + ADD `last_heartbeat_at` timestamp(3); +--> statement-breakpoint +ALTER TABLE `worker` + ADD `last_active_at` timestamp(3); +--> statement-breakpoint +CREATE INDEX `worker_last_heartbeat_at` ON `worker` (`last_heartbeat_at`); +--> statement-breakpoint +CREATE INDEX `worker_last_active_at` ON `worker` (`last_active_at`); +--> statement-breakpoint +ALTER TABLE `worker_token` + MODIFY COLUMN `scope` enum('client','host','activity') NOT NULL; diff --git a/ee/packages/den-db/drizzle/0003_rate_limit.sql b/ee/packages/den-db/drizzle/0003_rate_limit.sql new file mode 100644 index 00000000..eaed29bd --- /dev/null +++ b/ee/packages/den-db/drizzle/0003_rate_limit.sql @@ -0,0 +1,8 @@ +CREATE TABLE `rate_limit` ( + `id` varchar(255) NOT NULL, + `key` varchar(512) NOT NULL, + `count` int NOT NULL DEFAULT 0, + `last_request` bigint NOT NULL, + CONSTRAINT `rate_limit_id` PRIMARY KEY(`id`), + CONSTRAINT `rate_limit_key` UNIQUE(`key`) +); diff --git a/ee/packages/den-db/drizzle/0004_organization_plugin.manual.sql b/ee/packages/den-db/drizzle/0004_organization_plugin.manual.sql new file mode 100644 index 00000000..63fe1e78 --- /dev/null +++ b/ee/packages/den-db/drizzle/0004_organization_plugin.manual.sql @@ -0,0 +1,105 @@ +-- Manual SQL variant for MySQL/Vitess consoles. +-- - No `--> statement-breakpoint` markers +-- - No PREPARE/EXECUTE dynamic SQL +-- - Avoids `ADD COLUMN IF NOT EXISTS` (not supported in some Vitess setups) + +-- Run these only if the columns are missing. +-- Check first: +-- SELECT column_name FROM information_schema.columns +-- WHERE table_schema = DATABASE() AND table_name = 'session' +-- AND column_name IN ('active_organization_id', 'active_team_id'); + +ALTER TABLE `session` + ADD COLUMN `active_organization_id` varchar(64) NULL; + +ALTER TABLE `session` + ADD COLUMN `active_team_id` varchar(64) NULL; + +CREATE TABLE IF NOT EXISTS `organization` ( + `id` varchar(64) NOT NULL, + `name` varchar(255) NOT NULL, + `slug` varchar(255) NOT NULL, + `logo` varchar(2048), + `metadata` text, + `created_at` timestamp(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + `updated_at` timestamp(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + CONSTRAINT `organization_id` PRIMARY KEY(`id`), + CONSTRAINT `organization_slug` UNIQUE(`slug`) +); + +CREATE TABLE IF NOT EXISTS `member` ( + `id` varchar(64) NOT NULL, + `organization_id` varchar(64) NOT NULL, + `user_id` varchar(64) NOT NULL, + `role` varchar(255) NOT NULL DEFAULT 'member', + `created_at` timestamp(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + CONSTRAINT `member_id` PRIMARY KEY(`id`), + CONSTRAINT `member_organization_user` UNIQUE(`organization_id`, `user_id`), + KEY `member_organization_id` (`organization_id`), + KEY `member_user_id` (`user_id`) +); + +CREATE TABLE IF NOT EXISTS `invitation` ( + `id` varchar(64) NOT NULL, + `organization_id` varchar(64) NOT NULL, + `email` varchar(255) NOT NULL, + `role` varchar(255) NOT NULL, + `status` varchar(32) NOT NULL DEFAULT 'pending', + `team_id` varchar(64) DEFAULT NULL, + `inviter_id` varchar(64) NOT NULL, + `expires_at` timestamp(3) NOT NULL, + `created_at` timestamp(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + CONSTRAINT `invitation_id` PRIMARY KEY(`id`), + KEY `invitation_organization_id` (`organization_id`), + KEY `invitation_email` (`email`), + KEY `invitation_status` (`status`), + KEY `invitation_team_id` (`team_id`) +); + +CREATE TABLE IF NOT EXISTS `team` ( + `id` varchar(64) NOT NULL, + `name` varchar(255) NOT NULL, + `organization_id` varchar(64) NOT NULL, + `created_at` timestamp(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + `updated_at` timestamp(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + CONSTRAINT `team_id` PRIMARY KEY(`id`), + CONSTRAINT `team_organization_name` UNIQUE(`organization_id`, `name`), + KEY `team_organization_id` (`organization_id`) +); + +CREATE TABLE IF NOT EXISTS `team_member` ( + `id` varchar(64) NOT NULL, + `team_id` varchar(64) NOT NULL, + `user_id` varchar(64) NOT NULL, + `created_at` timestamp(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + CONSTRAINT `team_member_id` PRIMARY KEY(`id`), + CONSTRAINT `team_member_team_user` UNIQUE(`team_id`, `user_id`), + KEY `team_member_team_id` (`team_id`), + KEY `team_member_user_id` (`user_id`) +); + +CREATE TABLE IF NOT EXISTS `organization_role` ( + `id` varchar(64) NOT NULL, + `organization_id` varchar(64) NOT NULL, + `role` varchar(255) NOT NULL, + `permission` text NOT NULL, + `created_at` timestamp(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + `updated_at` timestamp(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + CONSTRAINT `organization_role_id` PRIMARY KEY(`id`), + CONSTRAINT `organization_role_name` UNIQUE(`organization_id`, `role`), + KEY `organization_role_organization_id` (`organization_id`) +); + +-- Optional legacy backfill. Run only if these legacy tables exist: +-- org +-- org_membership +-- +-- INSERT INTO `organization` (`id`, `name`, `slug`, `logo`, `metadata`, `created_at`, `updated_at`) +-- SELECT `id`, `name`, `slug`, NULL, NULL, `created_at`, `updated_at` +-- FROM `org` +-- WHERE `id` NOT IN (SELECT `id` FROM `organization`); +-- +-- INSERT INTO `member` (`id`, `organization_id`, `user_id`, `role`, `created_at`) +-- SELECT `id`, `org_id`, `user_id`, `role`, `created_at` +-- FROM `org_membership` +-- WHERE `id` NOT IN (SELECT `id` FROM `member`); diff --git a/ee/packages/den-db/drizzle/0004_organization_plugin.sql b/ee/packages/den-db/drizzle/0004_organization_plugin.sql new file mode 100644 index 00000000..646d9ef9 --- /dev/null +++ b/ee/packages/den-db/drizzle/0004_organization_plugin.sql @@ -0,0 +1,152 @@ +SET @has_active_organization_id := ( + SELECT COUNT(*) + FROM information_schema.columns + WHERE table_schema = DATABASE() + AND table_name = 'session' + AND column_name = 'active_organization_id' +); +--> statement-breakpoint +SET @add_active_organization_id_sql := IF( + @has_active_organization_id = 0, + 'ALTER TABLE `session` ADD COLUMN `active_organization_id` varchar(64) NULL', + 'SELECT 1' +); +--> statement-breakpoint +PREPARE add_active_organization_id_stmt FROM @add_active_organization_id_sql; +--> statement-breakpoint +EXECUTE add_active_organization_id_stmt; +--> statement-breakpoint +DEALLOCATE PREPARE add_active_organization_id_stmt; +--> statement-breakpoint +SET @has_active_team_id := ( + SELECT COUNT(*) + FROM information_schema.columns + WHERE table_schema = DATABASE() + AND table_name = 'session' + AND column_name = 'active_team_id' +); +--> statement-breakpoint +SET @add_active_team_id_sql := IF( + @has_active_team_id = 0, + 'ALTER TABLE `session` ADD COLUMN `active_team_id` varchar(64) NULL', + 'SELECT 1' +); +--> statement-breakpoint +PREPARE add_active_team_id_stmt FROM @add_active_team_id_sql; +--> statement-breakpoint +EXECUTE add_active_team_id_stmt; +--> statement-breakpoint +DEALLOCATE PREPARE add_active_team_id_stmt; +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS `organization` ( + `id` varchar(64) NOT NULL, + `name` varchar(255) NOT NULL, + `slug` varchar(255) NOT NULL, + `logo` varchar(2048), + `metadata` text, + `created_at` timestamp(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + `updated_at` timestamp(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + CONSTRAINT `organization_id` PRIMARY KEY(`id`), + CONSTRAINT `organization_slug` UNIQUE(`slug`) +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS `member` ( + `id` varchar(64) NOT NULL, + `organization_id` varchar(64) NOT NULL, + `user_id` varchar(64) NOT NULL, + `role` varchar(255) NOT NULL DEFAULT 'member', + `created_at` timestamp(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + CONSTRAINT `member_id` PRIMARY KEY(`id`), + CONSTRAINT `member_organization_user` UNIQUE(`organization_id`, `user_id`), + KEY `member_organization_id` (`organization_id`), + KEY `member_user_id` (`user_id`) +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS `invitation` ( + `id` varchar(64) NOT NULL, + `organization_id` varchar(64) NOT NULL, + `email` varchar(255) NOT NULL, + `role` varchar(255) NOT NULL, + `status` varchar(32) NOT NULL DEFAULT 'pending', + `team_id` varchar(64) DEFAULT NULL, + `inviter_id` varchar(64) NOT NULL, + `expires_at` timestamp(3) NOT NULL, + `created_at` timestamp(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + CONSTRAINT `invitation_id` PRIMARY KEY(`id`), + KEY `invitation_organization_id` (`organization_id`), + KEY `invitation_email` (`email`), + KEY `invitation_status` (`status`), + KEY `invitation_team_id` (`team_id`) +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS `team` ( + `id` varchar(64) NOT NULL, + `name` varchar(255) NOT NULL, + `organization_id` varchar(64) NOT NULL, + `created_at` timestamp(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + `updated_at` timestamp(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + CONSTRAINT `team_id` PRIMARY KEY(`id`), + CONSTRAINT `team_organization_name` UNIQUE(`organization_id`, `name`), + KEY `team_organization_id` (`organization_id`) +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS `team_member` ( + `id` varchar(64) NOT NULL, + `team_id` varchar(64) NOT NULL, + `user_id` varchar(64) NOT NULL, + `created_at` timestamp(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + CONSTRAINT `team_member_id` PRIMARY KEY(`id`), + CONSTRAINT `team_member_team_user` UNIQUE(`team_id`, `user_id`), + KEY `team_member_team_id` (`team_id`), + KEY `team_member_user_id` (`user_id`) +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS `organization_role` ( + `id` varchar(64) NOT NULL, + `organization_id` varchar(64) NOT NULL, + `role` varchar(255) NOT NULL, + `permission` text NOT NULL, + `created_at` timestamp(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + `updated_at` timestamp(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + CONSTRAINT `organization_role_id` PRIMARY KEY(`id`), + CONSTRAINT `organization_role_name` UNIQUE(`organization_id`, `role`), + KEY `organization_role_organization_id` (`organization_id`) +); +--> statement-breakpoint +SET @has_legacy_org_table := ( + SELECT COUNT(*) + FROM information_schema.tables + WHERE table_schema = DATABASE() + AND table_name = 'org' +); +--> statement-breakpoint +SET @copy_legacy_org_sql := IF( + @has_legacy_org_table > 0, + 'INSERT INTO `organization` (`id`, `name`, `slug`, `logo`, `metadata`, `created_at`, `updated_at`) SELECT `id`, `name`, `slug`, NULL, NULL, `created_at`, `updated_at` FROM `org` WHERE `id` NOT IN (SELECT `id` FROM `organization`)', + 'SELECT 1' +); +--> statement-breakpoint +PREPARE copy_legacy_org_stmt FROM @copy_legacy_org_sql; +--> statement-breakpoint +EXECUTE copy_legacy_org_stmt; +--> statement-breakpoint +DEALLOCATE PREPARE copy_legacy_org_stmt; +--> statement-breakpoint +SET @has_legacy_org_membership_table := ( + SELECT COUNT(*) + FROM information_schema.tables + WHERE table_schema = DATABASE() + AND table_name = 'org_membership' +); +--> statement-breakpoint +SET @copy_legacy_org_membership_sql := IF( + @has_legacy_org_membership_table > 0, + 'INSERT INTO `member` (`id`, `organization_id`, `user_id`, `role`, `created_at`) SELECT `id`, `org_id`, `user_id`, `role`, `created_at` FROM `org_membership` WHERE `id` NOT IN (SELECT `id` FROM `member`)', + 'SELECT 1' +); +--> statement-breakpoint +PREPARE copy_legacy_org_membership_stmt FROM @copy_legacy_org_membership_sql; +--> statement-breakpoint +EXECUTE copy_legacy_org_membership_stmt; +--> statement-breakpoint +DEALLOCATE PREPARE copy_legacy_org_membership_stmt; diff --git a/ee/packages/den-db/drizzle/0005_temp_template_sharing.sql b/ee/packages/den-db/drizzle/0005_temp_template_sharing.sql new file mode 100644 index 00000000..21348a06 --- /dev/null +++ b/ee/packages/den-db/drizzle/0005_temp_template_sharing.sql @@ -0,0 +1,14 @@ +CREATE TABLE IF NOT EXISTS `temp_template_sharing` ( + `id` varchar(64) NOT NULL, + `organization_id` varchar(64) NOT NULL, + `creator_member_id` varchar(64) NOT NULL, + `creator_user_id` varchar(64) NOT NULL, + `name` varchar(255) NOT NULL, + `template_json` text NOT NULL, + `created_at` timestamp(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + `updated_at` timestamp(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + CONSTRAINT `temp_template_sharing_id` PRIMARY KEY(`id`), + KEY `temp_template_sharing_org_id` (`organization_id`), + KEY `temp_template_sharing_creator_member_id` (`creator_member_id`), + KEY `temp_template_sharing_creator_user_id` (`creator_user_id`) +); diff --git a/ee/packages/den-db/drizzle/0006_skill_hub.sql b/ee/packages/den-db/drizzle/0006_skill_hub.sql new file mode 100644 index 00000000..92e82562 --- /dev/null +++ b/ee/packages/den-db/drizzle/0006_skill_hub.sql @@ -0,0 +1,88 @@ +ALTER TABLE `team_member` + ADD COLUMN `org_membership_id` varchar(64) NULL AFTER `team_id`; +--> statement-breakpoint +UPDATE `team_member` tm + INNER JOIN `team` t ON t.`id` = tm.`team_id` + INNER JOIN `member` m ON m.`organization_id` = t.`organization_id` AND m.`user_id` = tm.`user_id` +SET tm.`org_membership_id` = m.`id` +WHERE tm.`org_membership_id` IS NULL; +--> statement-breakpoint +DROP INDEX `team_member_team_user` ON `team_member`; +--> statement-breakpoint +DROP INDEX `team_member_user_id` ON `team_member`; +--> statement-breakpoint +ALTER TABLE `team_member` + MODIFY COLUMN `org_membership_id` varchar(64) NOT NULL; +--> statement-breakpoint +CREATE INDEX `team_member_org_membership_id` ON `team_member` (`org_membership_id`); +--> statement-breakpoint +ALTER TABLE `team_member` + ADD CONSTRAINT `team_member_team_org_membership` UNIQUE(`team_id`, `org_membership_id`); +--> statement-breakpoint +ALTER TABLE `team_member` + DROP COLUMN `user_id`; +--> statement-breakpoint +CREATE TABLE `skill` ( + `id` varchar(64) NOT NULL, + `organization_id` varchar(64) NOT NULL, + `created_by_org_membership_id` varchar(64) NOT NULL, + `title` varchar(255) NOT NULL, + `description` text, + `skill_text` text NOT NULL, + `shared` enum('org','public'), + `created_at` timestamp(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + `updated_at` timestamp(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + CONSTRAINT `skill_id` PRIMARY KEY(`id`) +); +--> statement-breakpoint +CREATE INDEX `skill_organization_id` ON `skill` (`organization_id`); +--> statement-breakpoint +CREATE INDEX `skill_created_by_org_membership_id` ON `skill` (`created_by_org_membership_id`); +--> statement-breakpoint +CREATE INDEX `skill_shared` ON `skill` (`shared`); +--> statement-breakpoint +CREATE TABLE `skill_hub` ( + `id` varchar(64) NOT NULL, + `organization_id` varchar(64) NOT NULL, + `created_by_org_membership_id` varchar(64) NOT NULL, + `name` varchar(255) NOT NULL, + `description` text, + `created_at` timestamp(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + `updated_at` timestamp(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + CONSTRAINT `skill_hub_id` PRIMARY KEY(`id`) +); +--> statement-breakpoint +CREATE INDEX `skill_hub_organization_id` ON `skill_hub` (`organization_id`); +--> statement-breakpoint +CREATE INDEX `skill_hub_created_by_org_membership_id` ON `skill_hub` (`created_by_org_membership_id`); +--> statement-breakpoint +CREATE TABLE `skill_hub_skill` ( + `id` varchar(64) NOT NULL, + `skill_hub_id` varchar(64) NOT NULL, + `skill_id` varchar(64) NOT NULL, + `org_membership_id` varchar(64) NOT NULL, + `created_at` timestamp(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + CONSTRAINT `skill_hub_skill_id` PRIMARY KEY(`id`), + CONSTRAINT `skill_hub_skill_hub_skill` UNIQUE(`skill_hub_id`, `skill_id`) +); +--> statement-breakpoint +CREATE INDEX `skill_hub_skill_skill_hub_id` ON `skill_hub_skill` (`skill_hub_id`); +--> statement-breakpoint +CREATE INDEX `skill_hub_skill_skill_id` ON `skill_hub_skill` (`skill_id`); +--> statement-breakpoint +CREATE TABLE `skill_hub_member` ( + `id` varchar(64) NOT NULL, + `skill_hub_id` varchar(64) NOT NULL, + `org_membership_id` varchar(64), + `team_id` varchar(64), + `created_at` timestamp(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + CONSTRAINT `skill_hub_member_id` PRIMARY KEY(`id`), + CONSTRAINT `skill_hub_member_hub_org_membership` UNIQUE(`skill_hub_id`, `org_membership_id`), + CONSTRAINT `skill_hub_member_hub_team` UNIQUE(`skill_hub_id`, `team_id`) +); +--> statement-breakpoint +CREATE INDEX `skill_hub_member_skill_hub_id` ON `skill_hub_member` (`skill_hub_id`); +--> statement-breakpoint +CREATE INDEX `skill_hub_member_org_membership_id` ON `skill_hub_member` (`org_membership_id`); +--> statement-breakpoint +CREATE INDEX `skill_hub_member_team_id` ON `skill_hub_member` (`team_id`); diff --git a/ee/packages/den-db/drizzle/meta/0006_snapshot.json b/ee/packages/den-db/drizzle/meta/0006_snapshot.json new file mode 100644 index 00000000..0e7f3d27 --- /dev/null +++ b/ee/packages/den-db/drizzle/meta/0006_snapshot.json @@ -0,0 +1,2066 @@ +{ + "version": "5", + "dialect": "mysql", + "id": "01bf4a61-490e-449d-b17d-295d8546bd22", + "prevId": "1da3dfe9-e476-4347-8b0e-0969b517ab84", + "tables": { + "account": { + "name": "account", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + } + }, + "indexes": { + "account_user_id": { + "name": "account_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "account_id": { + "name": "account_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "session": { + "name": "session", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "active_organization_id": { + "name": "active_organization_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "active_team_id": { + "name": "active_team_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + } + }, + "indexes": { + "session_token": { + "name": "session_token", + "columns": [ + "token" + ], + "isUnique": true + }, + "session_user_id": { + "name": "session_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "session_id": { + "name": "session_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user": { + "name": "user", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + } + }, + "indexes": { + "user_email": { + "name": "user_email", + "columns": [ + "email" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "user_id": { + "name": "user_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "verification": { + "name": "verification", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "identifier": { + "name": "identifier", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + } + }, + "indexes": { + "verification_identifier": { + "name": "verification_identifier", + "columns": [ + "identifier" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "verification_id": { + "name": "verification_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "desktop_handoff_grant": { + "name": "desktop_handoff_grant", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_token": { + "name": "session_token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "consumed_at": { + "name": "consumed_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": { + "desktop_handoff_grant_user_id": { + "name": "desktop_handoff_grant_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "desktop_handoff_grant_expires_at": { + "name": "desktop_handoff_grant_expires_at", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "desktop_handoff_grant_id": { + "name": "desktop_handoff_grant_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "invitation": { + "name": "invitation", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pending'" + }, + "team_id": { + "name": "team_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "inviter_id": { + "name": "inviter_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": { + "invitation_organization_id": { + "name": "invitation_organization_id", + "columns": [ + "organization_id" + ], + "isUnique": false + }, + "invitation_email": { + "name": "invitation_email", + "columns": [ + "email" + ], + "isUnique": false + }, + "invitation_status": { + "name": "invitation_status", + "columns": [ + "status" + ], + "isUnique": false + }, + "invitation_team_id": { + "name": "invitation_team_id", + "columns": [ + "team_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "invitation_id": { + "name": "invitation_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "member": { + "name": "member", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'member'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": { + "member_organization_id": { + "name": "member_organization_id", + "columns": [ + "organization_id" + ], + "isUnique": false + }, + "member_user_id": { + "name": "member_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "member_organization_user": { + "name": "member_organization_user", + "columns": [ + "organization_id", + "user_id" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "member_id": { + "name": "member_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "organization": { + "name": "organization", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "slug": { + "name": "slug", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "logo": { + "name": "logo", + "type": "varchar(2048)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + } + }, + "indexes": { + "organization_slug": { + "name": "organization_slug", + "columns": [ + "slug" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "organization_id": { + "name": "organization_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "organization_role": { + "name": "organization_role", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission": { + "name": "permission", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + } + }, + "indexes": { + "organization_role_organization_id": { + "name": "organization_role_organization_id", + "columns": [ + "organization_id" + ], + "isUnique": false + }, + "organization_role_name": { + "name": "organization_role_name", + "columns": [ + "organization_id", + "role" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "organization_role_id": { + "name": "organization_role_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "temp_template_sharing": { + "name": "temp_template_sharing", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "creator_member_id": { + "name": "creator_member_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "creator_user_id": { + "name": "creator_user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "template_json": { + "name": "template_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + } + }, + "indexes": { + "temp_template_sharing_org_id": { + "name": "temp_template_sharing_org_id", + "columns": [ + "organization_id" + ], + "isUnique": false + }, + "temp_template_sharing_creator_member_id": { + "name": "temp_template_sharing_creator_member_id", + "columns": [ + "creator_member_id" + ], + "isUnique": false + }, + "temp_template_sharing_creator_user_id": { + "name": "temp_template_sharing_creator_user_id", + "columns": [ + "creator_user_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "temp_template_sharing_id": { + "name": "temp_template_sharing_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "skill_hub_member": { + "name": "skill_hub_member", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "skill_hub_id": { + "name": "skill_hub_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "org_membership_id": { + "name": "org_membership_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "team_id": { + "name": "team_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": { + "skill_hub_member_skill_hub_id": { + "name": "skill_hub_member_skill_hub_id", + "columns": [ + "skill_hub_id" + ], + "isUnique": false + }, + "skill_hub_member_org_membership_id": { + "name": "skill_hub_member_org_membership_id", + "columns": [ + "org_membership_id" + ], + "isUnique": false + }, + "skill_hub_member_team_id": { + "name": "skill_hub_member_team_id", + "columns": [ + "team_id" + ], + "isUnique": false + }, + "skill_hub_member_hub_org_membership": { + "name": "skill_hub_member_hub_org_membership", + "columns": [ + "skill_hub_id", + "org_membership_id" + ], + "isUnique": true + }, + "skill_hub_member_hub_team": { + "name": "skill_hub_member_hub_team", + "columns": [ + "skill_hub_id", + "team_id" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "skill_hub_member_id": { + "name": "skill_hub_member_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "skill_hub_skill": { + "name": "skill_hub_skill", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "skill_hub_id": { + "name": "skill_hub_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "skill_id": { + "name": "skill_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "org_membership_id": { + "name": "org_membership_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": { + "skill_hub_skill_skill_hub_id": { + "name": "skill_hub_skill_skill_hub_id", + "columns": [ + "skill_hub_id" + ], + "isUnique": false + }, + "skill_hub_skill_skill_id": { + "name": "skill_hub_skill_skill_id", + "columns": [ + "skill_id" + ], + "isUnique": false + }, + "skill_hub_skill_hub_skill": { + "name": "skill_hub_skill_hub_skill", + "columns": [ + "skill_hub_id", + "skill_id" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "skill_hub_skill_id": { + "name": "skill_hub_skill_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "skill_hub": { + "name": "skill_hub", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_by_org_membership_id": { + "name": "created_by_org_membership_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + } + }, + "indexes": { + "skill_hub_organization_id": { + "name": "skill_hub_organization_id", + "columns": [ + "organization_id" + ], + "isUnique": false + }, + "skill_hub_created_by_org_membership_id": { + "name": "skill_hub_created_by_org_membership_id", + "columns": [ + "created_by_org_membership_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "skill_hub_id": { + "name": "skill_hub_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "skill": { + "name": "skill", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_by_org_membership_id": { + "name": "created_by_org_membership_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "skill_text": { + "name": "skill_text", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "shared": { + "name": "shared", + "type": "enum('org','public')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + } + }, + "indexes": { + "skill_organization_id": { + "name": "skill_organization_id", + "columns": [ + "organization_id" + ], + "isUnique": false + }, + "skill_created_by_org_membership_id": { + "name": "skill_created_by_org_membership_id", + "columns": [ + "created_by_org_membership_id" + ], + "isUnique": false + }, + "skill_shared": { + "name": "skill_shared", + "columns": [ + "shared" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "skill_id": { + "name": "skill_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "team_member": { + "name": "team_member", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "team_id": { + "name": "team_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "org_membership_id": { + "name": "org_membership_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": { + "team_member_team_id": { + "name": "team_member_team_id", + "columns": [ + "team_id" + ], + "isUnique": false + }, + "team_member_org_membership_id": { + "name": "team_member_org_membership_id", + "columns": [ + "org_membership_id" + ], + "isUnique": false + }, + "team_member_team_org_membership": { + "name": "team_member_team_org_membership", + "columns": [ + "team_id", + "org_membership_id" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "team_member_id": { + "name": "team_member_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "team": { + "name": "team", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + } + }, + "indexes": { + "team_organization_id": { + "name": "team_organization_id", + "columns": [ + "organization_id" + ], + "isUnique": false + }, + "team_organization_name": { + "name": "team_organization_name", + "columns": [ + "organization_id", + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "team_id": { + "name": "team_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "audit_event": { + "name": "audit_event", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "org_id": { + "name": "org_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "worker_id": { + "name": "worker_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "actor_user_id": { + "name": "actor_user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "action": { + "name": "action", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "payload": { + "name": "payload", + "type": "json", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": { + "audit_event_org_id": { + "name": "audit_event_org_id", + "columns": [ + "org_id" + ], + "isUnique": false + }, + "audit_event_worker_id": { + "name": "audit_event_worker_id", + "columns": [ + "worker_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "audit_event_id": { + "name": "audit_event_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "daytona_sandbox": { + "name": "daytona_sandbox", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "worker_id": { + "name": "worker_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sandbox_id": { + "name": "sandbox_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "workspace_volume_id": { + "name": "workspace_volume_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "data_volume_id": { + "name": "data_volume_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "signed_preview_url": { + "name": "signed_preview_url", + "type": "varchar(2048)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "signed_preview_url_expires_at": { + "name": "signed_preview_url_expires_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "region": { + "name": "region", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + } + }, + "indexes": { + "daytona_sandbox_worker_id": { + "name": "daytona_sandbox_worker_id", + "columns": [ + "worker_id" + ], + "isUnique": true + }, + "daytona_sandbox_sandbox_id": { + "name": "daytona_sandbox_sandbox_id", + "columns": [ + "sandbox_id" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "daytona_sandbox_id": { + "name": "daytona_sandbox_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "worker_bundle": { + "name": "worker_bundle", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "worker_id": { + "name": "worker_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "storage_url": { + "name": "storage_url", + "type": "varchar(2048)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": { + "worker_bundle_worker_id": { + "name": "worker_bundle_worker_id", + "columns": [ + "worker_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "worker_bundle_id": { + "name": "worker_bundle_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "worker_instance": { + "name": "worker_instance", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "worker_id": { + "name": "worker_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "region": { + "name": "region", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "varchar(2048)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "enum('provisioning','healthy','failed','stopped')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + } + }, + "indexes": { + "worker_instance_worker_id": { + "name": "worker_instance_worker_id", + "columns": [ + "worker_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "worker_instance_id": { + "name": "worker_instance_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "worker": { + "name": "worker", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "org_id": { + "name": "org_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "varchar(1024)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "destination": { + "name": "destination", + "type": "enum('local','cloud')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "enum('provisioning','healthy','failed','stopped')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "image_version": { + "name": "image_version", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "workspace_path": { + "name": "workspace_path", + "type": "varchar(1024)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sandbox_backend": { + "name": "sandbox_backend", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_heartbeat_at": { + "name": "last_heartbeat_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_active_at": { + "name": "last_active_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + } + }, + "indexes": { + "worker_org_id": { + "name": "worker_org_id", + "columns": [ + "org_id" + ], + "isUnique": false + }, + "worker_created_by_user_id": { + "name": "worker_created_by_user_id", + "columns": [ + "created_by_user_id" + ], + "isUnique": false + }, + "worker_status": { + "name": "worker_status", + "columns": [ + "status" + ], + "isUnique": false + }, + "worker_last_heartbeat_at": { + "name": "worker_last_heartbeat_at", + "columns": [ + "last_heartbeat_at" + ], + "isUnique": false + }, + "worker_last_active_at": { + "name": "worker_last_active_at", + "columns": [ + "last_active_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "worker_id": { + "name": "worker_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "worker_token": { + "name": "worker_token", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "worker_id": { + "name": "worker_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "enum('client','host','activity')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "worker_token_worker_id": { + "name": "worker_token_worker_id", + "columns": [ + "worker_id" + ], + "isUnique": false + }, + "worker_token_token": { + "name": "worker_token_token", + "columns": [ + "token" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "worker_token_id": { + "name": "worker_token_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "admin_allowlist": { + "name": "admin_allowlist", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "note": { + "name": "note", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + } + }, + "indexes": { + "admin_allowlist_email": { + "name": "admin_allowlist_email", + "columns": [ + "email" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "admin_allowlist_id": { + "name": "admin_allowlist_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "rate_limit": { + "name": "rate_limit", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "varchar(512)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "count": { + "name": "count", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "last_request": { + "name": "last_request", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "rate_limit_key": { + "name": "rate_limit_key", + "columns": [ + "key" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "rate_limit_id": { + "name": "rate_limit_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + } + }, + "views": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "tables": {}, + "indexes": {} + } +} \ No newline at end of file diff --git a/ee/packages/den-db/drizzle/meta/_journal.json b/ee/packages/den-db/drizzle/meta/_journal.json new file mode 100644 index 00000000..9df96f4a --- /dev/null +++ b/ee/packages/den-db/drizzle/meta/_journal.json @@ -0,0 +1,48 @@ +{ + "version": "7", + "dialect": "mysql", + "entries": [ + { + "idx": 1, + "version": "5", + "when": 1775077000001, + "tag": "0001_desktop_handoff_grants", + "breakpoints": true + }, + { + "idx": 2, + "version": "5", + "when": 1775077000002, + "tag": "0002_worker_activity_heartbeat", + "breakpoints": true + }, + { + "idx": 3, + "version": "5", + "when": 1775077000003, + "tag": "0003_rate_limit", + "breakpoints": true + }, + { + "idx": 4, + "version": "5", + "when": 1775077000004, + "tag": "0004_organization_plugin", + "breakpoints": true + }, + { + "idx": 5, + "version": "5", + "when": 1775077000005, + "tag": "0005_temp_template_sharing", + "breakpoints": true + }, + { + "idx": 6, + "version": "5", + "when": 1775077000006, + "tag": "0006_skill_hub", + "breakpoints": true + } + ] +} diff --git a/ee/packages/den-db/package.json b/ee/packages/den-db/package.json index 133ac8b7..cfa4c759 100644 --- a/ee/packages/den-db/package.json +++ b/ee/packages/den-db/package.json @@ -31,6 +31,11 @@ "development": "./src/schema/teams.ts", "default": "./dist/schema/teams.js" }, + "./schema/sharables/skills": { + "types": "./src/schema/sharables/skills.ts", + "development": "./src/schema/sharables/skills.ts", + "default": "./dist/schema/sharables/skills.js" + }, "./schema/workers": { "types": "./src/schema/workers.ts", "development": "./src/schema/workers.ts", diff --git a/ee/packages/den-db/src/drizzle.ts b/ee/packages/den-db/src/drizzle.ts index 3888f93b..e2e9600f 100644 --- a/ee/packages/den-db/src/drizzle.ts +++ b/ee/packages/den-db/src/drizzle.ts @@ -1 +1 @@ -export { and, asc, desc, eq, gt, isNotNull, isNull, sql } from "drizzle-orm" +export { and, asc, desc, eq, gt, inArray, isNotNull, isNull, or, sql } from "drizzle-orm" diff --git a/ee/packages/den-db/src/schema/index.ts b/ee/packages/den-db/src/schema/index.ts index e35637d7..0270b129 100644 --- a/ee/packages/den-db/src/schema/index.ts +++ b/ee/packages/den-db/src/schema/index.ts @@ -1,5 +1,6 @@ export * from "./auth" export * from "./org" +export * from "./sharables/skills" export * from "./teams" export * from "./workers" export * from "./system" diff --git a/ee/packages/den-db/src/schema/org.ts b/ee/packages/den-db/src/schema/org.ts index 381c9b92..a8719164 100644 --- a/ee/packages/den-db/src/schema/org.ts +++ b/ee/packages/den-db/src/schema/org.ts @@ -1,4 +1,4 @@ -import { sql } from "drizzle-orm" +import { relations, sql } from "drizzle-orm" import { index, mysqlTable, text, timestamp, uniqueIndex, varchar } from "drizzle-orm/mysql-core" import { denTypeIdColumn } from "../columns" @@ -110,11 +110,40 @@ export const TempTemplateSharingTable = mysqlTable( ], ) +export const organizationRelations = relations(OrganizationTable, ({ many }) => ({ + members: many(MemberTable), + roles: many(OrganizationRoleTable), + tempTemplateSharings: many(TempTemplateSharingTable), +})) + +export const memberRelations = relations(MemberTable, ({ many, one }) => ({ + organization: one(OrganizationTable, { + fields: [MemberTable.organizationId], + references: [OrganizationTable.id], + }), + createdTempTemplateSharings: many(TempTemplateSharingTable), +})) + +export const organizationRoleRelations = relations(OrganizationRoleTable, ({ one }) => ({ + organization: one(OrganizationTable, { + fields: [OrganizationRoleTable.organizationId], + references: [OrganizationTable.id], + }), +})) + +export const tempTemplateSharingRelations = relations(TempTemplateSharingTable, ({ one }) => ({ + organization: one(OrganizationTable, { + fields: [TempTemplateSharingTable.organizationId], + references: [OrganizationTable.id], + }), + creatorMember: one(MemberTable, { + fields: [TempTemplateSharingTable.creatorMemberId], + references: [MemberTable.id], + }), +})) + export const organization = OrganizationTable export const member = MemberTable export const invitation = InvitationTable export const organizationRole = OrganizationRoleTable export const tempTemplateSharing = TempTemplateSharingTable - -export const OrgTable = OrganizationTable -export const OrgMembershipTable = MemberTable diff --git a/ee/packages/den-db/src/schema/sharables/skills.ts b/ee/packages/den-db/src/schema/sharables/skills.ts index e69de29b..f9a6cf3f 100644 --- a/ee/packages/den-db/src/schema/sharables/skills.ts +++ b/ee/packages/den-db/src/schema/sharables/skills.ts @@ -0,0 +1,182 @@ +import { relations, sql } from "drizzle-orm"; +import { + index, + mysqlEnum, + mysqlTable, + text, + timestamp, + uniqueIndex, + varchar, +} from "drizzle-orm/mysql-core"; +import { denTypeIdColumn } from "../../columns"; +import { MemberTable, OrganizationTable } from "../org"; +import { TeamTable } from "../teams"; + +export const SkillTable = mysqlTable( + "skill", + { + id: denTypeIdColumn("skill", "id").notNull().primaryKey(), + organizationId: denTypeIdColumn( + "organization", + "organization_id", + ).notNull(), + createdByOrgMembershipId: denTypeIdColumn( + "member", + "created_by_org_membership_id", + ).notNull(), + title: varchar("title", { length: 255 }).notNull(), + description: text("description"), + skillText: text("skill_text").notNull(), + shared: mysqlEnum("shared", ["org", "public"]), + createdAt: timestamp("created_at", { fsp: 3 }).notNull().defaultNow(), + updatedAt: timestamp("updated_at", { fsp: 3 }) + .notNull() + .default(sql`CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)`), + }, + (table) => [ + index("skill_organization_id").on(table.organizationId), + index("skill_created_by_org_membership_id").on( + table.createdByOrgMembershipId, + ), + index("skill_shared").on(table.shared), + ], +); + +export const SkillHubTable = mysqlTable( + "skill_hub", + { + id: denTypeIdColumn("skillHub", "id").notNull().primaryKey(), + organizationId: denTypeIdColumn( + "organization", + "organization_id", + ).notNull(), + createdByOrgMembershipId: denTypeIdColumn( + "member", + "created_by_org_membership_id", + ).notNull(), + name: varchar("name", { length: 255 }).notNull(), + description: text("description"), + createdAt: timestamp("created_at", { fsp: 3 }).notNull().defaultNow(), + updatedAt: timestamp("updated_at", { fsp: 3 }) + .notNull() + .default(sql`CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)`), + }, + (table) => [ + index("skill_hub_organization_id").on(table.organizationId), + index("skill_hub_created_by_org_membership_id").on( + table.createdByOrgMembershipId, + ), + ], +); + +export const SkillHubSkillTable = mysqlTable( + "skill_hub_skill", + { + id: denTypeIdColumn("skillHubSkill", "id").notNull().primaryKey(), + skillHubId: denTypeIdColumn("skillHub", "skill_hub_id").notNull(), + skillId: denTypeIdColumn("skill", "skill_id").notNull(), + addedByOrgMembershipId: denTypeIdColumn( + "member", + "org_membership_id", + ).notNull(), + createdAt: timestamp("created_at", { fsp: 3 }).notNull().defaultNow(), + }, + (table) => [ + index("skill_hub_skill_skill_hub_id").on(table.skillHubId), + index("skill_hub_skill_skill_id").on(table.skillId), + uniqueIndex("skill_hub_skill_hub_skill").on( + table.skillHubId, + table.skillId, + ), + ], +); + +export const SkillHubMemberTable = mysqlTable( + "skill_hub_member", + { + id: denTypeIdColumn("skillHubMember", "id").notNull().primaryKey(), + skillHubId: denTypeIdColumn("skillHub", "skill_hub_id").notNull(), + orgMembershipId: denTypeIdColumn("member", "org_membership_id"), + teamId: denTypeIdColumn("team", "team_id"), + createdAt: timestamp("created_at", { fsp: 3 }).notNull().defaultNow(), + }, + (table) => [ + index("skill_hub_member_skill_hub_id").on(table.skillHubId), + index("skill_hub_member_org_membership_id").on(table.orgMembershipId), + index("skill_hub_member_team_id").on(table.teamId), + uniqueIndex("skill_hub_member_hub_org_membership").on( + table.skillHubId, + table.orgMembershipId, + ), + uniqueIndex("skill_hub_member_hub_team").on( + table.skillHubId, + table.teamId, + ), + ], +); + +export const skillRelations = relations(SkillTable, ({ many, one }) => ({ + organization: one(OrganizationTable, { + fields: [SkillTable.organizationId], + references: [OrganizationTable.id], + }), + createdByOrgMembership: one(MemberTable, { + fields: [SkillTable.createdByOrgMembershipId], + references: [MemberTable.id], + }), + skillHubLinks: many(SkillHubSkillTable), +})); + +export const skillHubRelations = relations(SkillHubTable, ({ many, one }) => ({ + organization: one(OrganizationTable, { + fields: [SkillHubTable.organizationId], + references: [OrganizationTable.id], + }), + createdByOrgMembership: one(MemberTable, { + fields: [SkillHubTable.createdByOrgMembershipId], + references: [MemberTable.id], + }), + skillLinks: many(SkillHubSkillTable), + memberLinks: many(SkillHubMemberTable), +})); + +export const skillHubSkillRelations = relations( + SkillHubSkillTable, + ({ one }) => ({ + skillHub: one(SkillHubTable, { + fields: [SkillHubSkillTable.skillHubId], + references: [SkillHubTable.id], + }), + skill: one(SkillTable, { + fields: [SkillHubSkillTable.skillId], + references: [SkillTable.id], + }), + addedByOrgMembership: one(MemberTable, { + fields: [SkillHubSkillTable.addedByOrgMembershipId], + references: [MemberTable.id], + }), + }), +); + +export const skillHubMemberRelations = relations( + SkillHubMemberTable, + ({ one }) => ({ + skillHub: one(SkillHubTable, { + fields: [SkillHubMemberTable.skillHubId], + references: [SkillHubTable.id], + }), + orgMembership: one(MemberTable, { + fields: [SkillHubMemberTable.orgMembershipId], + references: [MemberTable.id], + }), + team: one(TeamTable, { + fields: [SkillHubMemberTable.teamId], + references: [TeamTable.id], + }), + }), +); + +export const skill = SkillTable; +export const skillHub = SkillHubTable; +export const skillHubSkill = SkillHubSkillTable; +export const skillHubMember = SkillHubMemberTable; diff --git a/ee/packages/den-db/src/schema/teams.ts b/ee/packages/den-db/src/schema/teams.ts index 0139bb1c..0f142440 100644 --- a/ee/packages/den-db/src/schema/teams.ts +++ b/ee/packages/den-db/src/schema/teams.ts @@ -1,6 +1,7 @@ -import { sql } from "drizzle-orm" +import { relations, sql } from "drizzle-orm" import { index, mysqlTable, timestamp, uniqueIndex, varchar } from "drizzle-orm/mysql-core" import { denTypeIdColumn } from "../columns" +import { MemberTable, OrganizationTable } from "./org" export const TeamTable = mysqlTable( "team", @@ -24,15 +25,34 @@ export const TeamMemberTable = mysqlTable( { id: denTypeIdColumn("teamMember", "id").notNull().primaryKey(), teamId: denTypeIdColumn("team", "team_id").notNull(), - userId: denTypeIdColumn("user", "user_id").notNull(), + orgMembershipId: denTypeIdColumn("member", "org_membership_id").notNull(), createdAt: timestamp("created_at", { fsp: 3 }).notNull().defaultNow(), }, (table) => [ index("team_member_team_id").on(table.teamId), - index("team_member_user_id").on(table.userId), - uniqueIndex("team_member_team_user").on(table.teamId, table.userId), + index("team_member_org_membership_id").on(table.orgMembershipId), + uniqueIndex("team_member_team_org_membership").on(table.teamId, table.orgMembershipId), ], ) +export const teamRelations = relations(TeamTable, ({ many, one }) => ({ + organization: one(OrganizationTable, { + fields: [TeamTable.organizationId], + references: [OrganizationTable.id], + }), + memberships: many(TeamMemberTable), +})) + +export const teamMemberRelations = relations(TeamMemberTable, ({ one }) => ({ + team: one(TeamTable, { + fields: [TeamMemberTable.teamId], + references: [TeamTable.id], + }), + orgMembership: one(MemberTable, { + fields: [TeamMemberTable.orgMembershipId], + references: [MemberTable.id], + }), +})) + export const team = TeamTable export const teamMember = TeamMemberTable diff --git a/ee/packages/den-db/tsup.config.ts b/ee/packages/den-db/tsup.config.ts index 0a7bd9c5..8652a82c 100644 --- a/ee/packages/den-db/tsup.config.ts +++ b/ee/packages/den-db/tsup.config.ts @@ -6,6 +6,7 @@ export default defineConfig({ schema: "src/schema.ts", "schema/auth": "src/schema/auth.ts", "schema/org": "src/schema/org.ts", + "schema/sharables/skills": "src/schema/sharables/skills.ts", "schema/teams": "src/schema/teams.ts", "schema/workers": "src/schema/workers.ts", "schema/system": "src/schema/system.ts", diff --git a/ee/packages/utils/src/typeid.ts b/ee/packages/utils/src/typeid.ts index 5f5f1f77..eece8e3c 100644 --- a/ee/packages/utils/src/typeid.ts +++ b/ee/packages/utils/src/typeid.ts @@ -14,6 +14,10 @@ export const denTypeIdPrefixes = { invitation: "inv", team: "tem", teamMember: "tmb", + skill: "skl", + skillHub: "shb", + skillHubSkill: "shs", + skillHubMember: "shm", organizationRole: "orl", tempTemplateSharing: "tts", adminAllowlist: "aal", diff --git a/packaging/docker/Dockerfile.den b/packaging/docker/Dockerfile.den index 5fc3c4ba..79122758 100644 --- a/packaging/docker/Dockerfile.den +++ b/packaging/docker/Dockerfile.den @@ -9,18 +9,18 @@ COPY .npmrc /app/.npmrc COPY patches /app/patches COPY ee/packages/utils/package.json /app/ee/packages/utils/package.json COPY ee/packages/den-db/package.json /app/ee/packages/den-db/package.json -COPY ee/apps/den-controller/package.json /app/ee/apps/den-controller/package.json +COPY ee/apps/den-api/package.json /app/ee/apps/den-api/package.json RUN pnpm install --frozen-lockfile COPY ee/packages/utils /app/ee/packages/utils COPY ee/packages/den-db /app/ee/packages/den-db -COPY ee/apps/den-controller /app/ee/apps/den-controller +COPY ee/apps/den-api /app/ee/apps/den-api RUN pnpm --dir /app/ee/packages/utils run build RUN pnpm --dir /app/ee/packages/den-db run build -RUN pnpm --dir /app/ee/apps/den-controller run build +RUN pnpm --dir /app/ee/apps/den-api run build EXPOSE 8788 -CMD ["sh", "-lc", "yes | pnpm --dir /app/ee/packages/den-db run db:push && node ee/apps/den-controller/dist/index.js"] +CMD ["sh", "-lc", "yes | pnpm --dir /app/ee/packages/den-db run db:push && node ee/apps/den-api/dist/server.js"] diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index dadf1a87..35b9f67c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -419,9 +419,6 @@ importers: dotenv: specifier: ^16.4.5 version: 16.6.1 - drizzle-orm: - specifier: ^0.45.1 - version: 0.45.1(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(bun-types@1.3.6)(kysely@0.28.11)(mysql2@3.17.4) express: specifier: ^4.19.2 version: 4.22.1 @@ -441,9 +438,6 @@ importers: '@types/node': specifier: ^20.11.30 version: 20.12.12 - drizzle-kit: - specifier: ^0.31.9 - version: 0.31.9 tsx: specifier: ^4.15.7 version: 4.21.0