227 lines
8.3 KiB
TypeScript
227 lines
8.3 KiB
TypeScript
import { defineSchema, defineTable } from "convex/server";
|
|
import { v } from "convex/values";
|
|
import { authTables } from "@convex-dev/auth/server";
|
|
|
|
const schema = defineSchema({
|
|
...authTables,
|
|
projects: defineTable({
|
|
userId: v.id("users"),
|
|
name: v.string(),
|
|
isDefault: v.boolean(),
|
|
dorkingConfig: v.object({
|
|
selectedSourceIds: v.array(v.id("dataSources")),
|
|
}),
|
|
}).index("by_owner", ["userId"]),
|
|
dataSources: defineTable({
|
|
projectId: v.id("projects"),
|
|
type: v.literal("website"),
|
|
url: v.string(),
|
|
name: v.string(),
|
|
analysisStatus: v.union(
|
|
v.literal("pending"),
|
|
v.literal("completed"),
|
|
v.literal("failed")
|
|
),
|
|
lastAnalyzedAt: v.optional(v.number()),
|
|
lastError: v.optional(v.string()),
|
|
analysisResults: v.optional(
|
|
v.object({
|
|
features: v.array(v.string()),
|
|
painPoints: v.array(v.string()),
|
|
keywords: v.array(v.string()),
|
|
summary: v.string(),
|
|
})
|
|
),
|
|
metadata: v.optional(v.any()),
|
|
}).index("by_project_url", ["projectId", "url"]),
|
|
analyses: defineTable({
|
|
projectId: v.id("projects"),
|
|
dataSourceId: v.id("dataSources"),
|
|
createdAt: v.number(),
|
|
analysisVersion: v.string(),
|
|
productName: v.string(),
|
|
tagline: v.string(),
|
|
description: v.string(),
|
|
category: v.string(),
|
|
positioning: v.string(),
|
|
features: v.array(v.object({
|
|
name: v.string(),
|
|
description: v.string(),
|
|
benefits: v.array(v.string()),
|
|
useCases: v.array(v.string()),
|
|
})),
|
|
problemsSolved: v.array(v.object({
|
|
problem: v.string(),
|
|
severity: v.union(v.literal("high"), v.literal("medium"), v.literal("low")),
|
|
currentWorkarounds: v.array(v.string()),
|
|
emotionalImpact: v.string(),
|
|
searchTerms: v.array(v.string()),
|
|
})),
|
|
personas: v.array(v.object({
|
|
name: v.string(),
|
|
role: v.string(),
|
|
companySize: v.string(),
|
|
industry: v.string(),
|
|
painPoints: v.array(v.string()),
|
|
goals: v.array(v.string()),
|
|
techSavvy: v.union(v.literal("low"), v.literal("medium"), v.literal("high")),
|
|
objections: v.array(v.string()),
|
|
searchBehavior: v.array(v.string()),
|
|
})),
|
|
keywords: v.array(v.object({
|
|
term: v.string(),
|
|
type: v.union(
|
|
v.literal("product"),
|
|
v.literal("problem"),
|
|
v.literal("solution"),
|
|
v.literal("competitor"),
|
|
v.literal("feature"),
|
|
v.literal("longtail"),
|
|
v.literal("differentiator")
|
|
),
|
|
searchVolume: v.union(v.literal("high"), v.literal("medium"), v.literal("low")),
|
|
intent: v.union(v.literal("informational"), v.literal("navigational"), v.literal("transactional")),
|
|
funnel: v.union(v.literal("awareness"), v.literal("consideration"), v.literal("decision")),
|
|
emotionalIntensity: v.union(v.literal("frustrated"), v.literal("curious"), v.literal("ready")),
|
|
})),
|
|
useCases: v.array(v.object({
|
|
scenario: v.string(),
|
|
trigger: v.string(),
|
|
emotionalState: v.string(),
|
|
currentWorkflow: v.array(v.string()),
|
|
desiredOutcome: v.string(),
|
|
alternativeProducts: v.array(v.string()),
|
|
whyThisProduct: v.string(),
|
|
churnRisk: v.array(v.string()),
|
|
})),
|
|
competitors: v.array(v.object({
|
|
name: v.string(),
|
|
differentiator: v.string(),
|
|
theirStrength: v.string(),
|
|
switchTrigger: v.string(),
|
|
theirWeakness: v.string(),
|
|
})),
|
|
dorkQueries: v.array(v.object({
|
|
query: v.string(),
|
|
platform: v.union(
|
|
v.literal("reddit"),
|
|
v.literal("hackernews"),
|
|
v.literal("indiehackers"),
|
|
v.literal("twitter"),
|
|
v.literal("quora"),
|
|
v.literal("stackoverflow")
|
|
),
|
|
intent: v.union(
|
|
v.literal("looking-for"),
|
|
v.literal("frustrated"),
|
|
v.literal("alternative"),
|
|
v.literal("comparison"),
|
|
v.literal("problem-solving"),
|
|
v.literal("tutorial")
|
|
),
|
|
priority: v.union(v.literal("high"), v.literal("medium"), v.literal("low")),
|
|
})),
|
|
scrapedAt: v.string(),
|
|
})
|
|
.index("by_project_createdAt", ["projectId", "createdAt"])
|
|
.index("by_dataSource_createdAt", ["dataSourceId", "createdAt"]),
|
|
analysisSections: defineTable({
|
|
analysisId: v.id("analyses"),
|
|
sectionKey: v.string(),
|
|
items: v.any(),
|
|
lastPrompt: v.optional(v.string()),
|
|
source: v.union(v.literal("ai"), v.literal("manual"), v.literal("mixed")),
|
|
updatedAt: v.number(),
|
|
})
|
|
.index("by_analysis", ["analysisId"])
|
|
.index("by_analysis_section", ["analysisId", "sectionKey"]),
|
|
opportunities: defineTable({
|
|
projectId: v.id("projects"),
|
|
analysisId: v.optional(v.id("analyses")),
|
|
searchJobId: v.optional(v.id("searchJobs")),
|
|
url: v.string(),
|
|
platform: v.string(),
|
|
title: v.string(),
|
|
snippet: v.string(),
|
|
relevanceScore: v.number(),
|
|
intent: v.string(),
|
|
status: v.string(),
|
|
sentAt: v.optional(v.number()),
|
|
archivedAt: v.optional(v.number()),
|
|
suggestedApproach: v.string(),
|
|
matchedKeywords: v.array(v.string()),
|
|
matchedProblems: v.array(v.string()),
|
|
tags: v.optional(v.array(v.string())),
|
|
notes: v.optional(v.string()),
|
|
softPitch: v.boolean(),
|
|
createdAt: v.number(),
|
|
updatedAt: v.number(),
|
|
})
|
|
.index("by_project_status", ["projectId", "status"])
|
|
.index("by_project_createdAt", ["projectId", "createdAt"])
|
|
.index("by_project_url", ["projectId", "url"])
|
|
.index("by_project_searchJob", ["projectId", "searchJobId"]),
|
|
userActivity: defineTable({
|
|
userId: v.id("users"),
|
|
lastActiveDate: v.string(),
|
|
streak: v.number(),
|
|
updatedAt: v.number(),
|
|
}).index("by_user", ["userId"]),
|
|
seenUrls: defineTable({
|
|
projectId: v.id("projects"),
|
|
url: v.string(),
|
|
firstSeenAt: v.number(),
|
|
lastSeenAt: v.number(),
|
|
source: v.optional(v.string()),
|
|
})
|
|
.index("by_project_url", ["projectId", "url"])
|
|
.index("by_project_lastSeen", ["projectId", "lastSeenAt"]),
|
|
analysisJobs: defineTable({
|
|
projectId: v.id("projects"),
|
|
dataSourceId: v.optional(v.id("dataSources")),
|
|
status: v.union(
|
|
v.literal("pending"),
|
|
v.literal("running"),
|
|
v.literal("completed"),
|
|
v.literal("failed")
|
|
),
|
|
progress: v.optional(v.number()),
|
|
stage: v.optional(v.string()),
|
|
timeline: v.optional(v.array(v.object({
|
|
key: v.string(),
|
|
label: v.string(),
|
|
status: v.union(
|
|
v.literal("pending"),
|
|
v.literal("running"),
|
|
v.literal("completed"),
|
|
v.literal("failed")
|
|
),
|
|
detail: v.optional(v.string()),
|
|
}))),
|
|
error: v.optional(v.string()),
|
|
createdAt: v.number(),
|
|
updatedAt: v.number(),
|
|
})
|
|
.index("by_project_status", ["projectId", "status"])
|
|
.index("by_project_createdAt", ["projectId", "createdAt"])
|
|
.index("by_dataSource_createdAt", ["dataSourceId", "createdAt"]),
|
|
searchJobs: defineTable({
|
|
projectId: v.id("projects"),
|
|
status: v.union(
|
|
v.literal("pending"),
|
|
v.literal("running"),
|
|
v.literal("completed"),
|
|
v.literal("failed")
|
|
),
|
|
config: v.optional(v.any()),
|
|
progress: v.optional(v.number()),
|
|
error: v.optional(v.string()),
|
|
createdAt: v.number(),
|
|
updatedAt: v.number(),
|
|
})
|
|
.index("by_project_status", ["projectId", "status"])
|
|
.index("by_project_createdAt", ["projectId", "createdAt"]),
|
|
});
|
|
|
|
export default schema;
|