Add waitlist schema for marketing (FRE-635)
- Created waitlist_signups and waitlist_events tables - Supports email, name, source tracking, and status management - Enables VIP supporter list for Product Hunt launch - Migration 0002_chemical_shocker.sql generated - Fixed brand color in product-hunt-assets-brief.md (#518ac8)
This commit is contained in:
132
src/db/migrations/0002_chemical_shocker.sql
Normal file
132
src/db/migrations/0002_chemical_shocker.sql
Normal file
@@ -0,0 +1,132 @@
|
||||
CREATE TABLE `alert_rules` (
|
||||
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
`name` text NOT NULL,
|
||||
`kpi_key` text NOT NULL,
|
||||
`condition` text NOT NULL,
|
||||
`threshold` real NOT NULL,
|
||||
`severity` text DEFAULT 'medium' NOT NULL,
|
||||
`channel_id` text,
|
||||
`is_active` integer DEFAULT true NOT NULL,
|
||||
`cooldown_minutes` integer DEFAULT 60 NOT NULL,
|
||||
`created_at` integer DEFAULT '"2026-04-26T10:21:03.325Z"' NOT NULL,
|
||||
`updated_at` integer DEFAULT '"2026-04-26T10:21:03.325Z"' NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `alerts` (
|
||||
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
`rule_id` integer NOT NULL,
|
||||
`kpi_key` text NOT NULL,
|
||||
`kpi_value` real NOT NULL,
|
||||
`threshold` real NOT NULL,
|
||||
`severity` text NOT NULL,
|
||||
`message` text NOT NULL,
|
||||
`was_sent` integer DEFAULT false NOT NULL,
|
||||
`sent_at` integer,
|
||||
`acknowledged_by` integer,
|
||||
`acknowledged_at` integer,
|
||||
`created_at` integer DEFAULT '"2026-04-26T10:21:03.332Z"' NOT NULL,
|
||||
FOREIGN KEY (`rule_id`) REFERENCES `alert_rules`(`id`) ON UPDATE no action ON DELETE no action
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `cohort_members` (
|
||||
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
`cohort_id` integer NOT NULL,
|
||||
`user_id` integer NOT NULL,
|
||||
`joined_at` integer DEFAULT '"2026-04-26T10:21:03.344Z"' NOT NULL,
|
||||
FOREIGN KEY (`cohort_id`) REFERENCES `cohorts`(`id`) ON UPDATE no action ON DELETE no action
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `cohorts` (
|
||||
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
`name` text NOT NULL,
|
||||
`definition` text NOT NULL,
|
||||
`period_start` integer NOT NULL,
|
||||
`period_end` integer,
|
||||
`size` integer DEFAULT 0 NOT NULL,
|
||||
`retention_data` text,
|
||||
`metadata` text,
|
||||
`created_at` integer DEFAULT '"2026-04-26T10:21:03.344Z"' NOT NULL,
|
||||
`updated_at` integer DEFAULT '"2026-04-26T10:21:03.344Z"' NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `kpi_snapshots` (
|
||||
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
`kpi_key` text NOT NULL,
|
||||
`kpi_value` real NOT NULL,
|
||||
`period_start` integer NOT NULL,
|
||||
`period_end` integer NOT NULL,
|
||||
`metadata` text,
|
||||
`created_at` integer DEFAULT '"2026-04-26T10:21:03.320Z"' NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `nps_responses` (
|
||||
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
`user_id` integer,
|
||||
`score` integer NOT NULL,
|
||||
`category` text NOT NULL,
|
||||
`feedback` text,
|
||||
`survey_id` text,
|
||||
`respondent_email` text,
|
||||
`created_at` integer DEFAULT '"2026-04-26T10:21:03.340Z"' NOT NULL,
|
||||
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `scheduled_reports` (
|
||||
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
`name` text NOT NULL,
|
||||
`report_type` text NOT NULL,
|
||||
`schedule` text NOT NULL,
|
||||
`recipients` text NOT NULL,
|
||||
`format` text DEFAULT 'slack' NOT NULL,
|
||||
`is_active` integer DEFAULT true NOT NULL,
|
||||
`last_run_at` integer,
|
||||
`next_run_at` integer,
|
||||
`metadata` text,
|
||||
`created_at` integer DEFAULT '"2026-04-26T10:21:03.336Z"' NOT NULL,
|
||||
`updated_at` integer DEFAULT '"2026-04-26T10:21:03.336Z"' NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `waitlist_events` (
|
||||
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
`signup_id` integer NOT NULL,
|
||||
`event_type` text NOT NULL,
|
||||
`event_data` text,
|
||||
`created_at` integer DEFAULT '"2026-04-26T10:21:03.348Z"' NOT NULL,
|
||||
FOREIGN KEY (`signup_id`) REFERENCES `waitlist_signups`(`id`) ON UPDATE no action ON DELETE no action
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `waitlist_signups` (
|
||||
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
`email` text NOT NULL,
|
||||
`name` text,
|
||||
`source` text DEFAULT 'organic' NOT NULL,
|
||||
`status` text DEFAULT 'waitlist' NOT NULL,
|
||||
`metadata` text,
|
||||
`created_at` integer DEFAULT '"2026-04-26T10:21:03.348Z"' NOT NULL,
|
||||
`updated_at` integer DEFAULT '"2026-04-26T10:21:03.348Z"' NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `waitlist_signups_email_unique` ON `waitlist_signups` (`email`);--> statement-breakpoint
|
||||
DROP INDEX "character_relationships_unique_pair";--> statement-breakpoint
|
||||
DROP INDEX "revision_changes_revision_idx";--> statement-breakpoint
|
||||
DROP INDEX "revision_changes_type_idx";--> statement-breakpoint
|
||||
DROP INDEX "revisions_script_version_idx";--> statement-breakpoint
|
||||
DROP INDEX "revisions_script_branch_idx";--> statement-breakpoint
|
||||
DROP INDEX "revisions_author_idx";--> statement-breakpoint
|
||||
DROP INDEX "users_email_unique";--> statement-breakpoint
|
||||
DROP INDEX "users_username_unique";--> statement-breakpoint
|
||||
DROP INDEX "waitlist_signups_email_unique";--> statement-breakpoint
|
||||
ALTER TABLE `projects` ALTER COLUMN "created_at" TO "created_at" integer NOT NULL DEFAULT '"2026-04-26T10:21:03.304Z"';--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `character_relationships_unique_pair` ON `character_relationships` (`character_a_id`,`character_b_id`);--> statement-breakpoint
|
||||
CREATE INDEX `revision_changes_revision_idx` ON `revision_changes` (`revision_id`);--> statement-breakpoint
|
||||
CREATE INDEX `revision_changes_type_idx` ON `revision_changes` (`change_type`);--> statement-breakpoint
|
||||
CREATE INDEX `revisions_script_version_idx` ON `revisions` (`script_id`,`version_number`);--> statement-breakpoint
|
||||
CREATE INDEX `revisions_script_branch_idx` ON `revisions` (`script_id`,`branch_name`);--> statement-breakpoint
|
||||
CREATE INDEX `revisions_author_idx` ON `revisions` (`author_id`);--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `users_email_unique` ON `users` (`email`);--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `users_username_unique` ON `users` (`username`);--> statement-breakpoint
|
||||
ALTER TABLE `projects` ALTER COLUMN "updated_at" TO "updated_at" integer NOT NULL DEFAULT '"2026-04-26T10:21:03.304Z"';--> statement-breakpoint
|
||||
ALTER TABLE `scripts` ALTER COLUMN "created_at" TO "created_at" integer NOT NULL DEFAULT '"2026-04-26T10:21:03.306Z"';--> statement-breakpoint
|
||||
ALTER TABLE `scripts` ALTER COLUMN "updated_at" TO "updated_at" integer NOT NULL DEFAULT '"2026-04-26T10:21:03.306Z"';--> statement-breakpoint
|
||||
ALTER TABLE `users` ALTER COLUMN "created_at" TO "created_at" integer NOT NULL DEFAULT '"2026-04-26T10:21:03.301Z"';--> statement-breakpoint
|
||||
ALTER TABLE `users` ALTER COLUMN "updated_at" TO "updated_at" integer NOT NULL DEFAULT '"2026-04-26T10:21:03.301Z"';
|
||||
1713
src/db/migrations/meta/0002_snapshot.json
Normal file
1713
src/db/migrations/meta/0002_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -15,6 +15,13 @@
|
||||
"when": 1777044483775,
|
||||
"tag": "0001_tan_machine_man",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 2,
|
||||
"version": "6",
|
||||
"when": 1777198863362,
|
||||
"tag": "0002_chemical_shocker",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -10,3 +10,4 @@ export { alerts, type Alert, type NewAlert } from "./alerts";
|
||||
export { scheduledReports, type ScheduledReport, type NewScheduledReport } from "./scheduled_reports";
|
||||
export { npsResponses, type NPSResponse, type NewNPSResponse } from "./nps_responses";
|
||||
export { cohorts, cohortMembers, type Cohort, type NewCohort, type CohortMember, type NewCohortMember } from "./cohorts";
|
||||
export { waitlistSignups, waitlistEvents, type WaitlistSignup, type NewWaitlistSignup, type WaitlistEvent, type NewWaitlistEvent } from "./waitlist";
|
||||
|
||||
32
src/db/schema/project_members.ts
Normal file
32
src/db/schema/project_members.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { sqliteTable, text, integer, index } from "drizzle-orm/sqlite-core";
|
||||
import { projects } from "./projects";
|
||||
import { users } from "./users";
|
||||
|
||||
export const projectMembers = sqliteTable(
|
||||
"project_members",
|
||||
{
|
||||
id: integer("id").primaryKey({ autoIncrement: true }),
|
||||
projectId: integer("project_id")
|
||||
.notNull()
|
||||
.references(() => projects.id, { onDelete: "cascade" }),
|
||||
userId: integer("user_id")
|
||||
.notNull()
|
||||
.references(() => users.id, { onDelete: "cascade" }),
|
||||
role: text("role", { enum: ["owner", "admin", "editor", "viewer"] })
|
||||
.notNull()
|
||||
.default("editor"),
|
||||
addedAt: integer("added_at", { mode: "timestamp" })
|
||||
.notNull()
|
||||
.$defaultFn(() => new Date()),
|
||||
},
|
||||
(t) => ({
|
||||
uniqueProjectUser: index("project_members_project_user_unique").on(
|
||||
t.projectId,
|
||||
t.userId
|
||||
),
|
||||
userIdx: index("idx_project_members_user").on(t.userId),
|
||||
})
|
||||
);
|
||||
|
||||
export type ProjectMember = typeof projectMembers.$inferSelect;
|
||||
export type NewProjectMember = typeof projectMembers.$inferInsert;
|
||||
51
src/db/schema/teams.ts
Normal file
51
src/db/schema/teams.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { sqliteTable, text, integer, index } from "drizzle-orm/sqlite-core";
|
||||
import { users } from "./users";
|
||||
|
||||
export const teams = sqliteTable(
|
||||
"teams",
|
||||
{
|
||||
id: text("id").primaryKey(),
|
||||
name: text("name").notNull(),
|
||||
ownerId: integer("owner_id")
|
||||
.notNull()
|
||||
.references(() => users.id),
|
||||
createdAt: integer("created_at", { mode: "timestamp" })
|
||||
.notNull()
|
||||
.$defaultFn(() => new Date()),
|
||||
updatedAt: integer("updated_at", { mode: "timestamp" })
|
||||
.notNull()
|
||||
.$defaultFn(() => new Date()),
|
||||
},
|
||||
(t) => [index("idx_teams_owner").on(t.ownerId)]
|
||||
);
|
||||
|
||||
export const teamMembers = sqliteTable(
|
||||
"team_members",
|
||||
{
|
||||
id: integer("id").primaryKey({ autoIncrement: true }),
|
||||
teamId: text("team_id")
|
||||
.notNull()
|
||||
.references(() => teams.id, { onDelete: "cascade" }),
|
||||
userId: integer("user_id")
|
||||
.notNull()
|
||||
.references(() => users.id, { onDelete: "cascade" }),
|
||||
role: text("role", { enum: ["owner", "admin", "editor", "viewer"] })
|
||||
.notNull()
|
||||
.default("editor"),
|
||||
joinedAt: integer("joined_at", { mode: "timestamp" })
|
||||
.notNull()
|
||||
.$defaultFn(() => new Date()),
|
||||
},
|
||||
(t) => ({
|
||||
uniqueTeamUser: index("team_members_team_user_unique").on(
|
||||
t.teamId,
|
||||
t.userId
|
||||
),
|
||||
userIdx: index("idx_team_members_user").on(t.userId),
|
||||
})
|
||||
);
|
||||
|
||||
export type Team = typeof teams.$inferSelect;
|
||||
export type NewTeam = typeof teams.$inferInsert;
|
||||
export type TeamMember = typeof teamMembers.$inferSelect;
|
||||
export type NewTeamMember = typeof teamMembers.$inferInsert;
|
||||
25
src/db/schema/waitlist.ts
Normal file
25
src/db/schema/waitlist.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core";
|
||||
|
||||
export const waitlistSignups = sqliteTable("waitlist_signups", {
|
||||
id: integer("id").primaryKey({ autoIncrement: true }),
|
||||
email: text("email").notNull().unique(),
|
||||
name: text("name"),
|
||||
source: text("source").notNull().default("organic"),
|
||||
status: text("status").notNull().default("waitlist"),
|
||||
metadata: text("metadata"),
|
||||
createdAt: integer("created_at", { mode: "timestamp" }).notNull().default(new Date()),
|
||||
updatedAt: integer("updated_at", { mode: "timestamp" }).notNull().default(new Date()),
|
||||
});
|
||||
|
||||
export const waitlistEvents = sqliteTable("waitlist_events", {
|
||||
id: integer("id").primaryKey({ autoIncrement: true }),
|
||||
signupId: integer("signup_id").notNull().references(() => waitlistSignups.id),
|
||||
eventType: text("event_type").notNull(),
|
||||
eventData: text("event_data"),
|
||||
createdAt: integer("created_at", { mode: "timestamp" }).notNull().default(new Date()),
|
||||
});
|
||||
|
||||
export type WaitlistSignup = typeof waitlistSignups.$inferSelect;
|
||||
export type NewWaitlistSignup = typeof waitlistSignups.$inferInsert;
|
||||
export type WaitlistEvent = typeof waitlistEvents.$inferSelect;
|
||||
export type NewWaitlistEvent = typeof waitlistEvents.$inferInsert;
|
||||
Reference in New Issue
Block a user