mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-04 19:28:46 +01:00
Add public clip backend persistence
This commit is contained in:
1 parent
cfb42dbb95
commit
881ada6e02
17 files changed
+22059
No files matched your search
@@ -0,0 +1,271 @@
|
||||
CREATE TABLE "clip_comments" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"clip_id" uuid NOT NULL,
|
||||
"revision_id" uuid,
|
||||
"creator_profile_id" uuid,
|
||||
"author_user_id" text,
|
||||
"author_agent_id" uuid,
|
||||
"scope" text DEFAULT 'clip' NOT NULL,
|
||||
"category" text DEFAULT 'question' NOT NULL,
|
||||
"moderation_state" text DEFAULT 'normal' NOT NULL,
|
||||
"body" text NOT NULL,
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "clip_creator_profiles" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"company_id" uuid NOT NULL,
|
||||
"handle" text NOT NULL,
|
||||
"display_name" text NOT NULL,
|
||||
"bio" text,
|
||||
"avatar_url" text,
|
||||
"website_url" text,
|
||||
"verification_state" text DEFAULT 'unverified' NOT NULL,
|
||||
"reputation_summary" jsonb DEFAULT '{}'::jsonb NOT NULL,
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "clip_dependencies" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"clip_id" uuid NOT NULL,
|
||||
"revision_id" uuid NOT NULL,
|
||||
"type" text NOT NULL,
|
||||
"key" text NOT NULL,
|
||||
"display_name" text,
|
||||
"required" text DEFAULT 'required' NOT NULL,
|
||||
"metadata" jsonb DEFAULT '{}'::jsonb NOT NULL,
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "clip_flags" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"clip_id" uuid NOT NULL,
|
||||
"revision_id" uuid,
|
||||
"reporter_type" text DEFAULT 'anonymous' NOT NULL,
|
||||
"reporter_id" text,
|
||||
"reason" text NOT NULL,
|
||||
"details" text,
|
||||
"status" text DEFAULT 'open' NOT NULL,
|
||||
"metadata" jsonb DEFAULT '{}'::jsonb NOT NULL,
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "clip_imported_objects" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"clip_import_id" uuid NOT NULL,
|
||||
"destination_object_type" text NOT NULL,
|
||||
"destination_object_id" text NOT NULL,
|
||||
"package_path" text,
|
||||
"action" text NOT NULL,
|
||||
"local_fingerprint" text,
|
||||
"metadata" jsonb DEFAULT '{}'::jsonb NOT NULL,
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "clip_imports" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"destination_company_id" uuid NOT NULL,
|
||||
"clip_id" uuid NOT NULL,
|
||||
"revision_id" uuid NOT NULL,
|
||||
"revision_number" integer NOT NULL,
|
||||
"manifest_checksum" text NOT NULL,
|
||||
"artifact_checksum" text NOT NULL,
|
||||
"source_url" text,
|
||||
"revision_url" text,
|
||||
"status" text DEFAULT 'previewed' NOT NULL,
|
||||
"imported_by_user_id" text,
|
||||
"imported_by_agent_id" uuid,
|
||||
"collision_strategy" text,
|
||||
"selected_options" jsonb DEFAULT '{}'::jsonb NOT NULL,
|
||||
"first_run_validation_state" text DEFAULT 'not_run' NOT NULL,
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "clip_moderation_events" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"clip_id" uuid NOT NULL,
|
||||
"revision_id" uuid,
|
||||
"actor_type" text NOT NULL,
|
||||
"actor_id" text NOT NULL,
|
||||
"from_state" text,
|
||||
"to_state" text NOT NULL,
|
||||
"reason" text NOT NULL,
|
||||
"metadata" jsonb DEFAULT '{}'::jsonb NOT NULL,
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "clip_public_metric_events" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"clip_id" uuid NOT NULL,
|
||||
"revision_id" uuid,
|
||||
"event_type" text NOT NULL,
|
||||
"actor_hash" text,
|
||||
"metadata" jsonb DEFAULT '{}'::jsonb NOT NULL,
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "clip_ranking_snapshots" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"clip_id" uuid NOT NULL,
|
||||
"scope" text DEFAULT 'global' NOT NULL,
|
||||
"score" integer DEFAULT 0 NOT NULL,
|
||||
"factors" jsonb DEFAULT '{}'::jsonb NOT NULL,
|
||||
"snapshot_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "clip_revisions" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"clip_id" uuid NOT NULL,
|
||||
"revision_number" integer NOT NULL,
|
||||
"manifest_version" text DEFAULT 'paperclip.clip/v1' NOT NULL,
|
||||
"manifest_checksum" text NOT NULL,
|
||||
"artifact_checksum" text NOT NULL,
|
||||
"manifest_payload" jsonb NOT NULL,
|
||||
"artifact_ref" jsonb,
|
||||
"dependency_graph" jsonb DEFAULT '{}'::jsonb NOT NULL,
|
||||
"permissions" jsonb DEFAULT '[]'::jsonb NOT NULL,
|
||||
"secrets_schema" jsonb DEFAULT '[]'::jsonb NOT NULL,
|
||||
"budget_estimate" jsonb,
|
||||
"redaction_report" jsonb DEFAULT '{}'::jsonb NOT NULL,
|
||||
"dangerous_capabilities" jsonb DEFAULT '[]'::jsonb NOT NULL,
|
||||
"security_review_state" text DEFAULT 'unreviewed' NOT NULL,
|
||||
"verification_state" text DEFAULT 'not_run' NOT NULL,
|
||||
"compatibility" jsonb DEFAULT '{}'::jsonb NOT NULL,
|
||||
"change_summary" text,
|
||||
"breaking_changes" text,
|
||||
"migration_notes" text,
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "clip_showcase_posts" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"clip_id" uuid NOT NULL,
|
||||
"revision_id" uuid NOT NULL,
|
||||
"creator_profile_id" uuid,
|
||||
"author_user_id" text,
|
||||
"author_agent_id" uuid,
|
||||
"type" text DEFAULT 'community_example' NOT NULL,
|
||||
"title" text NOT NULL,
|
||||
"body" text,
|
||||
"media_refs" jsonb DEFAULT '[]'::jsonb NOT NULL,
|
||||
"validation_state" text DEFAULT 'not_run' NOT NULL,
|
||||
"moderation_state" text DEFAULT 'normal' NOT NULL,
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "clip_votes" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"clip_id" uuid NOT NULL,
|
||||
"revision_id" uuid NOT NULL,
|
||||
"actor_type" text NOT NULL,
|
||||
"actor_id" text NOT NULL,
|
||||
"vote" text NOT NULL,
|
||||
"reason" text,
|
||||
"metadata" jsonb DEFAULT '{}'::jsonb NOT NULL,
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "clips" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"source_company_id" uuid NOT NULL,
|
||||
"creator_profile_id" uuid NOT NULL,
|
||||
"slug" text NOT NULL,
|
||||
"type" text NOT NULL,
|
||||
"title" text NOT NULL,
|
||||
"summary" text NOT NULL,
|
||||
"description" text,
|
||||
"visibility" text DEFAULT 'unlisted' NOT NULL,
|
||||
"status" text DEFAULT 'pending_review' NOT NULL,
|
||||
"moderation_state" text DEFAULT 'normal' NOT NULL,
|
||||
"current_revision_id" uuid,
|
||||
"latest_approved_revision_id" uuid,
|
||||
"tags" jsonb DEFAULT '[]'::jsonb NOT NULL,
|
||||
"categories" jsonb DEFAULT '[]'::jsonb NOT NULL,
|
||||
"use_cases" jsonb DEFAULT '[]'::jsonb NOT NULL,
|
||||
"required_providers" jsonb DEFAULT '[]'::jsonb NOT NULL,
|
||||
"compatibility" jsonb DEFAULT '{}'::jsonb NOT NULL,
|
||||
"source_kind" text,
|
||||
"source_object_type" text,
|
||||
"source_object_id" text,
|
||||
"import_count" integer DEFAULT 0 NOT NULL,
|
||||
"successful_first_run_count" integer DEFAULT 0 NOT NULL,
|
||||
"vote_score" integer DEFAULT 0 NOT NULL,
|
||||
"upvote_count" integer DEFAULT 0 NOT NULL,
|
||||
"downvote_count" integer DEFAULT 0 NOT NULL,
|
||||
"comment_count" integer DEFAULT 0 NOT NULL,
|
||||
"showcase_count" integer DEFAULT 0 NOT NULL,
|
||||
"report_count" integer DEFAULT 0 NOT NULL,
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"delisted_at" timestamp with time zone
|
||||
);
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "clip_comments" ADD CONSTRAINT "clip_comments_clip_id_clips_id_fk" FOREIGN KEY ("clip_id") REFERENCES "public"."clips"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "clip_comments" ADD CONSTRAINT "clip_comments_revision_id_clip_revisions_id_fk" FOREIGN KEY ("revision_id") REFERENCES "public"."clip_revisions"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "clip_comments" ADD CONSTRAINT "clip_comments_creator_profile_id_clip_creator_profiles_id_fk" FOREIGN KEY ("creator_profile_id") REFERENCES "public"."clip_creator_profiles"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "clip_creator_profiles" ADD CONSTRAINT "clip_creator_profiles_company_id_companies_id_fk" FOREIGN KEY ("company_id") REFERENCES "public"."companies"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "clip_dependencies" ADD CONSTRAINT "clip_dependencies_clip_id_clips_id_fk" FOREIGN KEY ("clip_id") REFERENCES "public"."clips"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "clip_dependencies" ADD CONSTRAINT "clip_dependencies_revision_id_clip_revisions_id_fk" FOREIGN KEY ("revision_id") REFERENCES "public"."clip_revisions"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "clip_flags" ADD CONSTRAINT "clip_flags_clip_id_clips_id_fk" FOREIGN KEY ("clip_id") REFERENCES "public"."clips"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "clip_flags" ADD CONSTRAINT "clip_flags_revision_id_clip_revisions_id_fk" FOREIGN KEY ("revision_id") REFERENCES "public"."clip_revisions"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "clip_imported_objects" ADD CONSTRAINT "clip_imported_objects_clip_import_id_clip_imports_id_fk" FOREIGN KEY ("clip_import_id") REFERENCES "public"."clip_imports"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "clip_imports" ADD CONSTRAINT "clip_imports_destination_company_id_companies_id_fk" FOREIGN KEY ("destination_company_id") REFERENCES "public"."companies"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "clip_imports" ADD CONSTRAINT "clip_imports_clip_id_clips_id_fk" FOREIGN KEY ("clip_id") REFERENCES "public"."clips"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "clip_imports" ADD CONSTRAINT "clip_imports_revision_id_clip_revisions_id_fk" FOREIGN KEY ("revision_id") REFERENCES "public"."clip_revisions"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "clip_moderation_events" ADD CONSTRAINT "clip_moderation_events_clip_id_clips_id_fk" FOREIGN KEY ("clip_id") REFERENCES "public"."clips"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "clip_moderation_events" ADD CONSTRAINT "clip_moderation_events_revision_id_clip_revisions_id_fk" FOREIGN KEY ("revision_id") REFERENCES "public"."clip_revisions"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "clip_public_metric_events" ADD CONSTRAINT "clip_public_metric_events_clip_id_clips_id_fk" FOREIGN KEY ("clip_id") REFERENCES "public"."clips"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "clip_public_metric_events" ADD CONSTRAINT "clip_public_metric_events_revision_id_clip_revisions_id_fk" FOREIGN KEY ("revision_id") REFERENCES "public"."clip_revisions"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "clip_ranking_snapshots" ADD CONSTRAINT "clip_ranking_snapshots_clip_id_clips_id_fk" FOREIGN KEY ("clip_id") REFERENCES "public"."clips"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "clip_revisions" ADD CONSTRAINT "clip_revisions_clip_id_clips_id_fk" FOREIGN KEY ("clip_id") REFERENCES "public"."clips"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "clip_showcase_posts" ADD CONSTRAINT "clip_showcase_posts_clip_id_clips_id_fk" FOREIGN KEY ("clip_id") REFERENCES "public"."clips"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "clip_showcase_posts" ADD CONSTRAINT "clip_showcase_posts_revision_id_clip_revisions_id_fk" FOREIGN KEY ("revision_id") REFERENCES "public"."clip_revisions"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "clip_showcase_posts" ADD CONSTRAINT "clip_showcase_posts_creator_profile_id_clip_creator_profiles_id_fk" FOREIGN KEY ("creator_profile_id") REFERENCES "public"."clip_creator_profiles"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "clip_votes" ADD CONSTRAINT "clip_votes_clip_id_clips_id_fk" FOREIGN KEY ("clip_id") REFERENCES "public"."clips"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "clip_votes" ADD CONSTRAINT "clip_votes_revision_id_clip_revisions_id_fk" FOREIGN KEY ("revision_id") REFERENCES "public"."clip_revisions"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "clips" ADD CONSTRAINT "clips_source_company_id_companies_id_fk" FOREIGN KEY ("source_company_id") REFERENCES "public"."companies"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "clips" ADD CONSTRAINT "clips_creator_profile_id_clip_creator_profiles_id_fk" FOREIGN KEY ("creator_profile_id") REFERENCES "public"."clip_creator_profiles"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||
CREATE INDEX "clip_comments_clip_created_idx" ON "clip_comments" USING btree ("clip_id","created_at");--> statement-breakpoint
|
||||
CREATE INDEX "clip_comments_revision_idx" ON "clip_comments" USING btree ("revision_id");--> statement-breakpoint
|
||||
CREATE INDEX "clip_creator_profiles_company_idx" ON "clip_creator_profiles" USING btree ("company_id");--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX "clip_creator_profiles_handle_uq" ON "clip_creator_profiles" USING btree ("handle");--> statement-breakpoint
|
||||
CREATE INDEX "clip_dependencies_revision_idx" ON "clip_dependencies" USING btree ("revision_id");--> statement-breakpoint
|
||||
CREATE INDEX "clip_dependencies_type_key_idx" ON "clip_dependencies" USING btree ("type","key");--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX "clip_dependencies_revision_type_key_uq" ON "clip_dependencies" USING btree ("revision_id","type","key");--> statement-breakpoint
|
||||
CREATE INDEX "clip_flags_clip_status_idx" ON "clip_flags" USING btree ("clip_id","status");--> statement-breakpoint
|
||||
CREATE INDEX "clip_flags_revision_idx" ON "clip_flags" USING btree ("revision_id");--> statement-breakpoint
|
||||
CREATE INDEX "clip_imported_objects_import_idx" ON "clip_imported_objects" USING btree ("clip_import_id");--> statement-breakpoint
|
||||
CREATE INDEX "clip_imported_objects_destination_idx" ON "clip_imported_objects" USING btree ("destination_object_type","destination_object_id");--> statement-breakpoint
|
||||
CREATE INDEX "clip_imports_destination_idx" ON "clip_imports" USING btree ("destination_company_id","created_at");--> statement-breakpoint
|
||||
CREATE INDEX "clip_imports_revision_idx" ON "clip_imports" USING btree ("revision_id");--> statement-breakpoint
|
||||
CREATE INDEX "clip_moderation_events_clip_created_idx" ON "clip_moderation_events" USING btree ("clip_id","created_at");--> statement-breakpoint
|
||||
CREATE INDEX "clip_public_metric_events_clip_event_idx" ON "clip_public_metric_events" USING btree ("clip_id","event_type","created_at");--> statement-breakpoint
|
||||
CREATE INDEX "clip_ranking_snapshots_scope_score_idx" ON "clip_ranking_snapshots" USING btree ("scope","score");--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX "clip_ranking_snapshots_clip_scope_uq" ON "clip_ranking_snapshots" USING btree ("clip_id","scope");--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX "clip_revisions_clip_revision_uq" ON "clip_revisions" USING btree ("clip_id","revision_number");--> statement-breakpoint
|
||||
CREATE INDEX "clip_revisions_clip_created_idx" ON "clip_revisions" USING btree ("clip_id","created_at");--> statement-breakpoint
|
||||
CREATE INDEX "clip_revisions_manifest_checksum_idx" ON "clip_revisions" USING btree ("manifest_checksum");--> statement-breakpoint
|
||||
CREATE INDEX "clip_showcase_posts_clip_created_idx" ON "clip_showcase_posts" USING btree ("clip_id","created_at");--> statement-breakpoint
|
||||
CREATE INDEX "clip_showcase_posts_revision_idx" ON "clip_showcase_posts" USING btree ("revision_id");--> statement-breakpoint
|
||||
CREATE INDEX "clip_votes_revision_idx" ON "clip_votes" USING btree ("revision_id");--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX "clip_votes_revision_actor_uq" ON "clip_votes" USING btree ("revision_id","actor_type","actor_id");--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX "clips_slug_uq" ON "clips" USING btree ("slug");--> statement-breakpoint
|
||||
CREATE INDEX "clips_source_company_idx" ON "clips" USING btree ("source_company_id");--> statement-breakpoint
|
||||
CREATE INDEX "clips_creator_idx" ON "clips" USING btree ("creator_profile_id");--> statement-breakpoint
|
||||
CREATE INDEX "clips_public_browse_idx" ON "clips" USING btree ("visibility","status","moderation_state","updated_at");--> statement-breakpoint
|
||||
CREATE INDEX "clips_type_idx" ON "clips" USING btree ("type");--> statement-breakpoint
|
||||
CREATE OR REPLACE FUNCTION prevent_clip_revision_update()
|
||||
RETURNS trigger AS $$
|
||||
BEGIN
|
||||
RAISE EXCEPTION 'clip_revisions are immutable; publish a new revision instead';
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;--> statement-breakpoint
|
||||
CREATE TRIGGER prevent_clip_revision_update_trigger
|
||||
BEFORE UPDATE ON "clip_revisions"
|
||||
FOR EACH ROW EXECUTE FUNCTION prevent_clip_revision_update();
|
||||
File diff suppressed because it is too large.
Load diff
@@ -603,6 +603,13 @@
|
||||
"when": 1778787362162,
|
||||
"tag": "0085_tranquil_the_executioner",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 86,
|
||||
"version": "7",
|
||||
"when": 1778937700331,
|
||||
"tag": "0086_clean_red_hulk",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,304 @@
|
||||
import { integer, jsonb, pgTable, text, timestamp, uniqueIndex, uuid, index } from "drizzle-orm/pg-core";
|
||||
import { companies } from "./companies.js";
|
||||
|
||||
export const clipCreatorProfiles = pgTable(
|
||||
"clip_creator_profiles",
|
||||
{
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
companyId: uuid("company_id").notNull().references(() => companies.id),
|
||||
handle: text("handle").notNull(),
|
||||
displayName: text("display_name").notNull(),
|
||||
bio: text("bio"),
|
||||
avatarUrl: text("avatar_url"),
|
||||
websiteUrl: text("website_url"),
|
||||
verificationState: text("verification_state").notNull().default("unverified"),
|
||||
reputationSummary: jsonb("reputation_summary").$type<Record<string, unknown>>().notNull().default({}),
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
},
|
||||
(table) => ({
|
||||
companyIdx: index("clip_creator_profiles_company_idx").on(table.companyId),
|
||||
handleUq: uniqueIndex("clip_creator_profiles_handle_uq").on(table.handle),
|
||||
}),
|
||||
);
|
||||
|
||||
export const clips = pgTable(
|
||||
"clips",
|
||||
{
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
sourceCompanyId: uuid("source_company_id").notNull().references(() => companies.id),
|
||||
creatorProfileId: uuid("creator_profile_id").notNull().references(() => clipCreatorProfiles.id),
|
||||
slug: text("slug").notNull(),
|
||||
type: text("type").notNull(),
|
||||
title: text("title").notNull(),
|
||||
summary: text("summary").notNull(),
|
||||
description: text("description"),
|
||||
visibility: text("visibility").notNull().default("unlisted"),
|
||||
status: text("status").notNull().default("pending_review"),
|
||||
moderationState: text("moderation_state").notNull().default("normal"),
|
||||
currentRevisionId: uuid("current_revision_id"),
|
||||
latestApprovedRevisionId: uuid("latest_approved_revision_id"),
|
||||
tags: jsonb("tags").$type<string[]>().notNull().default([]),
|
||||
categories: jsonb("categories").$type<string[]>().notNull().default([]),
|
||||
useCases: jsonb("use_cases").$type<string[]>().notNull().default([]),
|
||||
requiredProviders: jsonb("required_providers").$type<string[]>().notNull().default([]),
|
||||
compatibility: jsonb("compatibility").$type<Record<string, unknown>>().notNull().default({}),
|
||||
sourceKind: text("source_kind"),
|
||||
sourceObjectType: text("source_object_type"),
|
||||
sourceObjectId: text("source_object_id"),
|
||||
importCount: integer("import_count").notNull().default(0),
|
||||
successfulFirstRunCount: integer("successful_first_run_count").notNull().default(0),
|
||||
voteScore: integer("vote_score").notNull().default(0),
|
||||
upvoteCount: integer("upvote_count").notNull().default(0),
|
||||
downvoteCount: integer("downvote_count").notNull().default(0),
|
||||
commentCount: integer("comment_count").notNull().default(0),
|
||||
showcaseCount: integer("showcase_count").notNull().default(0),
|
||||
reportCount: integer("report_count").notNull().default(0),
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
delistedAt: timestamp("delisted_at", { withTimezone: true }),
|
||||
},
|
||||
(table) => ({
|
||||
slugUq: uniqueIndex("clips_slug_uq").on(table.slug),
|
||||
sourceCompanyIdx: index("clips_source_company_idx").on(table.sourceCompanyId),
|
||||
creatorIdx: index("clips_creator_idx").on(table.creatorProfileId),
|
||||
publicBrowseIdx: index("clips_public_browse_idx").on(table.visibility, table.status, table.moderationState, table.updatedAt),
|
||||
typeIdx: index("clips_type_idx").on(table.type),
|
||||
}),
|
||||
);
|
||||
|
||||
export const clipRevisions = pgTable(
|
||||
"clip_revisions",
|
||||
{
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
clipId: uuid("clip_id").notNull().references(() => clips.id),
|
||||
revisionNumber: integer("revision_number").notNull(),
|
||||
manifestVersion: text("manifest_version").notNull().default("paperclip.clip/v1"),
|
||||
manifestChecksum: text("manifest_checksum").notNull(),
|
||||
artifactChecksum: text("artifact_checksum").notNull(),
|
||||
manifestPayload: jsonb("manifest_payload").$type<Record<string, unknown>>().notNull(),
|
||||
artifactRef: jsonb("artifact_ref").$type<Record<string, unknown> | null>(),
|
||||
dependencyGraph: jsonb("dependency_graph").$type<Record<string, unknown>>().notNull().default({}),
|
||||
permissions: jsonb("permissions").$type<Record<string, unknown>[]>().notNull().default([]),
|
||||
secretsSchema: jsonb("secrets_schema").$type<Record<string, unknown>[]>().notNull().default([]),
|
||||
budgetEstimate: jsonb("budget_estimate").$type<Record<string, unknown> | null>(),
|
||||
redactionReport: jsonb("redaction_report").$type<Record<string, unknown>>().notNull().default({}),
|
||||
dangerousCapabilities: jsonb("dangerous_capabilities").$type<string[]>().notNull().default([]),
|
||||
securityReviewState: text("security_review_state").notNull().default("unreviewed"),
|
||||
verificationState: text("verification_state").notNull().default("not_run"),
|
||||
compatibility: jsonb("compatibility").$type<Record<string, unknown>>().notNull().default({}),
|
||||
changeSummary: text("change_summary"),
|
||||
breakingChanges: text("breaking_changes"),
|
||||
migrationNotes: text("migration_notes"),
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
},
|
||||
(table) => ({
|
||||
clipRevisionUq: uniqueIndex("clip_revisions_clip_revision_uq").on(table.clipId, table.revisionNumber),
|
||||
clipCreatedIdx: index("clip_revisions_clip_created_idx").on(table.clipId, table.createdAt),
|
||||
manifestChecksumIdx: index("clip_revisions_manifest_checksum_idx").on(table.manifestChecksum),
|
||||
}),
|
||||
);
|
||||
|
||||
export const clipDependencies = pgTable(
|
||||
"clip_dependencies",
|
||||
{
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
clipId: uuid("clip_id").notNull().references(() => clips.id),
|
||||
revisionId: uuid("revision_id").notNull().references(() => clipRevisions.id),
|
||||
type: text("type").notNull(),
|
||||
key: text("key").notNull(),
|
||||
displayName: text("display_name"),
|
||||
required: text("required").notNull().default("required"),
|
||||
metadata: jsonb("metadata").$type<Record<string, unknown>>().notNull().default({}),
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
},
|
||||
(table) => ({
|
||||
revisionIdx: index("clip_dependencies_revision_idx").on(table.revisionId),
|
||||
typeKeyIdx: index("clip_dependencies_type_key_idx").on(table.type, table.key),
|
||||
revisionTypeKeyUq: uniqueIndex("clip_dependencies_revision_type_key_uq").on(table.revisionId, table.type, table.key),
|
||||
}),
|
||||
);
|
||||
|
||||
export const clipComments = pgTable(
|
||||
"clip_comments",
|
||||
{
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
clipId: uuid("clip_id").notNull().references(() => clips.id),
|
||||
revisionId: uuid("revision_id").references(() => clipRevisions.id),
|
||||
creatorProfileId: uuid("creator_profile_id").references(() => clipCreatorProfiles.id),
|
||||
authorUserId: text("author_user_id"),
|
||||
authorAgentId: uuid("author_agent_id"),
|
||||
scope: text("scope").notNull().default("clip"),
|
||||
category: text("category").notNull().default("question"),
|
||||
moderationState: text("moderation_state").notNull().default("normal"),
|
||||
body: text("body").notNull(),
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
},
|
||||
(table) => ({
|
||||
clipCreatedIdx: index("clip_comments_clip_created_idx").on(table.clipId, table.createdAt),
|
||||
revisionIdx: index("clip_comments_revision_idx").on(table.revisionId),
|
||||
}),
|
||||
);
|
||||
|
||||
export const clipVotes = pgTable(
|
||||
"clip_votes",
|
||||
{
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
clipId: uuid("clip_id").notNull().references(() => clips.id),
|
||||
revisionId: uuid("revision_id").notNull().references(() => clipRevisions.id),
|
||||
actorType: text("actor_type").notNull(),
|
||||
actorId: text("actor_id").notNull(),
|
||||
vote: text("vote").notNull(),
|
||||
reason: text("reason"),
|
||||
metadata: jsonb("metadata").$type<Record<string, unknown>>().notNull().default({}),
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
},
|
||||
(table) => ({
|
||||
revisionIdx: index("clip_votes_revision_idx").on(table.revisionId),
|
||||
actorUq: uniqueIndex("clip_votes_revision_actor_uq").on(table.revisionId, table.actorType, table.actorId),
|
||||
}),
|
||||
);
|
||||
|
||||
export const clipShowcasePosts = pgTable(
|
||||
"clip_showcase_posts",
|
||||
{
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
clipId: uuid("clip_id").notNull().references(() => clips.id),
|
||||
revisionId: uuid("revision_id").notNull().references(() => clipRevisions.id),
|
||||
creatorProfileId: uuid("creator_profile_id").references(() => clipCreatorProfiles.id),
|
||||
authorUserId: text("author_user_id"),
|
||||
authorAgentId: uuid("author_agent_id"),
|
||||
type: text("type").notNull().default("community_example"),
|
||||
title: text("title").notNull(),
|
||||
body: text("body"),
|
||||
mediaRefs: jsonb("media_refs").$type<Record<string, unknown>[]>().notNull().default([]),
|
||||
validationState: text("validation_state").notNull().default("not_run"),
|
||||
moderationState: text("moderation_state").notNull().default("normal"),
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
},
|
||||
(table) => ({
|
||||
clipCreatedIdx: index("clip_showcase_posts_clip_created_idx").on(table.clipId, table.createdAt),
|
||||
revisionIdx: index("clip_showcase_posts_revision_idx").on(table.revisionId),
|
||||
}),
|
||||
);
|
||||
|
||||
export const clipFlags = pgTable(
|
||||
"clip_flags",
|
||||
{
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
clipId: uuid("clip_id").notNull().references(() => clips.id),
|
||||
revisionId: uuid("revision_id").references(() => clipRevisions.id),
|
||||
reporterType: text("reporter_type").notNull().default("anonymous"),
|
||||
reporterId: text("reporter_id"),
|
||||
reason: text("reason").notNull(),
|
||||
details: text("details"),
|
||||
status: text("status").notNull().default("open"),
|
||||
metadata: jsonb("metadata").$type<Record<string, unknown>>().notNull().default({}),
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
},
|
||||
(table) => ({
|
||||
clipStatusIdx: index("clip_flags_clip_status_idx").on(table.clipId, table.status),
|
||||
revisionIdx: index("clip_flags_revision_idx").on(table.revisionId),
|
||||
}),
|
||||
);
|
||||
|
||||
export const clipModerationEvents = pgTable(
|
||||
"clip_moderation_events",
|
||||
{
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
clipId: uuid("clip_id").notNull().references(() => clips.id),
|
||||
revisionId: uuid("revision_id").references(() => clipRevisions.id),
|
||||
actorType: text("actor_type").notNull(),
|
||||
actorId: text("actor_id").notNull(),
|
||||
fromState: text("from_state"),
|
||||
toState: text("to_state").notNull(),
|
||||
reason: text("reason").notNull(),
|
||||
metadata: jsonb("metadata").$type<Record<string, unknown>>().notNull().default({}),
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
},
|
||||
(table) => ({
|
||||
clipCreatedIdx: index("clip_moderation_events_clip_created_idx").on(table.clipId, table.createdAt),
|
||||
}),
|
||||
);
|
||||
|
||||
export const clipRankingSnapshots = pgTable(
|
||||
"clip_ranking_snapshots",
|
||||
{
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
clipId: uuid("clip_id").notNull().references(() => clips.id),
|
||||
scope: text("scope").notNull().default("global"),
|
||||
score: integer("score").notNull().default(0),
|
||||
factors: jsonb("factors").$type<Record<string, unknown>>().notNull().default({}),
|
||||
snapshotAt: timestamp("snapshot_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
},
|
||||
(table) => ({
|
||||
scopeScoreIdx: index("clip_ranking_snapshots_scope_score_idx").on(table.scope, table.score),
|
||||
clipScopeUq: uniqueIndex("clip_ranking_snapshots_clip_scope_uq").on(table.clipId, table.scope),
|
||||
}),
|
||||
);
|
||||
|
||||
export const clipImports = pgTable(
|
||||
"clip_imports",
|
||||
{
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
destinationCompanyId: uuid("destination_company_id").notNull().references(() => companies.id),
|
||||
clipId: uuid("clip_id").notNull().references(() => clips.id),
|
||||
revisionId: uuid("revision_id").notNull().references(() => clipRevisions.id),
|
||||
revisionNumber: integer("revision_number").notNull(),
|
||||
manifestChecksum: text("manifest_checksum").notNull(),
|
||||
artifactChecksum: text("artifact_checksum").notNull(),
|
||||
sourceUrl: text("source_url"),
|
||||
revisionUrl: text("revision_url"),
|
||||
status: text("status").notNull().default("previewed"),
|
||||
importedByUserId: text("imported_by_user_id"),
|
||||
importedByAgentId: uuid("imported_by_agent_id"),
|
||||
collisionStrategy: text("collision_strategy"),
|
||||
selectedOptions: jsonb("selected_options").$type<Record<string, unknown>>().notNull().default({}),
|
||||
firstRunValidationState: text("first_run_validation_state").notNull().default("not_run"),
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
},
|
||||
(table) => ({
|
||||
destinationIdx: index("clip_imports_destination_idx").on(table.destinationCompanyId, table.createdAt),
|
||||
revisionIdx: index("clip_imports_revision_idx").on(table.revisionId),
|
||||
}),
|
||||
);
|
||||
|
||||
export const clipImportedObjects = pgTable(
|
||||
"clip_imported_objects",
|
||||
{
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
clipImportId: uuid("clip_import_id").notNull().references(() => clipImports.id),
|
||||
destinationObjectType: text("destination_object_type").notNull(),
|
||||
destinationObjectId: text("destination_object_id").notNull(),
|
||||
packagePath: text("package_path"),
|
||||
action: text("action").notNull(),
|
||||
localFingerprint: text("local_fingerprint"),
|
||||
metadata: jsonb("metadata").$type<Record<string, unknown>>().notNull().default({}),
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
},
|
||||
(table) => ({
|
||||
importIdx: index("clip_imported_objects_import_idx").on(table.clipImportId),
|
||||
destinationIdx: index("clip_imported_objects_destination_idx").on(table.destinationObjectType, table.destinationObjectId),
|
||||
}),
|
||||
);
|
||||
|
||||
export const clipPublicMetricEvents = pgTable(
|
||||
"clip_public_metric_events",
|
||||
{
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
clipId: uuid("clip_id").notNull().references(() => clips.id),
|
||||
revisionId: uuid("revision_id").references(() => clipRevisions.id),
|
||||
eventType: text("event_type").notNull(),
|
||||
actorHash: text("actor_hash"),
|
||||
metadata: jsonb("metadata").$type<Record<string, unknown>>().notNull().default({}),
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
},
|
||||
(table) => ({
|
||||
clipEventIdx: index("clip_public_metric_events_clip_event_idx").on(table.clipId, table.eventType, table.createdAt),
|
||||
}),
|
||||
);
|
||||
@@ -57,6 +57,21 @@ export { heartbeatRunEvents } from "./heartbeat_run_events.js";
|
||||
export { heartbeatRunWatchdogDecisions } from "./heartbeat_run_watchdog_decisions.js";
|
||||
export { costEvents } from "./cost_events.js";
|
||||
export { financeEvents } from "./finance_events.js";
|
||||
export {
|
||||
clipCreatorProfiles,
|
||||
clips,
|
||||
clipRevisions,
|
||||
clipDependencies,
|
||||
clipComments,
|
||||
clipVotes,
|
||||
clipShowcasePosts,
|
||||
clipFlags,
|
||||
clipModerationEvents,
|
||||
clipRankingSnapshots,
|
||||
clipImports,
|
||||
clipImportedObjects,
|
||||
clipPublicMetricEvents,
|
||||
} from "./clips.js";
|
||||
export { approvals } from "./approvals.js";
|
||||
export { approvalComments } from "./approval_comments.js";
|
||||
export { activityLog } from "./activity_log.js";
|
||||
|
||||
@@ -11,6 +11,8 @@ export const API = {
|
||||
goals: `${API_PREFIX}/goals`,
|
||||
approvals: `${API_PREFIX}/approvals`,
|
||||
secrets: `${API_PREFIX}/secrets`,
|
||||
clips: `${API_PREFIX}/clips`,
|
||||
publicClips: `${API_PREFIX}/public/clips`,
|
||||
secretProviderConfigs: `${API_PREFIX}/secret-provider-configs`,
|
||||
costs: `${API_PREFIX}/costs`,
|
||||
activity: `${API_PREFIX}/activity`,
|
||||
|
||||
@@ -701,6 +701,36 @@ export {
|
||||
FEEDBACK_VOTE_VALUES,
|
||||
DEFAULT_FEEDBACK_DATA_SHARING_TERMS_VERSION,
|
||||
} from "./types/feedback.js";
|
||||
export {
|
||||
CLIP_TYPES,
|
||||
CLIP_VISIBILITIES,
|
||||
CLIP_STATUSES,
|
||||
CLIP_MODERATION_STATES,
|
||||
CLIP_SECURITY_REVIEW_STATES,
|
||||
CLIP_VERIFICATION_STATES,
|
||||
CLIP_DEPENDENCY_TYPES,
|
||||
CLIP_DEPENDENCY_REQUIREMENTS,
|
||||
CLIP_COMMENT_CATEGORIES,
|
||||
CLIP_VOTE_VALUES,
|
||||
CLIP_REPORT_REASONS,
|
||||
CLIP_IMPORT_STATUSES,
|
||||
type ClipType,
|
||||
type ClipVisibility,
|
||||
type ClipStatus,
|
||||
type ClipModerationState,
|
||||
type ClipSecurityReviewState,
|
||||
type ClipVerificationState,
|
||||
type ClipDependencyType,
|
||||
type ClipDependencyRequirement,
|
||||
type ClipCommentCategory,
|
||||
type ClipVoteValue,
|
||||
type ClipReportReason,
|
||||
type ClipImportStatus,
|
||||
type ClipCreatorProfile,
|
||||
type ClipDependency,
|
||||
type ClipRevision,
|
||||
type PublicClip,
|
||||
} from "./types/clip.js";
|
||||
|
||||
export {
|
||||
DAILY_RETENTION_PRESETS,
|
||||
@@ -740,6 +770,24 @@ export {
|
||||
type UpdateCompany,
|
||||
type UpdateCompanyBranding,
|
||||
type UpsertIssueFeedbackVote,
|
||||
createClipCreatorProfileSchema,
|
||||
publishClipSchema,
|
||||
createClipRevisionSchema,
|
||||
updateClipSchema,
|
||||
createClipVoteSchema,
|
||||
createClipReportSchema,
|
||||
createClipCommentSchema,
|
||||
createClipShowcaseSchema,
|
||||
createClipImportTelemetrySchema,
|
||||
type CreateClipCreatorProfile,
|
||||
type PublishClip,
|
||||
type CreateClipRevision,
|
||||
type UpdateClip,
|
||||
type CreateClipVote,
|
||||
type CreateClipReport,
|
||||
type CreateClipComment,
|
||||
type CreateClipShowcase,
|
||||
type CreateClipImportTelemetry,
|
||||
environmentDriverSchema,
|
||||
environmentStatusSchema,
|
||||
environmentLeaseStatusSchema,
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
export const CLIP_TYPES = ["team", "agent", "skill", "routine", "bundle"] as const;
|
||||
export type ClipType = (typeof CLIP_TYPES)[number];
|
||||
|
||||
export const CLIP_VISIBILITIES = ["private_share", "unlisted", "public"] as const;
|
||||
export type ClipVisibility = (typeof CLIP_VISIBILITIES)[number];
|
||||
|
||||
export const CLIP_STATUSES = ["draft", "pending_review", "published", "delisted", "archived"] as const;
|
||||
export type ClipStatus = (typeof CLIP_STATUSES)[number];
|
||||
|
||||
export const CLIP_MODERATION_STATES = ["normal", "under_review", "warning", "limited", "blocked", "delisted"] as const;
|
||||
export type ClipModerationState = (typeof CLIP_MODERATION_STATES)[number];
|
||||
|
||||
export const CLIP_SECURITY_REVIEW_STATES = ["unreviewed", "automated_checked", "security_reviewed", "blocked"] as const;
|
||||
export type ClipSecurityReviewState = (typeof CLIP_SECURITY_REVIEW_STATES)[number];
|
||||
|
||||
export const CLIP_VERIFICATION_STATES = ["not_run", "passed", "failed", "stale"] as const;
|
||||
export type ClipVerificationState = (typeof CLIP_VERIFICATION_STATES)[number];
|
||||
|
||||
export const CLIP_DEPENDENCY_TYPES = ["adapter", "plugin", "skill", "secret", "permission", "runtime", "workspace"] as const;
|
||||
export type ClipDependencyType = (typeof CLIP_DEPENDENCY_TYPES)[number];
|
||||
|
||||
export const CLIP_DEPENDENCY_REQUIREMENTS = ["required", "optional"] as const;
|
||||
export type ClipDependencyRequirement = (typeof CLIP_DEPENDENCY_REQUIREMENTS)[number];
|
||||
|
||||
export const CLIP_COMMENT_CATEGORIES = [
|
||||
"question",
|
||||
"bug",
|
||||
"integration_help",
|
||||
"use_case_report",
|
||||
"security_concern",
|
||||
"maintainer_note",
|
||||
] as const;
|
||||
export type ClipCommentCategory = (typeof CLIP_COMMENT_CATEGORIES)[number];
|
||||
|
||||
export const CLIP_VOTE_VALUES = ["up", "down"] as const;
|
||||
export type ClipVoteValue = (typeof CLIP_VOTE_VALUES)[number];
|
||||
|
||||
export const CLIP_REPORT_REASONS = [
|
||||
"malicious_instructions",
|
||||
"credential_theft",
|
||||
"unsafe_automation",
|
||||
"spam",
|
||||
"copyright",
|
||||
"misleading_metadata",
|
||||
"broken_install",
|
||||
"impersonation",
|
||||
"other",
|
||||
] as const;
|
||||
export type ClipReportReason = (typeof CLIP_REPORT_REASONS)[number];
|
||||
|
||||
export const CLIP_IMPORT_STATUSES = [
|
||||
"previewed",
|
||||
"applied",
|
||||
"failed",
|
||||
"rolled_back",
|
||||
"update_available",
|
||||
"superseded",
|
||||
] as const;
|
||||
export type ClipImportStatus = (typeof CLIP_IMPORT_STATUSES)[number];
|
||||
|
||||
export interface ClipCreatorProfile {
|
||||
id: string;
|
||||
companyId: string;
|
||||
handle: string;
|
||||
displayName: string;
|
||||
bio: string | null;
|
||||
avatarUrl: string | null;
|
||||
websiteUrl: string | null;
|
||||
verificationState: string;
|
||||
reputationSummary: Record<string, unknown>;
|
||||
createdAt: Date | string;
|
||||
updatedAt: Date | string;
|
||||
}
|
||||
|
||||
export interface ClipDependency {
|
||||
id: string;
|
||||
clipId: string;
|
||||
revisionId: string;
|
||||
type: ClipDependencyType;
|
||||
key: string;
|
||||
displayName: string | null;
|
||||
required: ClipDependencyRequirement;
|
||||
metadata: Record<string, unknown>;
|
||||
createdAt: Date | string;
|
||||
}
|
||||
|
||||
export interface ClipRevision {
|
||||
id: string;
|
||||
clipId: string;
|
||||
revisionNumber: number;
|
||||
manifestVersion: string;
|
||||
manifestChecksum: string;
|
||||
artifactChecksum: string;
|
||||
manifestPayload: Record<string, unknown>;
|
||||
artifactRef: Record<string, unknown> | null;
|
||||
dependencyGraph: Record<string, unknown>;
|
||||
permissions: Record<string, unknown>[];
|
||||
secretsSchema: Record<string, unknown>[];
|
||||
budgetEstimate: Record<string, unknown> | null;
|
||||
redactionReport: Record<string, unknown>;
|
||||
dangerousCapabilities: string[];
|
||||
securityReviewState: ClipSecurityReviewState;
|
||||
verificationState: ClipVerificationState;
|
||||
compatibility: Record<string, unknown>;
|
||||
changeSummary: string | null;
|
||||
breakingChanges: string | null;
|
||||
migrationNotes: string | null;
|
||||
createdAt: Date | string;
|
||||
}
|
||||
|
||||
export interface PublicClip {
|
||||
id: string;
|
||||
slug: string;
|
||||
type: ClipType;
|
||||
title: string;
|
||||
summary: string;
|
||||
description: string | null;
|
||||
visibility: ClipVisibility;
|
||||
status: ClipStatus;
|
||||
moderationState: ClipModerationState;
|
||||
currentRevisionId: string | null;
|
||||
latestApprovedRevisionId: string | null;
|
||||
creatorProfileId: string;
|
||||
creator: Pick<ClipCreatorProfile, "id" | "handle" | "displayName" | "avatarUrl" | "verificationState" | "reputationSummary">;
|
||||
tags: string[];
|
||||
categories: string[];
|
||||
useCases: string[];
|
||||
requiredProviders: string[];
|
||||
compatibility: Record<string, unknown>;
|
||||
metrics: {
|
||||
importCount: number;
|
||||
successfulFirstRunCount: number;
|
||||
voteScore: number;
|
||||
upvoteCount: number;
|
||||
downvoteCount: number;
|
||||
commentCount: number;
|
||||
showcaseCount: number;
|
||||
reportCount: number;
|
||||
};
|
||||
currentRevision?: ClipRevision | null;
|
||||
dependencies?: ClipDependency[];
|
||||
createdAt: Date | string;
|
||||
updatedAt: Date | string;
|
||||
}
|
||||
@@ -1,4 +1,34 @@
|
||||
export type { Company } from "./company.js";
|
||||
export {
|
||||
CLIP_TYPES,
|
||||
CLIP_VISIBILITIES,
|
||||
CLIP_STATUSES,
|
||||
CLIP_MODERATION_STATES,
|
||||
CLIP_SECURITY_REVIEW_STATES,
|
||||
CLIP_VERIFICATION_STATES,
|
||||
CLIP_DEPENDENCY_TYPES,
|
||||
CLIP_DEPENDENCY_REQUIREMENTS,
|
||||
CLIP_COMMENT_CATEGORIES,
|
||||
CLIP_VOTE_VALUES,
|
||||
CLIP_REPORT_REASONS,
|
||||
CLIP_IMPORT_STATUSES,
|
||||
type ClipType,
|
||||
type ClipVisibility,
|
||||
type ClipStatus,
|
||||
type ClipModerationState,
|
||||
type ClipSecurityReviewState,
|
||||
type ClipVerificationState,
|
||||
type ClipDependencyType,
|
||||
type ClipDependencyRequirement,
|
||||
type ClipCommentCategory,
|
||||
type ClipVoteValue,
|
||||
type ClipReportReason,
|
||||
type ClipImportStatus,
|
||||
type ClipCreatorProfile,
|
||||
type ClipDependency,
|
||||
type ClipRevision,
|
||||
type PublicClip,
|
||||
} from "./clip.js";
|
||||
export type {
|
||||
Environment,
|
||||
EnvironmentLease,
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
import { z } from "zod";
|
||||
import {
|
||||
CLIP_COMMENT_CATEGORIES,
|
||||
CLIP_DEPENDENCY_REQUIREMENTS,
|
||||
CLIP_DEPENDENCY_TYPES,
|
||||
CLIP_IMPORT_STATUSES,
|
||||
CLIP_MODERATION_STATES,
|
||||
CLIP_REPORT_REASONS,
|
||||
CLIP_SECURITY_REVIEW_STATES,
|
||||
CLIP_STATUSES,
|
||||
CLIP_TYPES,
|
||||
CLIP_VERIFICATION_STATES,
|
||||
CLIP_VISIBILITIES,
|
||||
CLIP_VOTE_VALUES,
|
||||
} from "../types/clip.js";
|
||||
|
||||
const slugSchema = z
|
||||
.string()
|
||||
.min(3)
|
||||
.max(96)
|
||||
.regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/);
|
||||
|
||||
const publicTextSchema = z.string().trim().min(1).max(12_000);
|
||||
const shortPublicTextSchema = z.string().trim().min(1).max(280);
|
||||
const tagArraySchema = z.array(z.string().trim().min(1).max(48)).max(24).default([]);
|
||||
const recordSchema = z.record(z.string(), z.unknown());
|
||||
const optionalRecordSchema = recordSchema.nullable().optional();
|
||||
|
||||
export const clipTypeSchema = z.enum(CLIP_TYPES);
|
||||
export const clipVisibilitySchema = z.enum(CLIP_VISIBILITIES);
|
||||
export const clipStatusSchema = z.enum(CLIP_STATUSES);
|
||||
export const clipModerationStateSchema = z.enum(CLIP_MODERATION_STATES);
|
||||
export const clipSecurityReviewStateSchema = z.enum(CLIP_SECURITY_REVIEW_STATES);
|
||||
export const clipVerificationStateSchema = z.enum(CLIP_VERIFICATION_STATES);
|
||||
export const clipDependencyTypeSchema = z.enum(CLIP_DEPENDENCY_TYPES);
|
||||
export const clipVoteValueSchema = z.enum(CLIP_VOTE_VALUES);
|
||||
export const clipReportReasonSchema = z.enum(CLIP_REPORT_REASONS);
|
||||
|
||||
export const createClipCreatorProfileSchema = z.object({
|
||||
handle: slugSchema,
|
||||
displayName: z.string().trim().min(1).max(96),
|
||||
bio: z.string().trim().max(1_000).nullable().optional(),
|
||||
avatarUrl: z.string().url().nullable().optional(),
|
||||
websiteUrl: z.string().url().nullable().optional(),
|
||||
verificationState: z.string().trim().min(1).max(48).optional(),
|
||||
reputationSummary: recordSchema.optional(),
|
||||
});
|
||||
|
||||
export type CreateClipCreatorProfile = z.infer<typeof createClipCreatorProfileSchema>;
|
||||
|
||||
export const clipDependencyInputSchema = z.object({
|
||||
type: clipDependencyTypeSchema,
|
||||
key: z.string().trim().min(1).max(160),
|
||||
displayName: z.string().trim().min(1).max(160).nullable().optional(),
|
||||
required: z.enum(CLIP_DEPENDENCY_REQUIREMENTS).optional(),
|
||||
metadata: recordSchema.optional(),
|
||||
});
|
||||
|
||||
export type ClipDependencyInput = z.infer<typeof clipDependencyInputSchema>;
|
||||
|
||||
export const clipRevisionInputSchema = z.object({
|
||||
manifestVersion: z.string().trim().min(1).max(64).optional(),
|
||||
manifestChecksum: z.string().trim().min(8).max(160),
|
||||
artifactChecksum: z.string().trim().min(8).max(160),
|
||||
manifestPayload: recordSchema,
|
||||
artifactRef: optionalRecordSchema,
|
||||
dependencyGraph: recordSchema.optional(),
|
||||
dependencies: z.array(clipDependencyInputSchema).max(200).optional(),
|
||||
permissions: z.array(recordSchema).max(200).optional(),
|
||||
secretsSchema: z.array(recordSchema).max(200).optional(),
|
||||
budgetEstimate: optionalRecordSchema,
|
||||
redactionReport: recordSchema.optional(),
|
||||
dangerousCapabilities: z.array(z.string().trim().min(1).max(80)).max(100).optional(),
|
||||
securityReviewState: clipSecurityReviewStateSchema.optional(),
|
||||
verificationState: clipVerificationStateSchema.optional(),
|
||||
compatibility: recordSchema.optional(),
|
||||
changeSummary: z.string().trim().max(4_000).nullable().optional(),
|
||||
breakingChanges: z.string().trim().max(4_000).nullable().optional(),
|
||||
migrationNotes: z.string().trim().max(4_000).nullable().optional(),
|
||||
});
|
||||
|
||||
export type ClipRevisionInput = z.infer<typeof clipRevisionInputSchema>;
|
||||
|
||||
export const publishClipSchema = z.object({
|
||||
creatorProfileId: z.string().uuid().optional(),
|
||||
creatorProfile: createClipCreatorProfileSchema.optional(),
|
||||
slug: slugSchema,
|
||||
type: clipTypeSchema,
|
||||
title: z.string().trim().min(1).max(160),
|
||||
summary: shortPublicTextSchema,
|
||||
description: publicTextSchema.nullable().optional(),
|
||||
visibility: clipVisibilitySchema.optional(),
|
||||
status: clipStatusSchema.optional(),
|
||||
tags: tagArraySchema.optional(),
|
||||
categories: tagArraySchema.optional(),
|
||||
useCases: tagArraySchema.optional(),
|
||||
requiredProviders: tagArraySchema.optional(),
|
||||
compatibility: recordSchema.optional(),
|
||||
sourceKind: z.string().trim().min(1).max(80).nullable().optional(),
|
||||
sourceObjectType: z.string().trim().min(1).max(80).nullable().optional(),
|
||||
sourceObjectId: z.string().trim().min(1).max(160).nullable().optional(),
|
||||
revision: clipRevisionInputSchema,
|
||||
}).refine((value) => value.creatorProfileId || value.creatorProfile, {
|
||||
message: "creatorProfileId or creatorProfile is required",
|
||||
path: ["creatorProfileId"],
|
||||
});
|
||||
|
||||
export type PublishClip = z.infer<typeof publishClipSchema>;
|
||||
|
||||
export const createClipRevisionSchema = clipRevisionInputSchema;
|
||||
export type CreateClipRevision = z.infer<typeof createClipRevisionSchema>;
|
||||
|
||||
export const updateClipSchema = z
|
||||
.object({
|
||||
title: z.string().trim().min(1).max(160).optional(),
|
||||
summary: shortPublicTextSchema.optional(),
|
||||
description: publicTextSchema.nullable().optional(),
|
||||
visibility: clipVisibilitySchema.optional(),
|
||||
status: clipStatusSchema.optional(),
|
||||
moderationState: clipModerationStateSchema.optional(),
|
||||
latestApprovedRevisionId: z.string().uuid().nullable().optional(),
|
||||
tags: tagArraySchema.optional(),
|
||||
categories: tagArraySchema.optional(),
|
||||
useCases: tagArraySchema.optional(),
|
||||
requiredProviders: tagArraySchema.optional(),
|
||||
compatibility: recordSchema.optional(),
|
||||
moderationReason: z.string().trim().max(1_000).optional(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
export type UpdateClip = z.infer<typeof updateClipSchema>;
|
||||
|
||||
export const createClipVoteSchema = z.object({
|
||||
revisionNumber: z.number().int().positive().optional(),
|
||||
vote: clipVoteValueSchema,
|
||||
reason: z.string().trim().max(400).nullable().optional(),
|
||||
metadata: recordSchema.optional(),
|
||||
});
|
||||
|
||||
export type CreateClipVote = z.infer<typeof createClipVoteSchema>;
|
||||
|
||||
export const createClipReportSchema = z.object({
|
||||
revisionNumber: z.number().int().positive().optional(),
|
||||
reason: clipReportReasonSchema,
|
||||
details: z.string().trim().max(4_000).nullable().optional(),
|
||||
metadata: recordSchema.optional(),
|
||||
});
|
||||
|
||||
export type CreateClipReport = z.infer<typeof createClipReportSchema>;
|
||||
|
||||
export const createClipCommentSchema = z.object({
|
||||
revisionNumber: z.number().int().positive().optional(),
|
||||
scope: z.enum(["clip", "revision"]).optional(),
|
||||
category: z.enum(CLIP_COMMENT_CATEGORIES).optional(),
|
||||
body: publicTextSchema,
|
||||
});
|
||||
|
||||
export type CreateClipComment = z.infer<typeof createClipCommentSchema>;
|
||||
|
||||
export const createClipShowcaseSchema = z.object({
|
||||
revisionNumber: z.number().int().positive().optional(),
|
||||
type: z.string().trim().min(1).max(80).optional(),
|
||||
title: z.string().trim().min(1).max(160),
|
||||
body: publicTextSchema.nullable().optional(),
|
||||
mediaRefs: z.array(recordSchema).max(12).optional(),
|
||||
validationState: clipVerificationStateSchema.optional(),
|
||||
});
|
||||
|
||||
export type CreateClipShowcase = z.infer<typeof createClipShowcaseSchema>;
|
||||
|
||||
export const createClipImportTelemetrySchema = z.object({
|
||||
revisionNumber: z.number().int().positive().optional(),
|
||||
destinationCompanyId: z.string().uuid().optional(),
|
||||
status: z.enum(CLIP_IMPORT_STATUSES).optional(),
|
||||
sourceUrl: z.string().url().nullable().optional(),
|
||||
revisionUrl: z.string().url().nullable().optional(),
|
||||
metadata: recordSchema.optional(),
|
||||
});
|
||||
|
||||
export type CreateClipImportTelemetry = z.infer<typeof createClipImportTelemetrySchema>;
|
||||
@@ -26,6 +26,39 @@ export {
|
||||
type UpdateCompany,
|
||||
type UpdateCompanyBranding,
|
||||
} from "./company.js";
|
||||
export {
|
||||
clipTypeSchema,
|
||||
clipVisibilitySchema,
|
||||
clipStatusSchema,
|
||||
clipModerationStateSchema,
|
||||
clipSecurityReviewStateSchema,
|
||||
clipVerificationStateSchema,
|
||||
clipDependencyTypeSchema,
|
||||
clipVoteValueSchema,
|
||||
clipReportReasonSchema,
|
||||
createClipCreatorProfileSchema,
|
||||
clipDependencyInputSchema,
|
||||
clipRevisionInputSchema,
|
||||
publishClipSchema,
|
||||
createClipRevisionSchema,
|
||||
updateClipSchema,
|
||||
createClipVoteSchema,
|
||||
createClipReportSchema,
|
||||
createClipCommentSchema,
|
||||
createClipShowcaseSchema,
|
||||
createClipImportTelemetrySchema,
|
||||
type CreateClipCreatorProfile,
|
||||
type ClipDependencyInput,
|
||||
type ClipRevisionInput,
|
||||
type PublishClip,
|
||||
type CreateClipRevision,
|
||||
type UpdateClip,
|
||||
type CreateClipVote,
|
||||
type CreateClipReport,
|
||||
type CreateClipComment,
|
||||
type CreateClipShowcase,
|
||||
type CreateClipImportTelemetry,
|
||||
} from "./clip.js";
|
||||
export {
|
||||
environmentDriverSchema,
|
||||
environmentStatusSchema,
|
||||
|
||||
@@ -0,0 +1,186 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest";
|
||||
import {
|
||||
clipComments,
|
||||
clipCreatorProfiles,
|
||||
clipDependencies,
|
||||
clipFlags,
|
||||
clipImportedObjects,
|
||||
clipImports,
|
||||
clipModerationEvents,
|
||||
clipPublicMetricEvents,
|
||||
clipRankingSnapshots,
|
||||
clipRevisions,
|
||||
clipShowcasePosts,
|
||||
clipVotes,
|
||||
clips,
|
||||
companies,
|
||||
createDb,
|
||||
} from "@paperclipai/db";
|
||||
import {
|
||||
getEmbeddedPostgresTestSupport,
|
||||
startEmbeddedPostgresTestDatabase,
|
||||
} from "./helpers/embedded-postgres.js";
|
||||
import { clipService } from "../services/clips.ts";
|
||||
|
||||
const embeddedPostgresSupport = await getEmbeddedPostgresTestSupport();
|
||||
const describeEmbeddedPostgres = embeddedPostgresSupport.supported ? describe : describe.skip;
|
||||
|
||||
if (!embeddedPostgresSupport.supported) {
|
||||
console.warn(
|
||||
`Skipping embedded Postgres clips service tests on this host: ${embeddedPostgresSupport.reason ?? "unsupported environment"}`,
|
||||
);
|
||||
}
|
||||
|
||||
describeEmbeddedPostgres("clipService", () => {
|
||||
let db!: ReturnType<typeof createDb>;
|
||||
let svc!: ReturnType<typeof clipService>;
|
||||
let tempDb: Awaited<ReturnType<typeof startEmbeddedPostgresTestDatabase>> | null = null;
|
||||
|
||||
beforeAll(async () => {
|
||||
tempDb = await startEmbeddedPostgresTestDatabase("paperclip-clips-service-");
|
||||
db = createDb(tempDb.connectionString);
|
||||
svc = clipService(db);
|
||||
}, 20_000);
|
||||
|
||||
afterEach(async () => {
|
||||
await db.delete(clipPublicMetricEvents);
|
||||
await db.delete(clipImportedObjects);
|
||||
await db.delete(clipImports);
|
||||
await db.delete(clipRankingSnapshots);
|
||||
await db.delete(clipModerationEvents);
|
||||
await db.delete(clipShowcasePosts);
|
||||
await db.delete(clipVotes);
|
||||
await db.delete(clipComments);
|
||||
await db.delete(clipFlags);
|
||||
await db.delete(clipDependencies);
|
||||
await db.delete(clipRevisions);
|
||||
await db.delete(clips);
|
||||
await db.delete(clipCreatorProfiles);
|
||||
await db.delete(companies);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await tempDb?.cleanup();
|
||||
});
|
||||
|
||||
async function seedCompany() {
|
||||
const companyId = randomUUID();
|
||||
await db.insert(companies).values({
|
||||
id: companyId,
|
||||
name: "Paperclip",
|
||||
issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`,
|
||||
requireBoardApprovalForNewAgents: false,
|
||||
});
|
||||
return companyId;
|
||||
}
|
||||
|
||||
async function publishFixture(companyId: string) {
|
||||
return svc.publish(companyId, {
|
||||
creatorProfile: {
|
||||
handle: `creator-${companyId.slice(0, 8)}`,
|
||||
displayName: "Creator Labs",
|
||||
},
|
||||
slug: `support-triage-${companyId.slice(0, 8)}`,
|
||||
type: "agent",
|
||||
title: "Support Triage Agent",
|
||||
summary: "Routes support tickets to the right specialist.",
|
||||
description: "A sanitized public description.",
|
||||
visibility: "public",
|
||||
status: "published",
|
||||
tags: ["support"],
|
||||
requiredProviders: ["github"],
|
||||
sourceKind: "paperclip_company_object",
|
||||
sourceObjectType: "agent",
|
||||
sourceObjectId: "private-agent-id",
|
||||
revision: {
|
||||
manifestVersion: "paperclip.clip/v1",
|
||||
manifestChecksum: "sha256:manifest-v1",
|
||||
artifactChecksum: "sha256:artifact-v1",
|
||||
manifestPayload: {
|
||||
schema: "paperclip.clip/v1",
|
||||
privateSourceCompanyId: "must-not-come-from-public-api",
|
||||
clip: { slug: "support-triage" },
|
||||
},
|
||||
dependencyGraph: { adapters: [{ type: "codex_local" }] },
|
||||
dependencies: [
|
||||
{ type: "adapter", key: "codex_local", required: "optional" },
|
||||
{ type: "permission", key: "github.issues.write" },
|
||||
],
|
||||
redactionReport: { summary: { omitted: 3 } },
|
||||
dangerousCapabilities: ["github"],
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
it("publishes sanitized public detail without exposing private source identifiers", async () => {
|
||||
const companyId = await seedCompany();
|
||||
const { clip, revision } = await publishFixture(companyId);
|
||||
|
||||
const detail = await svc.getPublicDetail(clip.slug);
|
||||
|
||||
expect(detail).toMatchObject({
|
||||
id: clip.id,
|
||||
slug: clip.slug,
|
||||
title: "Support Triage Agent",
|
||||
currentRevision: {
|
||||
id: revision.id,
|
||||
revisionNumber: 1,
|
||||
manifestChecksum: "sha256:manifest-v1",
|
||||
},
|
||||
});
|
||||
expect(detail?.dependencies?.map((dependency) => dependency.key).sort()).toEqual([
|
||||
"codex_local",
|
||||
"github.issues.write",
|
||||
]);
|
||||
expect(detail).not.toHaveProperty("sourceCompanyId");
|
||||
expect(detail).not.toHaveProperty("sourceObjectId");
|
||||
});
|
||||
|
||||
it("stores revisions append-only and serves exact revision manifests", async () => {
|
||||
const companyId = await seedCompany();
|
||||
const { clip, revision } = await publishFixture(companyId);
|
||||
|
||||
const update = await svc.createRevision(clip.id, {
|
||||
manifestChecksum: "sha256:manifest-v2",
|
||||
artifactChecksum: "sha256:artifact-v2",
|
||||
manifestPayload: { schema: "paperclip.clip/v1", revision: 2 },
|
||||
changeSummary: "Adds a safer default.",
|
||||
});
|
||||
|
||||
expect(update.revision.revisionNumber).toBe(2);
|
||||
const firstRevision = await svc.getPublicRevision(clip.slug, 1);
|
||||
const secondRevision = await svc.getPublicDetail(clip.slug);
|
||||
|
||||
expect(firstRevision?.currentRevision?.manifestChecksum).toBe("sha256:manifest-v1");
|
||||
expect(secondRevision?.currentRevision?.manifestChecksum).toBe("sha256:manifest-v2");
|
||||
await expect(
|
||||
db.update(clipRevisions).set({ changeSummary: "mutated" }).where(eq(clipRevisions.id, revision.id)),
|
||||
).rejects.toThrow(/Failed query/);
|
||||
const [unchanged] = await db.select().from(clipRevisions).where(eq(clipRevisions.id, revision.id));
|
||||
expect(unchanged?.changeSummary).toBeNull();
|
||||
});
|
||||
|
||||
it("enforces public moderation state on reads while keeping report audit data", async () => {
|
||||
const companyId = await seedCompany();
|
||||
const { clip } = await publishFixture(companyId);
|
||||
|
||||
await svc.createReport(clip.slug, { reason: "unsafe_automation", details: "It asks for broad shell access." }, {
|
||||
actorType: "anonymous",
|
||||
actorId: "anonymous",
|
||||
});
|
||||
const underReview = await svc.getPublicDetail(clip.slug);
|
||||
expect(underReview?.moderationState).toBe("under_review");
|
||||
|
||||
await svc.updateClip(clip.id, { moderationState: "blocked", moderationReason: "Confirmed malicious behavior." }, {
|
||||
actorType: "user",
|
||||
actorId: "moderator",
|
||||
});
|
||||
|
||||
expect(await svc.getPublicDetail(clip.slug)).toBeNull();
|
||||
const reports = await db.select().from(clipFlags).where(eq(clipFlags.clipId, clip.id));
|
||||
expect(reports).toHaveLength(1);
|
||||
expect(reports[0]?.reason).toBe("unsafe_automation");
|
||||
});
|
||||
});
|
||||
@@ -23,6 +23,7 @@ import { goalRoutes } from "./routes/goals.js";
|
||||
import { approvalRoutes } from "./routes/approvals.js";
|
||||
import { secretRoutes } from "./routes/secrets.js";
|
||||
import { costRoutes } from "./routes/costs.js";
|
||||
import { clipRoutes } from "./routes/clips.js";
|
||||
import { activityRoutes } from "./routes/activity.js";
|
||||
import { dashboardRoutes } from "./routes/dashboard.js";
|
||||
import { userProfileRoutes } from "./routes/user-profiles.js";
|
||||
@@ -204,6 +205,7 @@ export async function createApp(
|
||||
api.use(approvalRoutes(db, { pluginWorkerManager: workerManager }));
|
||||
api.use(secretRoutes(db));
|
||||
api.use(costRoutes(db, { pluginWorkerManager: workerManager }));
|
||||
api.use(clipRoutes(db));
|
||||
api.use(activityRoutes(db));
|
||||
api.use(dashboardRoutes(db));
|
||||
api.use(userProfileRoutes(db));
|
||||
|
||||
@@ -0,0 +1,342 @@
|
||||
import { Router, type Request } from "express";
|
||||
import type { Db } from "@paperclipai/db";
|
||||
import {
|
||||
createClipCommentSchema,
|
||||
createClipCreatorProfileSchema,
|
||||
createClipImportTelemetrySchema,
|
||||
createClipReportSchema,
|
||||
createClipRevisionSchema,
|
||||
createClipShowcaseSchema,
|
||||
createClipVoteSchema,
|
||||
publishClipSchema,
|
||||
updateClipSchema,
|
||||
} from "@paperclipai/shared";
|
||||
import { validate } from "../middleware/validate.js";
|
||||
import { forbidden } from "../errors.js";
|
||||
import { assertAuthenticated, assertCompanyAccess, getActorInfo } from "./authz.js";
|
||||
import { clipService } from "../services/clips.js";
|
||||
import { logActivity } from "../services/index.js";
|
||||
|
||||
const CLIP_RATE_LIMIT_WINDOW_MS = 60_000;
|
||||
const CLIP_RATE_LIMIT_MAX_REQUESTS = 60;
|
||||
const clipRateLimitHits = new Map<string, number[]>();
|
||||
|
||||
function actorForClip(req: Request) {
|
||||
if (req.actor.type === "agent") {
|
||||
return {
|
||||
actorType: "agent" as const,
|
||||
actorId: req.actor.agentId ?? "unknown-agent",
|
||||
agentId: req.actor.agentId ?? null,
|
||||
userId: null,
|
||||
};
|
||||
}
|
||||
if (req.actor.type === "board") {
|
||||
return {
|
||||
actorType: "user" as const,
|
||||
actorId: req.actor.userId ?? "board",
|
||||
agentId: null,
|
||||
userId: req.actor.userId ?? null,
|
||||
};
|
||||
}
|
||||
return {
|
||||
actorType: "anonymous" as const,
|
||||
actorId: "anonymous",
|
||||
agentId: null,
|
||||
userId: null,
|
||||
};
|
||||
}
|
||||
|
||||
function requireAuthenticatedClipActor(req: Request) {
|
||||
assertAuthenticated(req);
|
||||
const actor = actorForClip(req);
|
||||
if (actor.actorType === "anonymous") throw forbidden("Authenticated actor required");
|
||||
return actor;
|
||||
}
|
||||
|
||||
function consumeClipRateLimit(req: Request, res: { setHeader(name: string, value: string): void; status(code: number): { json(value: unknown): void } }, action: string) {
|
||||
const now = Date.now();
|
||||
const cutoff = now - CLIP_RATE_LIMIT_WINDOW_MS;
|
||||
const actor = actorForClip(req);
|
||||
const ip = req.ip || req.socket.remoteAddress || "unknown-ip";
|
||||
const key = `${action}:${actor.actorType}:${actor.actorId}:${ip}`;
|
||||
const recent = (clipRateLimitHits.get(key) ?? []).filter((hit) => hit > cutoff);
|
||||
if (recent.length >= CLIP_RATE_LIMIT_MAX_REQUESTS) {
|
||||
const retryAfterSeconds = Math.max(1, Math.ceil(((recent[0] ?? now) + CLIP_RATE_LIMIT_WINDOW_MS - now) / 1000));
|
||||
clipRateLimitHits.set(key, recent);
|
||||
res.setHeader("Retry-After", String(retryAfterSeconds));
|
||||
res.status(429).json({ error: "Clip rate limit exceeded", retryAfterSeconds });
|
||||
return false;
|
||||
}
|
||||
recent.push(now);
|
||||
clipRateLimitHits.set(key, recent);
|
||||
return true;
|
||||
}
|
||||
|
||||
export function clipRoutes(db: Db) {
|
||||
const router = Router();
|
||||
const svc = clipService(db);
|
||||
|
||||
router.get("/public/clips", async (req, res) => {
|
||||
const q = typeof req.query.q === "string" ? req.query.q : null;
|
||||
const type = typeof req.query.type === "string" ? req.query.type : null;
|
||||
const tag = typeof req.query.tag === "string" ? req.query.tag : null;
|
||||
const limit = typeof req.query.limit === "string" ? Number(req.query.limit) : undefined;
|
||||
const offset = typeof req.query.offset === "string" ? Number(req.query.offset) : undefined;
|
||||
res.json(await svc.listPublic({ q, type, tag, limit, offset }));
|
||||
});
|
||||
|
||||
router.get("/public/clips/:slug", async (req, res) => {
|
||||
const detail = await svc.getPublicDetail(req.params.slug as string);
|
||||
if (!detail) {
|
||||
res.status(404).json({ error: "Clip not found" });
|
||||
return;
|
||||
}
|
||||
res.json(detail);
|
||||
});
|
||||
|
||||
router.get("/public/clips/:slug/revisions/:revisionNumber", async (req, res) => {
|
||||
const revisionNumber = Number(req.params.revisionNumber);
|
||||
const detail = Number.isInteger(revisionNumber) && revisionNumber > 0
|
||||
? await svc.getPublicRevision(req.params.slug as string, revisionNumber)
|
||||
: null;
|
||||
if (!detail) {
|
||||
res.status(404).json({ error: "Clip revision not found" });
|
||||
return;
|
||||
}
|
||||
res.json(detail);
|
||||
});
|
||||
|
||||
router.get("/public/clips/:slug/manifest", async (req, res) => {
|
||||
const detail = await svc.getPublicDetail(req.params.slug as string);
|
||||
if (!detail?.currentRevision) {
|
||||
res.status(404).json({ error: "Clip manifest not found" });
|
||||
return;
|
||||
}
|
||||
res.json(detail.currentRevision.manifestPayload);
|
||||
});
|
||||
|
||||
router.get("/public/clips/:slug/revisions/:revisionNumber/manifest", async (req, res) => {
|
||||
const revisionNumber = Number(req.params.revisionNumber);
|
||||
const detail = Number.isInteger(revisionNumber) && revisionNumber > 0
|
||||
? await svc.getPublicRevision(req.params.slug as string, revisionNumber)
|
||||
: null;
|
||||
if (!detail?.currentRevision) {
|
||||
res.status(404).json({ error: "Clip manifest not found" });
|
||||
return;
|
||||
}
|
||||
res.json(detail.currentRevision.manifestPayload);
|
||||
});
|
||||
|
||||
router.get("/public/creators/:handle", async (req, res) => {
|
||||
const profile = await svc.getCreatorPublicProfile(req.params.handle as string);
|
||||
if (!profile) {
|
||||
res.status(404).json({ error: "Creator profile not found" });
|
||||
return;
|
||||
}
|
||||
res.json(profile);
|
||||
});
|
||||
|
||||
router.post("/companies/:companyId/clips/profiles", validate(createClipCreatorProfileSchema), async (req, res) => {
|
||||
const companyId = req.params.companyId as string;
|
||||
assertCompanyAccess(req, companyId);
|
||||
const profile = await svc.createCreatorProfile(companyId, req.body);
|
||||
const actor = getActorInfo(req);
|
||||
await logActivity(db, {
|
||||
companyId,
|
||||
actorType: actor.actorType,
|
||||
actorId: actor.actorId,
|
||||
agentId: actor.agentId,
|
||||
runId: actor.runId,
|
||||
action: "clip.profile_created",
|
||||
entityType: "clip_creator_profile",
|
||||
entityId: profile.id,
|
||||
details: { handle: profile.handle },
|
||||
});
|
||||
res.status(201).json(profile);
|
||||
});
|
||||
|
||||
router.post("/companies/:companyId/clips/publish", validate(publishClipSchema), async (req, res) => {
|
||||
const companyId = req.params.companyId as string;
|
||||
assertCompanyAccess(req, companyId);
|
||||
if (!consumeClipRateLimit(req, res, "publish")) return;
|
||||
const result = await svc.publish(companyId, req.body);
|
||||
const actor = getActorInfo(req);
|
||||
await logActivity(db, {
|
||||
companyId,
|
||||
actorType: actor.actorType,
|
||||
actorId: actor.actorId,
|
||||
agentId: actor.agentId,
|
||||
runId: actor.runId,
|
||||
action: "clip.published",
|
||||
entityType: "clip",
|
||||
entityId: result.clip.id,
|
||||
details: {
|
||||
slug: result.clip.slug,
|
||||
revisionId: result.revision.id,
|
||||
revisionNumber: result.revision.revisionNumber,
|
||||
visibility: result.clip.visibility,
|
||||
status: result.clip.status,
|
||||
},
|
||||
});
|
||||
res.status(201).json(result);
|
||||
});
|
||||
|
||||
router.post("/clips/:clipId/revisions", validate(createClipRevisionSchema), async (req, res) => {
|
||||
const clip = await svc.getClipById(req.params.clipId as string);
|
||||
if (!clip) {
|
||||
res.status(404).json({ error: "Clip not found" });
|
||||
return;
|
||||
}
|
||||
assertCompanyAccess(req, clip.sourceCompanyId);
|
||||
if (!consumeClipRateLimit(req, res, "update")) return;
|
||||
const result = await svc.createRevision(clip.id, req.body);
|
||||
const actor = getActorInfo(req);
|
||||
await logActivity(db, {
|
||||
companyId: clip.sourceCompanyId,
|
||||
actorType: actor.actorType,
|
||||
actorId: actor.actorId,
|
||||
agentId: actor.agentId,
|
||||
runId: actor.runId,
|
||||
action: "clip.revision_published",
|
||||
entityType: "clip_revision",
|
||||
entityId: result.revision.id,
|
||||
details: {
|
||||
clipId: clip.id,
|
||||
slug: clip.slug,
|
||||
revisionNumber: result.revision.revisionNumber,
|
||||
},
|
||||
});
|
||||
res.status(201).json(result);
|
||||
});
|
||||
|
||||
router.patch("/clips/:clipId", validate(updateClipSchema), async (req, res) => {
|
||||
const clip = await svc.getClipById(req.params.clipId as string);
|
||||
if (!clip) {
|
||||
res.status(404).json({ error: "Clip not found" });
|
||||
return;
|
||||
}
|
||||
assertCompanyAccess(req, clip.sourceCompanyId);
|
||||
const actorInfo = getActorInfo(req);
|
||||
const updated = await svc.updateClip(clip.id, req.body, {
|
||||
actorType: actorInfo.actorType,
|
||||
actorId: actorInfo.actorId,
|
||||
agentId: actorInfo.agentId,
|
||||
userId: req.actor.type === "board" ? req.actor.userId ?? null : null,
|
||||
});
|
||||
await logActivity(db, {
|
||||
companyId: clip.sourceCompanyId,
|
||||
actorType: actorInfo.actorType,
|
||||
actorId: actorInfo.actorId,
|
||||
agentId: actorInfo.agentId,
|
||||
runId: actorInfo.runId,
|
||||
action: req.body.status === "delisted" || req.body.moderationState === "delisted"
|
||||
? "clip.delisted"
|
||||
: req.body.moderationState
|
||||
? "clip.moderated"
|
||||
: "clip.updated",
|
||||
entityType: "clip",
|
||||
entityId: clip.id,
|
||||
details: {
|
||||
slug: clip.slug,
|
||||
status: updated.status,
|
||||
moderationState: updated.moderationState,
|
||||
},
|
||||
});
|
||||
res.json(updated);
|
||||
});
|
||||
|
||||
router.post("/public/clips/:slug/votes", validate(createClipVoteSchema), async (req, res) => {
|
||||
const actor = requireAuthenticatedClipActor(req);
|
||||
if (!consumeClipRateLimit(req, res, "vote")) return;
|
||||
const result = await svc.createVote(req.params.slug as string, req.body, actor);
|
||||
await logActivity(db, {
|
||||
companyId: result.clip.sourceCompanyId,
|
||||
actorType: actor.actorType === "agent" ? "agent" : "user",
|
||||
actorId: actor.actorId,
|
||||
agentId: actor.agentId,
|
||||
action: "clip.vote_created",
|
||||
entityType: "clip",
|
||||
entityId: result.clip.id,
|
||||
details: {
|
||||
slug: result.clip.slug,
|
||||
revisionId: result.revision.id,
|
||||
revisionNumber: result.revision.revisionNumber,
|
||||
vote: req.body.vote,
|
||||
},
|
||||
});
|
||||
res.status(201).json({ ok: true, metrics: result.clip });
|
||||
});
|
||||
|
||||
router.post("/public/clips/:slug/report", validate(createClipReportSchema), async (req, res) => {
|
||||
const actor = actorForClip(req);
|
||||
if (!consumeClipRateLimit(req, res, "report")) return;
|
||||
const clip = await svc.createReport(req.params.slug as string, req.body, actor);
|
||||
await logActivity(db, {
|
||||
companyId: clip.sourceCompanyId,
|
||||
actorType: actor.actorType === "agent" ? "agent" : actor.actorType === "user" ? "user" : "system",
|
||||
actorId: actor.actorId,
|
||||
agentId: actor.agentId,
|
||||
action: "clip.report_created",
|
||||
entityType: "clip",
|
||||
entityId: clip.id,
|
||||
details: { slug: clip.slug, reason: req.body.reason, moderationState: clip.moderationState },
|
||||
});
|
||||
res.status(201).json({ ok: true });
|
||||
});
|
||||
|
||||
router.post("/public/clips/:slug/comments", validate(createClipCommentSchema), async (req, res) => {
|
||||
const actor = requireAuthenticatedClipActor(req);
|
||||
if (!consumeClipRateLimit(req, res, "comment")) return;
|
||||
const clip = await svc.createComment(req.params.slug as string, req.body, actor);
|
||||
await logActivity(db, {
|
||||
companyId: clip.sourceCompanyId,
|
||||
actorType: actor.actorType === "agent" ? "agent" : "user",
|
||||
actorId: actor.actorId,
|
||||
agentId: actor.agentId,
|
||||
action: "clip.comment_created",
|
||||
entityType: "clip",
|
||||
entityId: clip.id,
|
||||
details: { slug: clip.slug },
|
||||
});
|
||||
res.status(201).json({ ok: true });
|
||||
});
|
||||
|
||||
router.post("/public/clips/:slug/showcase", validate(createClipShowcaseSchema), async (req, res) => {
|
||||
const actor = requireAuthenticatedClipActor(req);
|
||||
if (!consumeClipRateLimit(req, res, "showcase")) return;
|
||||
const clip = await svc.createShowcase(req.params.slug as string, req.body, actor);
|
||||
await logActivity(db, {
|
||||
companyId: clip.sourceCompanyId,
|
||||
actorType: actor.actorType === "agent" ? "agent" : "user",
|
||||
actorId: actor.actorId,
|
||||
agentId: actor.agentId,
|
||||
action: "clip.showcase_created",
|
||||
entityType: "clip",
|
||||
entityId: clip.id,
|
||||
details: { slug: clip.slug, validationState: req.body.validationState },
|
||||
});
|
||||
res.status(201).json({ ok: true });
|
||||
});
|
||||
|
||||
router.post("/public/clips/:slug/import-telemetry", validate(createClipImportTelemetrySchema), async (req, res) => {
|
||||
if (req.body.destinationCompanyId) {
|
||||
assertCompanyAccess(req, req.body.destinationCompanyId);
|
||||
}
|
||||
if (!consumeClipRateLimit(req, res, "import")) return;
|
||||
const actor = actorForClip(req);
|
||||
const clip = await svc.recordImportTelemetry(req.params.slug as string, req.body, actor);
|
||||
await logActivity(db, {
|
||||
companyId: req.body.destinationCompanyId ?? clip.sourceCompanyId,
|
||||
actorType: actor.actorType === "agent" ? "agent" : actor.actorType === "user" ? "user" : "system",
|
||||
actorId: actor.actorId,
|
||||
agentId: actor.agentId,
|
||||
action: "clip.import_telemetry_created",
|
||||
entityType: "clip",
|
||||
entityId: clip.id,
|
||||
details: { slug: clip.slug, status: req.body.status ?? "previewed" },
|
||||
});
|
||||
res.status(201).json({ ok: true });
|
||||
});
|
||||
|
||||
return router;
|
||||
}
|
||||
@@ -10,6 +10,7 @@ export { goalRoutes } from "./goals.js";
|
||||
export { approvalRoutes } from "./approvals.js";
|
||||
export { secretRoutes } from "./secrets.js";
|
||||
export { costRoutes } from "./costs.js";
|
||||
export { clipRoutes } from "./clips.js";
|
||||
export { activityRoutes } from "./activity.js";
|
||||
export { dashboardRoutes } from "./dashboard.js";
|
||||
export { sidebarBadgeRoutes } from "./sidebar-badges.js";
|
||||
|
||||
@@ -0,0 +1,688 @@
|
||||
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;
|
||||
};
|
||||
|
||||
function first<T>(rows: T[]): T | null {
|
||||
return rows[0] ?? null;
|
||||
}
|
||||
|
||||
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 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;
|
||||
return first(await db.select().from(clipRevisions).where(eq(clipRevisions.id, revisionId)).limit(1));
|
||||
}
|
||||
|
||||
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;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
} = {}) {
|
||||
const conditions = [
|
||||
eq(clips.visibility, "public"),
|
||||
eq(clips.status, PUBLIC_STATUS),
|
||||
inArray(clips.moderationState, PUBLIC_BROWSE_MODERATION_STATES),
|
||||
];
|
||||
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) 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");
|
||||
return createCreatorProfile(companyId, input.creatorProfile);
|
||||
}
|
||||
|
||||
async function insertRevision(clipId: string, input: CreateClipRevision, requestedRevisionNumber?: number) {
|
||||
const revisionNumber = requestedRevisionNumber ?? await nextRevisionNumber(clipId);
|
||||
const [revision] = await db
|
||||
.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 db.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(clipId: string) {
|
||||
const [row] = await db
|
||||
.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(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 = await getClipById(clipId);
|
||||
if (!clip) throw notFound("Clip not found");
|
||||
const revision = await insertRevision(clip.id, input);
|
||||
const [updated] = await db
|
||||
.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();
|
||||
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({ 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.filter((clip) => clip.creatorProfileId === profile.id),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
createCreatorProfile,
|
||||
getCreatorProfileById,
|
||||
getClipById,
|
||||
getClipBySlug,
|
||||
listPublic,
|
||||
getPublicDetail,
|
||||
getPublicRevision,
|
||||
getCreatorPublicProfile,
|
||||
publish,
|
||||
createRevision,
|
||||
updateClip,
|
||||
createVote,
|
||||
createReport,
|
||||
createComment,
|
||||
createShowcase,
|
||||
recordImportTelemetry,
|
||||
};
|
||||
}
|
||||
@@ -32,6 +32,7 @@ export { budgetService } from "./budgets.js";
|
||||
export { secretService } from "./secrets.js";
|
||||
export { routineService } from "./routines.js";
|
||||
export { costService } from "./costs.js";
|
||||
export { clipService } from "./clips.js";
|
||||
export { financeService } from "./finance.js";
|
||||
export { heartbeatService } from "./heartbeat.js";
|
||||
export {
|
||||
|
||||
Reference in new issue
Block a user