feat: wire convex data foundations
This commit is contained in:
297
convex/schema.ts
Normal file
297
convex/schema.ts
Normal file
@@ -0,0 +1,297 @@
|
||||
import { defineSchema, defineTable } from "convex/server";
|
||||
import { v } from "convex/values";
|
||||
|
||||
const campaignStatus = v.union(v.literal("active"), v.literal("paused"));
|
||||
const leadPriority = v.union(
|
||||
v.literal("high"),
|
||||
v.literal("medium"),
|
||||
v.literal("low"),
|
||||
v.literal("defer"),
|
||||
);
|
||||
const leadContactStatus = v.union(
|
||||
v.literal("new"),
|
||||
v.literal("missing_contact"),
|
||||
v.literal("audit_ready"),
|
||||
v.literal("outreach_ready"),
|
||||
v.literal("contacted"),
|
||||
v.literal("replied"),
|
||||
v.literal("do_not_contact"),
|
||||
);
|
||||
const leadDuplicateStatus = v.union(
|
||||
v.literal("unchecked"),
|
||||
v.literal("unique"),
|
||||
v.literal("possible_duplicate"),
|
||||
v.literal("duplicate"),
|
||||
);
|
||||
const leadBlacklistStatus = v.union(v.literal("clear"), v.literal("blocked"));
|
||||
const auditStatus = v.union(
|
||||
v.literal("draft"),
|
||||
v.literal("approved"),
|
||||
v.literal("published"),
|
||||
v.literal("deactivated"),
|
||||
);
|
||||
const outreachStrategy = v.union(
|
||||
v.literal("call_first"),
|
||||
v.literal("email_first"),
|
||||
v.literal("defer"),
|
||||
v.literal("do_not_contact"),
|
||||
);
|
||||
const outreachApprovalStatus = v.union(
|
||||
v.literal("draft"),
|
||||
v.literal("approved"),
|
||||
v.literal("rejected"),
|
||||
);
|
||||
const outreachSendStatus = v.union(
|
||||
v.literal("not_sent"),
|
||||
v.literal("queued"),
|
||||
v.literal("sent"),
|
||||
v.literal("failed"),
|
||||
);
|
||||
const outreachResponseStatus = v.union(
|
||||
v.literal("none"),
|
||||
v.literal("manual_reply_recorded"),
|
||||
v.literal("no_interest"),
|
||||
v.literal("follow_up_needed"),
|
||||
);
|
||||
const outreachSalesStatus = v.union(
|
||||
v.literal("follow_up_planned"),
|
||||
v.literal("follow_up_sent"),
|
||||
v.literal("reply_received"),
|
||||
v.literal("not_interested"),
|
||||
v.literal("later"),
|
||||
v.literal("meeting_scheduled"),
|
||||
v.literal("proposal_requested"),
|
||||
v.literal("proposal_sent"),
|
||||
v.literal("won"),
|
||||
v.literal("lost"),
|
||||
v.literal("do_not_pursue"),
|
||||
);
|
||||
const blacklistType = v.union(
|
||||
v.literal("domain"),
|
||||
v.literal("email"),
|
||||
v.literal("phone"),
|
||||
v.literal("company"),
|
||||
v.literal("google_place_id"),
|
||||
);
|
||||
const runType = v.union(
|
||||
v.literal("campaign"),
|
||||
v.literal("lead_discovery"),
|
||||
v.literal("audit"),
|
||||
v.literal("outreach"),
|
||||
v.literal("lifecycle"),
|
||||
);
|
||||
const runStatus = v.union(
|
||||
v.literal("pending"),
|
||||
v.literal("running"),
|
||||
v.literal("succeeded"),
|
||||
v.literal("failed"),
|
||||
v.literal("canceled"),
|
||||
);
|
||||
const runEventLevel = v.union(
|
||||
v.literal("info"),
|
||||
v.literal("warning"),
|
||||
v.literal("error"),
|
||||
);
|
||||
const screenshotViewport = v.union(v.literal("desktop"), v.literal("mobile"));
|
||||
const settingsValue = v.union(v.string(), v.number(), v.boolean(), v.null());
|
||||
const auditMetricSummary = v.object({
|
||||
performanceScore: v.optional(v.number()),
|
||||
accessibilityScore: v.optional(v.number()),
|
||||
bestPracticesScore: v.optional(v.number()),
|
||||
seoScore: v.optional(v.number()),
|
||||
notes: v.optional(v.array(v.string())),
|
||||
});
|
||||
const playwrightSummary = v.object({
|
||||
pagesVisited: v.number(),
|
||||
contactLinksFound: v.number(),
|
||||
formsFound: v.number(),
|
||||
notes: v.optional(v.array(v.string())),
|
||||
});
|
||||
const eventDetail = v.object({
|
||||
label: v.string(),
|
||||
value: v.string(),
|
||||
source: v.optional(v.string()),
|
||||
});
|
||||
|
||||
export default defineSchema({
|
||||
campaigns: defineTable({
|
||||
name: v.string(),
|
||||
categoryMode: v.union(v.literal("preset"), v.literal("custom")),
|
||||
category: v.string(),
|
||||
customSearchTerm: v.optional(v.string()),
|
||||
postalCode: v.string(),
|
||||
region: v.optional(v.string()),
|
||||
latitude: v.optional(v.number()),
|
||||
longitude: v.optional(v.number()),
|
||||
radiusKm: v.number(),
|
||||
maxNewLeadsPerRun: v.number(),
|
||||
maxAuditsPerRun: v.number(),
|
||||
recurrence: v.union(
|
||||
v.literal("manual"),
|
||||
v.literal("daily"),
|
||||
v.literal("weekly"),
|
||||
v.literal("monthly"),
|
||||
),
|
||||
status: campaignStatus,
|
||||
lastRunAt: v.optional(v.number()),
|
||||
nextRunAt: v.optional(v.number()),
|
||||
createdAt: v.number(),
|
||||
updatedAt: v.number(),
|
||||
})
|
||||
.index("by_status", ["status"])
|
||||
.index("by_nextRunAt", ["nextRunAt"])
|
||||
.index("by_status_and_nextRunAt", ["status", "nextRunAt"]),
|
||||
|
||||
leads: defineTable({
|
||||
campaignId: v.optional(v.id("campaigns")),
|
||||
companyName: v.string(),
|
||||
niche: v.optional(v.string()),
|
||||
address: v.optional(v.string()),
|
||||
city: v.optional(v.string()),
|
||||
postalCode: v.optional(v.string()),
|
||||
googlePlaceId: v.optional(v.string()),
|
||||
googleMapsUrl: v.optional(v.string()),
|
||||
websiteDomain: v.optional(v.string()),
|
||||
phone: v.optional(v.string()),
|
||||
email: v.optional(v.string()),
|
||||
emailSource: v.optional(v.string()),
|
||||
contactPerson: v.optional(v.string()),
|
||||
priority: leadPriority,
|
||||
contactStatus: leadContactStatus,
|
||||
duplicateStatus: leadDuplicateStatus,
|
||||
blacklistStatus: leadBlacklistStatus,
|
||||
notes: v.optional(v.string()),
|
||||
createdAt: v.number(),
|
||||
updatedAt: v.number(),
|
||||
})
|
||||
.index("by_campaignId", ["campaignId"])
|
||||
.index("by_contactStatus", ["contactStatus"])
|
||||
.index("by_googlePlaceId", ["googlePlaceId"])
|
||||
.index("by_websiteDomain", ["websiteDomain"])
|
||||
.index("by_priority_and_contactStatus", ["priority", "contactStatus"]),
|
||||
|
||||
audits: defineTable({
|
||||
leadId: v.id("leads"),
|
||||
status: auditStatus,
|
||||
slug: v.string(),
|
||||
checkedDomain: v.string(),
|
||||
checkedPages: v.array(v.string()),
|
||||
pageSpeedSummary: v.optional(auditMetricSummary),
|
||||
playwrightSummary: v.optional(playwrightSummary),
|
||||
textFindings: v.optional(v.array(v.string())),
|
||||
skillSummaries: v.optional(
|
||||
v.array(
|
||||
v.object({
|
||||
name: v.string(),
|
||||
purpose: v.string(),
|
||||
summary: v.string(),
|
||||
}),
|
||||
),
|
||||
),
|
||||
multimodalSummary: v.optional(v.string()),
|
||||
internalSummary: v.optional(v.string()),
|
||||
publicSummary: v.optional(v.string()),
|
||||
publicBody: v.optional(v.string()),
|
||||
ctaType: v.optional(v.string()),
|
||||
publishedAt: v.optional(v.number()),
|
||||
reviewDueAt: v.optional(v.number()),
|
||||
deactivatedAt: v.optional(v.number()),
|
||||
createdAt: v.number(),
|
||||
updatedAt: v.number(),
|
||||
})
|
||||
.index("by_leadId", ["leadId"])
|
||||
.index("by_slug", ["slug"])
|
||||
.index("by_status", ["status"])
|
||||
.index("by_status_and_reviewDueAt", ["status", "reviewDueAt"]),
|
||||
|
||||
auditScreenshots: defineTable({
|
||||
auditId: v.id("audits"),
|
||||
storageId: v.id("_storage"),
|
||||
viewport: screenshotViewport,
|
||||
sourceUrl: v.string(),
|
||||
capturedAt: v.number(),
|
||||
width: v.number(),
|
||||
height: v.number(),
|
||||
mimeType: v.string(),
|
||||
createdAt: v.number(),
|
||||
})
|
||||
.index("by_auditId", ["auditId"])
|
||||
.index("by_auditId_and_viewport", ["auditId", "viewport"])
|
||||
.index("by_storageId", ["storageId"]),
|
||||
|
||||
outreachRecords: defineTable({
|
||||
leadId: v.id("leads"),
|
||||
auditId: v.optional(v.id("audits")),
|
||||
strategy: outreachStrategy,
|
||||
phoneScript: v.optional(v.string()),
|
||||
emailSubject: v.optional(v.string()),
|
||||
emailBody: v.optional(v.string()),
|
||||
followUpDraft: v.optional(v.string()),
|
||||
approvalStatus: outreachApprovalStatus,
|
||||
sendStatus: outreachSendStatus,
|
||||
sentAt: v.optional(v.number()),
|
||||
responseStatus: outreachResponseStatus,
|
||||
salesStatus: outreachSalesStatus,
|
||||
createdAt: v.number(),
|
||||
updatedAt: v.number(),
|
||||
})
|
||||
.index("by_leadId", ["leadId"])
|
||||
.index("by_auditId", ["auditId"])
|
||||
.index("by_approvalStatus", ["approvalStatus"])
|
||||
.index("by_sendStatus", ["sendStatus"]),
|
||||
|
||||
blacklistEntries: defineTable({
|
||||
type: blacklistType,
|
||||
value: v.string(),
|
||||
normalizedValue: v.string(),
|
||||
note: v.optional(v.string()),
|
||||
createdAt: v.number(),
|
||||
})
|
||||
.index("by_type_and_normalizedValue", ["type", "normalizedValue"])
|
||||
.index("by_normalizedValue", ["normalizedValue"]),
|
||||
|
||||
agentRuns: defineTable({
|
||||
type: runType,
|
||||
campaignId: v.optional(v.id("campaigns")),
|
||||
leadId: v.optional(v.id("leads")),
|
||||
auditId: v.optional(v.id("audits")),
|
||||
status: runStatus,
|
||||
startedAt: v.optional(v.number()),
|
||||
finishedAt: v.optional(v.number()),
|
||||
currentStep: v.optional(v.string()),
|
||||
errorSummary: v.optional(v.string()),
|
||||
counters: v.optional(
|
||||
v.object({
|
||||
leadsFound: v.number(),
|
||||
leadsCreated: v.number(),
|
||||
auditsCreated: v.number(),
|
||||
outreachPrepared: v.number(),
|
||||
errors: v.number(),
|
||||
}),
|
||||
),
|
||||
createdAt: v.number(),
|
||||
updatedAt: v.number(),
|
||||
})
|
||||
.index("by_status", ["status"])
|
||||
.index("by_type_and_status", ["type", "status"])
|
||||
.index("by_campaignId_and_status", ["campaignId", "status"])
|
||||
.index("by_auditId", ["auditId"]),
|
||||
|
||||
agentRunEvents: defineTable({
|
||||
runId: v.id("agentRuns"),
|
||||
level: runEventLevel,
|
||||
message: v.string(),
|
||||
details: v.optional(v.array(eventDetail)),
|
||||
createdAt: v.number(),
|
||||
})
|
||||
.index("by_runId_and_createdAt", ["runId", "createdAt"])
|
||||
.index("by_level", ["level"]),
|
||||
|
||||
settings: defineTable({
|
||||
key: v.string(),
|
||||
value: settingsValue,
|
||||
description: v.optional(v.string()),
|
||||
createdAt: v.number(),
|
||||
updatedAt: v.number(),
|
||||
}).index("by_key", ["key"]),
|
||||
});
|
||||
Reference in New Issue
Block a user