mirror of
https://github.com/openclaw/clawhub.git
synced 2026-08-14 08:52:21 +00:00
* feat: select official org for github sync sources * fix: rename github sync publisher field
203 lines
6.7 KiB
TypeScript
203 lines
6.7 KiB
TypeScript
import { ConvexError, v } from "convex/values";
|
|
import type { Doc, Id } from "./_generated/dataModel";
|
|
import type { MutationCtx, QueryCtx } from "./_generated/server";
|
|
import { internalQuery, mutation, query } from "./functions";
|
|
import { requireUser } from "./lib/access";
|
|
import { adjustGlobalPublicSkillsCount, getPublicSkillVisibilityDelta } from "./lib/globalStats";
|
|
import { isOfficialPublisher } from "./lib/officialPublishers";
|
|
import { isPublisherActive, isPublisherRoleAllowed, requirePublisherRole } from "./lib/publishers";
|
|
|
|
type PublicGitHubSkillSource = Pick<
|
|
Doc<"githubSkillSources">,
|
|
| "_id"
|
|
| "repo"
|
|
| "defaultBranch"
|
|
| "lastSyncStatus"
|
|
| "lastSyncError"
|
|
| "lastSyncErrorAt"
|
|
| "displayManifestStatus"
|
|
| "displayManifestFetchedAt"
|
|
| "displayManifestCommit"
|
|
| "lastSyncInvalidSkills"
|
|
| "createdAt"
|
|
| "updatedAt"
|
|
> & {
|
|
ownerPublisher: Pick<Doc<"publishers">, "_id" | "handle" | "displayName"> | null;
|
|
skills: Array<
|
|
Pick<Doc<"skills">, "_id" | "slug" | "displayName" | "githubPath" | "githubCurrentStatus">
|
|
>;
|
|
};
|
|
|
|
export const getByIdInternal = internalQuery({
|
|
args: { sourceId: v.id("githubSkillSources") },
|
|
handler: async (ctx, args) => ctx.db.get(args.sourceId),
|
|
});
|
|
|
|
async function toPublicGitHubSkillSource(
|
|
ctx: Pick<QueryCtx, "db">,
|
|
source: Doc<"githubSkillSources">,
|
|
): Promise<PublicGitHubSkillSource> {
|
|
const skills = await ctx.db
|
|
.query("skills")
|
|
.withIndex("by_github_source", (q) => q.eq("githubSourceId", source._id))
|
|
.collect();
|
|
const visibleGitHubSkills = skills
|
|
.filter((skill) => skill.installKind === "github" && !skill.softDeletedAt)
|
|
.sort((a, b) => a.displayName.localeCompare(b.displayName))
|
|
.map((skill) => ({
|
|
_id: skill._id,
|
|
slug: skill.slug,
|
|
displayName: skill.displayName,
|
|
githubPath: skill.githubPath,
|
|
githubCurrentStatus: skill.githubCurrentStatus,
|
|
}));
|
|
const ownerPublisher = source.ownerPublisherId ? await ctx.db.get(source.ownerPublisherId) : null;
|
|
|
|
return {
|
|
_id: source._id as Id<"githubSkillSources">,
|
|
repo: source.repo,
|
|
ownerPublisher: ownerPublisher
|
|
? {
|
|
_id: ownerPublisher._id,
|
|
handle: ownerPublisher.handle,
|
|
displayName: ownerPublisher.displayName,
|
|
}
|
|
: null,
|
|
defaultBranch: source.defaultBranch,
|
|
lastSyncStatus: source.lastSyncStatus,
|
|
lastSyncError: source.lastSyncError,
|
|
lastSyncErrorAt: source.lastSyncErrorAt,
|
|
displayManifestStatus: source.displayManifestStatus,
|
|
displayManifestFetchedAt: source.displayManifestFetchedAt,
|
|
displayManifestCommit: source.displayManifestCommit,
|
|
lastSyncInvalidSkills: source.lastSyncInvalidSkills,
|
|
createdAt: source.createdAt,
|
|
updatedAt: source.updatedAt,
|
|
skills: visibleGitHubSkills,
|
|
};
|
|
}
|
|
|
|
export const listForPublisher = query({
|
|
args: { ownerPublisherId: v.id("publishers") },
|
|
handler: async (ctx, args): Promise<PublicGitHubSkillSource[]> => {
|
|
const { userId } = await requireUser(ctx);
|
|
await requirePublisherRole(ctx, {
|
|
publisherId: args.ownerPublisherId,
|
|
userId,
|
|
allowed: ["admin"],
|
|
});
|
|
const sources = await ctx.db
|
|
.query("githubSkillSources")
|
|
.withIndex("by_owner_publisher", (q) => q.eq("ownerPublisherId", args.ownerPublisherId))
|
|
.collect();
|
|
const sortedSources = sources.sort((a, b) => b.updatedAt - a.updatedAt);
|
|
return await Promise.all(sortedSources.map((source) => toPublicGitHubSkillSource(ctx, source)));
|
|
},
|
|
});
|
|
|
|
export const listForManageableOfficialPublishers = query({
|
|
args: {},
|
|
handler: async (ctx): Promise<PublicGitHubSkillSource[]> => {
|
|
const { userId } = await requireUser(ctx);
|
|
const memberships = await ctx.db
|
|
.query("publisherMembers")
|
|
.withIndex("by_user", (q) => q.eq("userId", userId))
|
|
.collect();
|
|
const ownerPublisherIds: Id<"publishers">[] = [];
|
|
for (const membership of memberships) {
|
|
if (!isPublisherRoleAllowed(membership.role, ["admin"])) continue;
|
|
const publisher = await ctx.db.get(membership.publisherId);
|
|
if (
|
|
!publisher ||
|
|
publisher.kind !== "org" ||
|
|
!isPublisherActive(publisher) ||
|
|
!(await isOfficialPublisher(ctx, publisher))
|
|
) {
|
|
continue;
|
|
}
|
|
ownerPublisherIds.push(publisher._id);
|
|
}
|
|
const sourceGroups = await Promise.all(
|
|
ownerPublisherIds.map((ownerPublisherId) =>
|
|
ctx.db
|
|
.query("githubSkillSources")
|
|
.withIndex("by_owner_publisher", (q) => q.eq("ownerPublisherId", ownerPublisherId))
|
|
.collect(),
|
|
),
|
|
);
|
|
const sortedSources = sourceGroups.flat().sort((a, b) => b.updatedAt - a.updatedAt);
|
|
return await Promise.all(sortedSources.map((source) => toPublicGitHubSkillSource(ctx, source)));
|
|
},
|
|
});
|
|
|
|
export async function deleteForPublisherHandler(
|
|
ctx: MutationCtx,
|
|
args: {
|
|
ownerPublisherId: Id<"publishers">;
|
|
sourceId: Id<"githubSkillSources">;
|
|
now?: number;
|
|
},
|
|
) {
|
|
const { userId } = await requireUser(ctx);
|
|
await requirePublisherRole(ctx, {
|
|
publisherId: args.ownerPublisherId,
|
|
userId,
|
|
allowed: ["admin"],
|
|
});
|
|
|
|
const source = await ctx.db.get(args.sourceId);
|
|
if (!source || source.ownerPublisherId !== args.ownerPublisherId) {
|
|
throw new ConvexError("GitHub source not found.");
|
|
}
|
|
|
|
const now = args.now ?? Date.now();
|
|
const contents = await ctx.db
|
|
.query("githubSkillContents")
|
|
.withIndex("by_github_source", (q) => q.eq("githubSourceId", args.sourceId))
|
|
.collect();
|
|
for (const content of contents) {
|
|
await ctx.db.delete(content._id);
|
|
}
|
|
|
|
const skills = await ctx.db
|
|
.query("skills")
|
|
.withIndex("by_github_source", (q) => q.eq("githubSourceId", args.sourceId))
|
|
.collect();
|
|
let deletedSkills = 0;
|
|
let publicSkillDelta = 0;
|
|
for (const skill of skills) {
|
|
if (skill.installKind !== "github") continue;
|
|
|
|
const nextSkill: Doc<"skills"> = {
|
|
...skill,
|
|
softDeletedAt: skill.softDeletedAt ?? now,
|
|
githubCurrentStatus: "missing",
|
|
githubRemovedAt: skill.githubRemovedAt ?? now,
|
|
updatedAt: now,
|
|
};
|
|
publicSkillDelta += getPublicSkillVisibilityDelta(skill, nextSkill);
|
|
await ctx.db.patch(skill._id, {
|
|
softDeletedAt: nextSkill.softDeletedAt,
|
|
githubCurrentStatus: nextSkill.githubCurrentStatus,
|
|
githubRemovedAt: nextSkill.githubRemovedAt,
|
|
updatedAt: now,
|
|
});
|
|
deletedSkills += 1;
|
|
}
|
|
|
|
if (publicSkillDelta !== 0) {
|
|
await adjustGlobalPublicSkillsCount(ctx, publicSkillDelta, now);
|
|
}
|
|
await ctx.db.delete(args.sourceId);
|
|
|
|
return { ok: true as const, deletedSkills };
|
|
}
|
|
|
|
export const deleteForPublisher: ReturnType<typeof mutation> = mutation({
|
|
args: {
|
|
ownerPublisherId: v.id("publishers"),
|
|
sourceId: v.id("githubSkillSources"),
|
|
},
|
|
handler: async (ctx, args) => deleteForPublisherHandler(ctx, args),
|
|
});
|