mirror of
https://github.com/different-ai/openwork
synced 2026-04-25 17:15:34 +02:00
feat(den): add skill hubs and restore den-db migrations (#1285)
* feat(den-db): add skill hub schema and own migrations * feat(den-api): add skill hub org routes * fix(den-db): restore drizzle migration workflow Move the Docker Den service onto den-api and repair den-db's Drizzle metadata so skill hub migrations generate incrementally from the package. * refactor(den-db): drop legacy org table aliases --------- Co-authored-by: src-opn <src-opn@users.noreply.github.com>
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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<T extends { Variables: OrgRouteVariables }>(app: Hono<T>) {
|
||||
@@ -11,5 +12,6 @@ export function registerOrgRoutes<T extends { Variables: OrgRouteVariables }>(ap
|
||||
registerOrgInvitationRoutes(app)
|
||||
registerOrgMemberRoutes(app)
|
||||
registerOrgRoleRoutes(app)
|
||||
registerOrgSkillRoutes(app)
|
||||
registerOrgTemplateRoutes(app)
|
||||
}
|
||||
|
||||
875
ee/apps/den-api/src/routes/org/skills.ts
Normal file
875
ee/apps/den-api/src/routes/org/skills.ts
Normal file
@@ -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<SkillId>()
|
||||
}
|
||||
|
||||
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<SkillId>
|
||||
}) {
|
||||
return input.skill.createdByOrgMembershipId === input.currentMemberId
|
||||
|| input.skill.shared !== null
|
||||
|| input.accessibleSkillIds.has(input.skill.id)
|
||||
}
|
||||
|
||||
export function registerOrgSkillRoutes<T extends { Variables: OrgRouteVariables & Partial<MemberTeamsContext> }>(app: Hono<T>) {
|
||||
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<SkillHubId, SkillRow[]>()
|
||||
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<SkillHubId, typeof memberAccessRows>()
|
||||
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<SkillHubId, typeof teamAccessRows>()
|
||||
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<SkillHubId, { orgMembershipIds: MemberId[]; teamIds: TeamId[] }>()
|
||||
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)
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
AuditEventTable,
|
||||
AuthUserTable,
|
||||
DaytonaSandboxTable,
|
||||
OrgMembershipTable,
|
||||
MemberTable,
|
||||
WorkerBundleTable,
|
||||
WorkerInstanceTable,
|
||||
WorkerTable,
|
||||
@@ -63,7 +63,7 @@ export type WorkerRouteVariables = AuthContextVariables & Partial<UserOrganizati
|
||||
type WorkerRow = typeof WorkerTable.$inferSelect
|
||||
type WorkerInstanceRow = typeof WorkerInstanceTable.$inferSelect
|
||||
export type WorkerId = WorkerRow["id"]
|
||||
type OrgId = typeof OrgMembershipTable.$inferSelect.organizationId
|
||||
type OrgId = typeof MemberTable.$inferSelect.organizationId
|
||||
type UserId = typeof AuthUserTable.$inferSelect.id
|
||||
|
||||
export const token = () => randomBytes(32).toString("hex")
|
||||
|
||||
36
ee/packages/den-db/README.md
Normal file
36
ee/packages/den-db/README.md
Normal file
@@ -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.
|
||||
@@ -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(),
|
||||
})
|
||||
|
||||
13
ee/packages/den-db/drizzle/0001_desktop_handoff_grants.sql
Normal file
13
ee/packages/den-db/drizzle/0001_desktop_handoff_grants.sql
Normal file
@@ -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`);
|
||||
@@ -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;
|
||||
8
ee/packages/den-db/drizzle/0003_rate_limit.sql
Normal file
8
ee/packages/den-db/drizzle/0003_rate_limit.sql
Normal file
@@ -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`)
|
||||
);
|
||||
105
ee/packages/den-db/drizzle/0004_organization_plugin.manual.sql
Normal file
105
ee/packages/den-db/drizzle/0004_organization_plugin.manual.sql
Normal file
@@ -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`);
|
||||
152
ee/packages/den-db/drizzle/0004_organization_plugin.sql
Normal file
152
ee/packages/den-db/drizzle/0004_organization_plugin.sql
Normal file
@@ -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;
|
||||
14
ee/packages/den-db/drizzle/0005_temp_template_sharing.sql
Normal file
14
ee/packages/den-db/drizzle/0005_temp_template_sharing.sql
Normal file
@@ -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`)
|
||||
);
|
||||
88
ee/packages/den-db/drizzle/0006_skill_hub.sql
Normal file
88
ee/packages/den-db/drizzle/0006_skill_hub.sql
Normal file
@@ -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`);
|
||||
2066
ee/packages/den-db/drizzle/meta/0006_snapshot.json
Normal file
2066
ee/packages/den-db/drizzle/meta/0006_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
48
ee/packages/den-db/drizzle/meta/_journal.json
Normal file
48
ee/packages/den-db/drizzle/meta/_journal.json
Normal file
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export * from "./auth"
|
||||
export * from "./org"
|
||||
export * from "./sharables/skills"
|
||||
export * from "./teams"
|
||||
export * from "./workers"
|
||||
export * from "./system"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"]
|
||||
|
||||
6
pnpm-lock.yaml
generated
6
pnpm-lock.yaml
generated
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user