mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-27 02:44:50 +01:00
723 lines
26 KiB
TypeScript
723 lines
26 KiB
TypeScript
import { and, desc, eq, ilike, inArray, sql } from "drizzle-orm";
|
|
import type { Db } from "@paperclipai/db";
|
|
import {
|
|
clipComments,
|
|
clipCreatorProfiles,
|
|
clipDependencies,
|
|
clipFlags,
|
|
clipImports,
|
|
clipModerationEvents,
|
|
clipPublicMetricEvents,
|
|
clipRankingSnapshots,
|
|
clipRevisions,
|
|
clipShowcasePosts,
|
|
clipVotes,
|
|
clips,
|
|
} from "@paperclipai/db";
|
|
import type {
|
|
ClipDependency,
|
|
ClipModerationState,
|
|
ClipRevision,
|
|
CreateClipComment,
|
|
CreateClipCreatorProfile,
|
|
CreateClipImportTelemetry,
|
|
CreateClipReport,
|
|
CreateClipRevision,
|
|
CreateClipShowcase,
|
|
CreateClipVote,
|
|
PublicClip,
|
|
PublishClip,
|
|
UpdateClip,
|
|
} from "@paperclipai/shared";
|
|
import { conflict, notFound, unprocessable } from "../errors.js";
|
|
|
|
const PUBLIC_BROWSE_MODERATION_STATES = ["normal", "warning", "limited"];
|
|
const PUBLIC_DETAIL_MODERATION_STATES = ["normal", "under_review", "warning", "limited"];
|
|
const PUBLIC_DETAIL_VISIBILITIES = ["public", "unlisted"];
|
|
const PUBLIC_STATUS = "published";
|
|
const DEFAULT_MANIFEST_VERSION = "paperclip.clip/v1";
|
|
|
|
type Actor = {
|
|
actorType: "agent" | "user" | "system" | "anonymous";
|
|
actorId: string;
|
|
agentId?: string | null;
|
|
userId?: string | null;
|
|
};
|
|
|
|
type ClipRevisionClient = Pick<Db, "insert" | "select">;
|
|
|
|
function first<T>(rows: T[]): T | null {
|
|
return rows[0] ?? null;
|
|
}
|
|
|
|
function isPubliclyReadableRevision(revision: typeof clipRevisions.$inferSelect) {
|
|
return revision.securityReviewState !== "blocked" && revision.verificationState !== "failed";
|
|
}
|
|
|
|
function publicMetrics(row: typeof clips.$inferSelect) {
|
|
return {
|
|
importCount: row.importCount,
|
|
successfulFirstRunCount: row.successfulFirstRunCount,
|
|
voteScore: row.voteScore,
|
|
upvoteCount: row.upvoteCount,
|
|
downvoteCount: row.downvoteCount,
|
|
commentCount: row.commentCount,
|
|
showcaseCount: row.showcaseCount,
|
|
reportCount: row.reportCount,
|
|
};
|
|
}
|
|
|
|
function serializePublicClip(input: {
|
|
clip: typeof clips.$inferSelect;
|
|
creator: typeof clipCreatorProfiles.$inferSelect;
|
|
revision?: typeof clipRevisions.$inferSelect | null;
|
|
dependencies?: (typeof clipDependencies.$inferSelect)[];
|
|
}): PublicClip {
|
|
return {
|
|
id: input.clip.id,
|
|
slug: input.clip.slug,
|
|
type: input.clip.type as PublicClip["type"],
|
|
title: input.clip.title,
|
|
summary: input.clip.summary,
|
|
description: input.clip.description,
|
|
visibility: input.clip.visibility as PublicClip["visibility"],
|
|
status: input.clip.status as PublicClip["status"],
|
|
moderationState: input.clip.moderationState as PublicClip["moderationState"],
|
|
currentRevisionId: input.clip.currentRevisionId,
|
|
latestApprovedRevisionId: input.clip.latestApprovedRevisionId,
|
|
creatorProfileId: input.clip.creatorProfileId,
|
|
creator: {
|
|
id: input.creator.id,
|
|
handle: input.creator.handle,
|
|
displayName: input.creator.displayName,
|
|
avatarUrl: input.creator.avatarUrl,
|
|
verificationState: input.creator.verificationState,
|
|
reputationSummary: input.creator.reputationSummary,
|
|
},
|
|
tags: input.clip.tags,
|
|
categories: input.clip.categories,
|
|
useCases: input.clip.useCases,
|
|
requiredProviders: input.clip.requiredProviders,
|
|
compatibility: input.clip.compatibility,
|
|
metrics: publicMetrics(input.clip),
|
|
currentRevision: input.revision
|
|
? {
|
|
...input.revision,
|
|
securityReviewState: input.revision.securityReviewState as ClipRevision["securityReviewState"],
|
|
verificationState: input.revision.verificationState as ClipRevision["verificationState"],
|
|
}
|
|
: null,
|
|
dependencies: input.dependencies?.map((dependency) => ({
|
|
...dependency,
|
|
type: dependency.type as ClipDependency["type"],
|
|
required: dependency.required as ClipDependency["required"],
|
|
})),
|
|
createdAt: input.clip.createdAt,
|
|
updatedAt: input.clip.updatedAt,
|
|
};
|
|
}
|
|
|
|
function actorColumns(actor: Actor) {
|
|
return {
|
|
actorType: actor.actorType,
|
|
actorId: actor.actorId,
|
|
};
|
|
}
|
|
|
|
export function clipService(db: Db) {
|
|
async function getCreatorProfileById(id: string) {
|
|
return first(await db.select().from(clipCreatorProfiles).where(eq(clipCreatorProfiles.id, id)).limit(1));
|
|
}
|
|
|
|
async function getCreatorProfileByHandle(handle: string) {
|
|
return first(await db.select().from(clipCreatorProfiles).where(eq(clipCreatorProfiles.handle, handle)).limit(1));
|
|
}
|
|
|
|
async function getClipById(id: string) {
|
|
return first(await db.select().from(clips).where(eq(clips.id, id)).limit(1));
|
|
}
|
|
|
|
async function getClipBySlug(slug: string) {
|
|
return first(await db.select().from(clips).where(eq(clips.slug, slug)).limit(1));
|
|
}
|
|
|
|
async function getRevisionByNumber(clipId: string, revisionNumber: number) {
|
|
return first(
|
|
await db
|
|
.select()
|
|
.from(clipRevisions)
|
|
.where(and(eq(clipRevisions.clipId, clipId), eq(clipRevisions.revisionNumber, revisionNumber)))
|
|
.limit(1),
|
|
);
|
|
}
|
|
|
|
async function getPublicCurrentRevision(clip: typeof clips.$inferSelect) {
|
|
const revisionId = clip.latestApprovedRevisionId ?? clip.currentRevisionId;
|
|
if (!revisionId) return null;
|
|
const revision = first(await db.select().from(clipRevisions).where(eq(clipRevisions.id, revisionId)).limit(1));
|
|
return revision && isPubliclyReadableRevision(revision) ? revision : null;
|
|
}
|
|
|
|
async function getPublicClipBySlug(slug: string) {
|
|
const rows = await db
|
|
.select({ clip: clips, creator: clipCreatorProfiles })
|
|
.from(clips)
|
|
.innerJoin(clipCreatorProfiles, eq(clips.creatorProfileId, clipCreatorProfiles.id))
|
|
.where(
|
|
and(
|
|
eq(clips.slug, slug),
|
|
inArray(clips.visibility, PUBLIC_DETAIL_VISIBILITIES),
|
|
eq(clips.status, PUBLIC_STATUS),
|
|
inArray(clips.moderationState, PUBLIC_DETAIL_MODERATION_STATES),
|
|
),
|
|
)
|
|
.limit(1);
|
|
return first(rows);
|
|
}
|
|
|
|
async function listPublic(input: {
|
|
q?: string | null;
|
|
type?: string | null;
|
|
tag?: string | null;
|
|
creatorProfileId?: string | null;
|
|
includeUnlisted?: boolean;
|
|
limit?: number;
|
|
offset?: number;
|
|
} = {}) {
|
|
const conditions = [
|
|
input.includeUnlisted
|
|
? inArray(clips.visibility, PUBLIC_DETAIL_VISIBILITIES)
|
|
: eq(clips.visibility, "public"),
|
|
eq(clips.status, PUBLIC_STATUS),
|
|
inArray(
|
|
clips.moderationState,
|
|
input.includeUnlisted ? PUBLIC_DETAIL_MODERATION_STATES : PUBLIC_BROWSE_MODERATION_STATES,
|
|
),
|
|
];
|
|
if (input.creatorProfileId) conditions.push(eq(clips.creatorProfileId, input.creatorProfileId));
|
|
if (input.type) conditions.push(eq(clips.type, input.type));
|
|
if (input.q) {
|
|
const q = `%${input.q.trim()}%`;
|
|
conditions.push(sql`(${clips.title} ilike ${q} or ${clips.summary} ilike ${q})`);
|
|
}
|
|
if (input.tag) {
|
|
conditions.push(sql`${clips.tags} @> ${JSON.stringify([input.tag])}::jsonb`);
|
|
}
|
|
|
|
const rows = await db
|
|
.select({ clip: clips, creator: clipCreatorProfiles })
|
|
.from(clips)
|
|
.innerJoin(clipCreatorProfiles, eq(clips.creatorProfileId, clipCreatorProfiles.id))
|
|
.where(and(...conditions))
|
|
.orderBy(desc(clips.updatedAt))
|
|
.limit(Math.min(Math.max(input.limit ?? 24, 1), 100))
|
|
.offset(Math.max(input.offset ?? 0, 0));
|
|
|
|
return rows.map((row) => serializePublicClip({ ...row }));
|
|
}
|
|
|
|
async function getPublicDetail(slug: string) {
|
|
const row = await getPublicClipBySlug(slug);
|
|
if (!row) return null;
|
|
const revision = await getPublicCurrentRevision(row.clip);
|
|
const dependencies = revision
|
|
? await db.select().from(clipDependencies).where(eq(clipDependencies.revisionId, revision.id))
|
|
: [];
|
|
return serializePublicClip({
|
|
clip: row.clip,
|
|
creator: row.creator,
|
|
revision,
|
|
dependencies,
|
|
});
|
|
}
|
|
|
|
async function getPublicRevision(slug: string, revisionNumber: number) {
|
|
const row = await getPublicClipBySlug(slug);
|
|
if (!row) return null;
|
|
const revision = await getRevisionByNumber(row.clip.id, revisionNumber);
|
|
if (!revision || !isPubliclyReadableRevision(revision)) return null;
|
|
const dependencies = await db.select().from(clipDependencies).where(eq(clipDependencies.revisionId, revision.id));
|
|
return serializePublicClip({
|
|
clip: row.clip,
|
|
creator: row.creator,
|
|
revision,
|
|
dependencies,
|
|
});
|
|
}
|
|
|
|
async function createCreatorProfile(companyId: string, input: CreateClipCreatorProfile) {
|
|
const [profile] = await db
|
|
.insert(clipCreatorProfiles)
|
|
.values({
|
|
companyId,
|
|
handle: input.handle,
|
|
displayName: input.displayName,
|
|
bio: input.bio ?? null,
|
|
avatarUrl: input.avatarUrl ?? null,
|
|
websiteUrl: input.websiteUrl ?? null,
|
|
verificationState: input.verificationState ?? "unverified",
|
|
reputationSummary: input.reputationSummary ?? {},
|
|
})
|
|
.returning();
|
|
return profile;
|
|
}
|
|
|
|
async function resolveCreatorProfile(companyId: string, input: PublishClip) {
|
|
if (input.creatorProfileId) {
|
|
const profile = await getCreatorProfileById(input.creatorProfileId);
|
|
if (!profile || profile.companyId !== companyId) {
|
|
throw unprocessable("Creator profile does not belong to this company");
|
|
}
|
|
return profile;
|
|
}
|
|
if (!input.creatorProfile) throw unprocessable("creatorProfileId or creatorProfile is required");
|
|
const existing = await getCreatorProfileByHandle(input.creatorProfile.handle);
|
|
if (existing) {
|
|
if (existing.companyId !== companyId) {
|
|
throw conflict("Creator profile handle already belongs to another company");
|
|
}
|
|
return existing;
|
|
}
|
|
return createCreatorProfile(companyId, input.creatorProfile);
|
|
}
|
|
|
|
async function insertRevision(client: ClipRevisionClient, clipId: string, input: CreateClipRevision, requestedRevisionNumber?: number) {
|
|
const revisionNumber = requestedRevisionNumber ?? await nextRevisionNumber(client, clipId);
|
|
const [revision] = await client
|
|
.insert(clipRevisions)
|
|
.values({
|
|
clipId,
|
|
revisionNumber,
|
|
manifestVersion: input.manifestVersion ?? DEFAULT_MANIFEST_VERSION,
|
|
manifestChecksum: input.manifestChecksum,
|
|
artifactChecksum: input.artifactChecksum,
|
|
manifestPayload: input.manifestPayload,
|
|
artifactRef: input.artifactRef ?? null,
|
|
dependencyGraph: input.dependencyGraph ?? {},
|
|
permissions: input.permissions ?? [],
|
|
secretsSchema: input.secretsSchema ?? [],
|
|
budgetEstimate: input.budgetEstimate ?? null,
|
|
redactionReport: input.redactionReport ?? {},
|
|
dangerousCapabilities: input.dangerousCapabilities ?? [],
|
|
securityReviewState: input.securityReviewState ?? "unreviewed",
|
|
verificationState: input.verificationState ?? "not_run",
|
|
compatibility: input.compatibility ?? {},
|
|
changeSummary: input.changeSummary ?? null,
|
|
breakingChanges: input.breakingChanges ?? null,
|
|
migrationNotes: input.migrationNotes ?? null,
|
|
})
|
|
.returning();
|
|
|
|
if (input.dependencies?.length) {
|
|
await client.insert(clipDependencies).values(
|
|
input.dependencies.map((dependency) => ({
|
|
clipId,
|
|
revisionId: revision.id,
|
|
type: dependency.type,
|
|
key: dependency.key,
|
|
displayName: dependency.displayName ?? null,
|
|
required: dependency.required ?? "required",
|
|
metadata: dependency.metadata ?? {},
|
|
})),
|
|
);
|
|
}
|
|
return revision;
|
|
}
|
|
|
|
async function nextRevisionNumber(client: Pick<Db, "select">, clipId: string) {
|
|
const [row] = await client
|
|
.select({ maxRevision: sql<number>`coalesce(max(${clipRevisions.revisionNumber}), 0)` })
|
|
.from(clipRevisions)
|
|
.where(eq(clipRevisions.clipId, clipId));
|
|
return Number(row?.maxRevision ?? 0) + 1;
|
|
}
|
|
|
|
async function publish(companyId: string, input: PublishClip) {
|
|
const existing = await getClipBySlug(input.slug);
|
|
if (existing) throw conflict("Clip slug already exists");
|
|
const creatorProfile = await resolveCreatorProfile(companyId, input);
|
|
const [clip] = await db
|
|
.insert(clips)
|
|
.values({
|
|
sourceCompanyId: companyId,
|
|
creatorProfileId: creatorProfile.id,
|
|
slug: input.slug,
|
|
type: input.type,
|
|
title: input.title,
|
|
summary: input.summary,
|
|
description: input.description ?? null,
|
|
visibility: input.visibility ?? "unlisted",
|
|
status: input.status ?? (input.visibility === "public" ? "pending_review" : "published"),
|
|
tags: input.tags ?? [],
|
|
categories: input.categories ?? [],
|
|
useCases: input.useCases ?? [],
|
|
requiredProviders: input.requiredProviders ?? [],
|
|
compatibility: input.compatibility ?? {},
|
|
sourceKind: input.sourceKind ?? null,
|
|
sourceObjectType: input.sourceObjectType ?? null,
|
|
sourceObjectId: input.sourceObjectId ?? null,
|
|
})
|
|
.returning();
|
|
const revision = await insertRevision(db, clip.id, input.revision, 1);
|
|
const visibleRevisionId = clip.status === "published" ? revision.id : null;
|
|
const [updated] = await db
|
|
.update(clips)
|
|
.set({
|
|
currentRevisionId: revision.id,
|
|
latestApprovedRevisionId: visibleRevisionId,
|
|
updatedAt: new Date(),
|
|
})
|
|
.where(eq(clips.id, clip.id))
|
|
.returning();
|
|
await rebuildRankingSnapshot(updated);
|
|
return { clip: updated, revision, creatorProfile };
|
|
}
|
|
|
|
async function createRevision(clipId: string, input: CreateClipRevision) {
|
|
const { clip: updated, revision } = await db.transaction(async (tx) => {
|
|
await tx.execute(sql`select ${clips.id} from ${clips} where ${clips.id} = ${clipId} for update`);
|
|
const clip = first(await tx.select().from(clips).where(eq(clips.id, clipId)).limit(1));
|
|
if (!clip) throw notFound("Clip not found");
|
|
const revision = await insertRevision(tx, clip.id, input);
|
|
const [updated] = await tx
|
|
.update(clips)
|
|
.set({
|
|
currentRevisionId: revision.id,
|
|
latestApprovedRevisionId: clip.status === "published" && clip.moderationState !== "blocked" ? revision.id : clip.latestApprovedRevisionId,
|
|
updatedAt: new Date(),
|
|
})
|
|
.where(eq(clips.id, clip.id))
|
|
.returning();
|
|
return { clip: updated, revision };
|
|
});
|
|
await rebuildRankingSnapshot(updated);
|
|
return { clip: updated, revision };
|
|
}
|
|
|
|
async function updateClip(clipId: string, input: UpdateClip, actor: Actor) {
|
|
const existing = await getClipById(clipId);
|
|
if (!existing) throw notFound("Clip not found");
|
|
|
|
if (input.latestApprovedRevisionId) {
|
|
const revision = first(
|
|
await db
|
|
.select()
|
|
.from(clipRevisions)
|
|
.where(and(eq(clipRevisions.id, input.latestApprovedRevisionId), eq(clipRevisions.clipId, clipId)))
|
|
.limit(1),
|
|
);
|
|
if (!revision) throw unprocessable("latestApprovedRevisionId must belong to this clip");
|
|
}
|
|
|
|
const moderationStateChanged =
|
|
input.moderationState !== undefined && input.moderationState !== existing.moderationState;
|
|
|
|
const [updated] = await db
|
|
.update(clips)
|
|
.set({
|
|
title: input.title ?? existing.title,
|
|
summary: input.summary ?? existing.summary,
|
|
description: input.description === undefined ? existing.description : input.description,
|
|
visibility: input.visibility ?? existing.visibility,
|
|
status: input.status ?? existing.status,
|
|
moderationState: input.moderationState ?? existing.moderationState,
|
|
latestApprovedRevisionId: input.latestApprovedRevisionId === undefined
|
|
? existing.latestApprovedRevisionId
|
|
: input.latestApprovedRevisionId,
|
|
tags: input.tags ?? existing.tags,
|
|
categories: input.categories ?? existing.categories,
|
|
useCases: input.useCases ?? existing.useCases,
|
|
requiredProviders: input.requiredProviders ?? existing.requiredProviders,
|
|
compatibility: input.compatibility ?? existing.compatibility,
|
|
delistedAt: input.status === "delisted" || input.moderationState === "delisted" ? new Date() : existing.delistedAt,
|
|
updatedAt: new Date(),
|
|
})
|
|
.where(eq(clips.id, clipId))
|
|
.returning();
|
|
|
|
if (moderationStateChanged) {
|
|
await db.insert(clipModerationEvents).values({
|
|
clipId,
|
|
...actorColumns(actor),
|
|
fromState: existing.moderationState,
|
|
toState: input.moderationState as ClipModerationState,
|
|
reason: input.moderationReason ?? "moderation_state_updated",
|
|
});
|
|
}
|
|
await rebuildRankingSnapshot(updated);
|
|
return updated;
|
|
}
|
|
|
|
async function createVote(slug: string, input: CreateClipVote, actor: Actor) {
|
|
const row = await getPublicClipBySlug(slug);
|
|
if (!row) throw notFound("Clip not found");
|
|
const revision = input.revisionNumber
|
|
? await getRevisionByNumber(row.clip.id, input.revisionNumber)
|
|
: await getPublicCurrentRevision(row.clip);
|
|
if (!revision) throw notFound("Clip revision not found");
|
|
|
|
const existing = first(
|
|
await db
|
|
.select()
|
|
.from(clipVotes)
|
|
.where(and(
|
|
eq(clipVotes.revisionId, revision.id),
|
|
eq(clipVotes.actorType, actor.actorType),
|
|
eq(clipVotes.actorId, actor.actorId),
|
|
))
|
|
.limit(1),
|
|
);
|
|
|
|
if (existing) {
|
|
await db
|
|
.update(clipVotes)
|
|
.set({ vote: input.vote, reason: input.reason ?? null, metadata: input.metadata ?? {}, updatedAt: new Date() })
|
|
.where(eq(clipVotes.id, existing.id));
|
|
} else {
|
|
await db.insert(clipVotes).values({
|
|
clipId: row.clip.id,
|
|
revisionId: revision.id,
|
|
actorType: actor.actorType,
|
|
actorId: actor.actorId,
|
|
vote: input.vote,
|
|
reason: input.reason ?? null,
|
|
metadata: input.metadata ?? {},
|
|
});
|
|
}
|
|
const updated = await refreshVoteCounters(row.clip.id);
|
|
await rebuildRankingSnapshot(updated);
|
|
return { clip: updated, revision };
|
|
}
|
|
|
|
async function createReport(slug: string, input: CreateClipReport, actor: Actor) {
|
|
const row = await getPublicClipBySlug(slug);
|
|
if (!row) throw notFound("Clip not found");
|
|
const revision = input.revisionNumber ? await getRevisionByNumber(row.clip.id, input.revisionNumber) : null;
|
|
await db.insert(clipFlags).values({
|
|
clipId: row.clip.id,
|
|
revisionId: revision?.id ?? null,
|
|
reporterType: actor.actorType,
|
|
reporterId: actor.actorType === "anonymous" ? null : actor.actorId,
|
|
reason: input.reason,
|
|
details: input.details ?? null,
|
|
metadata: input.metadata ?? {},
|
|
});
|
|
const [updated] = await db
|
|
.update(clips)
|
|
.set({
|
|
reportCount: sql`${clips.reportCount} + 1`,
|
|
moderationState: row.clip.moderationState === "normal" ? "under_review" : row.clip.moderationState,
|
|
updatedAt: new Date(),
|
|
})
|
|
.where(eq(clips.id, row.clip.id))
|
|
.returning();
|
|
await rebuildRankingSnapshot(updated);
|
|
return updated;
|
|
}
|
|
|
|
async function createComment(slug: string, input: CreateClipComment, actor: Actor) {
|
|
const row = await getPublicClipBySlug(slug);
|
|
if (!row) throw notFound("Clip not found");
|
|
const revision = input.revisionNumber ? await getRevisionByNumber(row.clip.id, input.revisionNumber) : null;
|
|
await db.insert(clipComments).values({
|
|
clipId: row.clip.id,
|
|
revisionId: revision?.id ?? null,
|
|
authorUserId: actor.userId ?? null,
|
|
authorAgentId: actor.agentId ?? null,
|
|
scope: input.scope ?? (revision ? "revision" : "clip"),
|
|
category: input.category ?? "question",
|
|
body: input.body,
|
|
});
|
|
const [updated] = await db
|
|
.update(clips)
|
|
.set({ commentCount: sql`${clips.commentCount} + 1`, updatedAt: new Date() })
|
|
.where(eq(clips.id, row.clip.id))
|
|
.returning();
|
|
return updated;
|
|
}
|
|
|
|
async function createShowcase(slug: string, input: CreateClipShowcase, actor: Actor) {
|
|
const row = await getPublicClipBySlug(slug);
|
|
if (!row) throw notFound("Clip not found");
|
|
const revision = input.revisionNumber
|
|
? await getRevisionByNumber(row.clip.id, input.revisionNumber)
|
|
: await getPublicCurrentRevision(row.clip);
|
|
if (!revision) throw notFound("Clip revision not found");
|
|
await db.insert(clipShowcasePosts).values({
|
|
clipId: row.clip.id,
|
|
revisionId: revision.id,
|
|
authorUserId: actor.userId ?? null,
|
|
authorAgentId: actor.agentId ?? null,
|
|
type: input.type ?? "community_example",
|
|
title: input.title,
|
|
body: input.body ?? null,
|
|
mediaRefs: input.mediaRefs ?? [],
|
|
validationState: input.validationState ?? "not_run",
|
|
});
|
|
const [updated] = await db
|
|
.update(clips)
|
|
.set({
|
|
showcaseCount: sql`${clips.showcaseCount} + 1`,
|
|
successfulFirstRunCount: input.validationState === "passed"
|
|
? sql`${clips.successfulFirstRunCount} + 1`
|
|
: row.clip.successfulFirstRunCount,
|
|
updatedAt: new Date(),
|
|
})
|
|
.where(eq(clips.id, row.clip.id))
|
|
.returning();
|
|
await rebuildRankingSnapshot(updated);
|
|
return updated;
|
|
}
|
|
|
|
async function recordImportTelemetry(slug: string, input: CreateClipImportTelemetry, actor: Actor) {
|
|
const row = await getPublicClipBySlug(slug);
|
|
if (!row) throw notFound("Clip not found");
|
|
const revision = input.revisionNumber
|
|
? await getRevisionByNumber(row.clip.id, input.revisionNumber)
|
|
: await getPublicCurrentRevision(row.clip);
|
|
if (!revision) throw notFound("Clip revision not found");
|
|
await db.insert(clipPublicMetricEvents).values({
|
|
clipId: row.clip.id,
|
|
revisionId: revision.id,
|
|
eventType: "import_telemetry",
|
|
actorHash: actor.actorType === "anonymous" ? null : `${actor.actorType}:${actor.actorId}`,
|
|
metadata: input.metadata ?? {},
|
|
});
|
|
if (input.destinationCompanyId) {
|
|
await db.insert(clipImports).values({
|
|
destinationCompanyId: input.destinationCompanyId,
|
|
clipId: row.clip.id,
|
|
revisionId: revision.id,
|
|
revisionNumber: revision.revisionNumber,
|
|
manifestChecksum: revision.manifestChecksum,
|
|
artifactChecksum: revision.artifactChecksum,
|
|
sourceUrl: input.sourceUrl ?? null,
|
|
revisionUrl: input.revisionUrl ?? null,
|
|
status: input.status ?? "previewed",
|
|
importedByUserId: actor.userId ?? null,
|
|
importedByAgentId: actor.agentId ?? null,
|
|
});
|
|
}
|
|
const [updated] = await db
|
|
.update(clips)
|
|
.set({
|
|
importCount: input.status === "applied" ? sql`${clips.importCount} + 1` : row.clip.importCount,
|
|
updatedAt: new Date(),
|
|
})
|
|
.where(eq(clips.id, row.clip.id))
|
|
.returning();
|
|
await rebuildRankingSnapshot(updated);
|
|
return updated;
|
|
}
|
|
|
|
async function refreshVoteCounters(clipId: string) {
|
|
const [updated] = await db
|
|
.update(clips)
|
|
.set({
|
|
upvoteCount: sql`(select count(*)::int from ${clipVotes} where ${clipVotes.clipId} = ${clipId} and ${clipVotes.vote} = 'up')`,
|
|
downvoteCount: sql`(select count(*)::int from ${clipVotes} where ${clipVotes.clipId} = ${clipId} and ${clipVotes.vote} = 'down')`,
|
|
voteScore: sql`(
|
|
(select count(*)::int from ${clipVotes} where ${clipVotes.clipId} = ${clipId} and ${clipVotes.vote} = 'up')
|
|
-
|
|
(select count(*)::int from ${clipVotes} where ${clipVotes.clipId} = ${clipId} and ${clipVotes.vote} = 'down')
|
|
)`,
|
|
updatedAt: new Date(),
|
|
})
|
|
.where(eq(clips.id, clipId))
|
|
.returning();
|
|
return updated;
|
|
}
|
|
|
|
async function rebuildRankingSnapshot(clip: typeof clips.$inferSelect) {
|
|
const reviewBonus = clip.moderationState === "normal" ? 10 : clip.moderationState === "warning" ? -10 : -30;
|
|
const score =
|
|
reviewBonus
|
|
+ clip.successfulFirstRunCount * 10
|
|
+ clip.importCount * 3
|
|
+ clip.upvoteCount * 2
|
|
- clip.downvoteCount * 2
|
|
- clip.reportCount * 8
|
|
+ clip.showcaseCount * 3;
|
|
await db
|
|
.insert(clipRankingSnapshots)
|
|
.values({
|
|
clipId: clip.id,
|
|
scope: "global",
|
|
score,
|
|
factors: {
|
|
moderationState: clip.moderationState,
|
|
successfulFirstRunCount: clip.successfulFirstRunCount,
|
|
importCount: clip.importCount,
|
|
upvoteCount: clip.upvoteCount,
|
|
downvoteCount: clip.downvoteCount,
|
|
reportCount: clip.reportCount,
|
|
showcaseCount: clip.showcaseCount,
|
|
},
|
|
})
|
|
.onConflictDoUpdate({
|
|
target: [clipRankingSnapshots.clipId, clipRankingSnapshots.scope],
|
|
set: {
|
|
score,
|
|
factors: {
|
|
moderationState: clip.moderationState,
|
|
successfulFirstRunCount: clip.successfulFirstRunCount,
|
|
importCount: clip.importCount,
|
|
upvoteCount: clip.upvoteCount,
|
|
downvoteCount: clip.downvoteCount,
|
|
reportCount: clip.reportCount,
|
|
showcaseCount: clip.showcaseCount,
|
|
},
|
|
snapshotAt: new Date(),
|
|
},
|
|
});
|
|
}
|
|
|
|
async function getCreatorPublicProfile(handle: string) {
|
|
const profile = first(
|
|
await db
|
|
.select()
|
|
.from(clipCreatorProfiles)
|
|
.where(ilike(clipCreatorProfiles.handle, handle))
|
|
.limit(1),
|
|
);
|
|
if (!profile) return null;
|
|
const publicClips = await listPublic({
|
|
creatorProfileId: profile.id,
|
|
limit: 100,
|
|
});
|
|
return {
|
|
profile: {
|
|
id: profile.id,
|
|
handle: profile.handle,
|
|
displayName: profile.displayName,
|
|
bio: profile.bio,
|
|
avatarUrl: profile.avatarUrl,
|
|
websiteUrl: profile.websiteUrl,
|
|
verificationState: profile.verificationState,
|
|
reputationSummary: profile.reputationSummary,
|
|
},
|
|
clips: publicClips,
|
|
};
|
|
}
|
|
|
|
return {
|
|
createCreatorProfile,
|
|
getCreatorProfileById,
|
|
getCreatorProfileByHandle,
|
|
getClipById,
|
|
getClipBySlug,
|
|
listPublic,
|
|
getPublicDetail,
|
|
getPublicRevision,
|
|
getCreatorPublicProfile,
|
|
publish,
|
|
createRevision,
|
|
updateClip,
|
|
createVote,
|
|
createReport,
|
|
createComment,
|
|
createShowcase,
|
|
recordImportTelemetry,
|
|
};
|
|
}
|