@hogsend/db 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/LICENSE +93 -0
  2. package/README.md +14 -0
  3. package/drizzle/0000_nifty_songbird.sql +188 -0
  4. package/drizzle/0001_minor_shockwave.sql +13 -0
  5. package/drizzle/0002_early_owl.sql +1 -0
  6. package/drizzle/0003_bizarre_annihilus.sql +9 -0
  7. package/drizzle/0004_brave_betty_brant.sql +1 -0
  8. package/drizzle/0005_groovy_princess_powerful.sql +8 -0
  9. package/drizzle/0006_groovy_charles_xavier.sql +100 -0
  10. package/drizzle/0007_serious_captain_universe.sql +1 -0
  11. package/drizzle/0008_demonic_agent_brand.sql +5 -0
  12. package/drizzle/meta/0000_snapshot.json +1264 -0
  13. package/drizzle/meta/0001_snapshot.json +1353 -0
  14. package/drizzle/meta/0002_snapshot.json +1380 -0
  15. package/drizzle/meta/0003_snapshot.json +1443 -0
  16. package/drizzle/meta/0004_snapshot.json +1464 -0
  17. package/drizzle/meta/0005_snapshot.json +1588 -0
  18. package/drizzle/meta/0006_snapshot.json +2331 -0
  19. package/drizzle/meta/0007_snapshot.json +2346 -0
  20. package/drizzle/meta/0008_snapshot.json +2449 -0
  21. package/drizzle/meta/_journal.json +69 -0
  22. package/package.json +49 -0
  23. package/src/index.ts +35 -0
  24. package/src/migrate-client.ts +56 -0
  25. package/src/migrate.ts +173 -0
  26. package/src/schema/_shared.ts +10 -0
  27. package/src/schema/alert-history.ts +21 -0
  28. package/src/schema/alert-rules.ts +36 -0
  29. package/src/schema/api-keys.ts +30 -0
  30. package/src/schema/audit-logs.ts +22 -0
  31. package/src/schema/auth.ts +89 -0
  32. package/src/schema/contacts.ts +31 -0
  33. package/src/schema/dead-letter-queue.ts +31 -0
  34. package/src/schema/email-preferences.ts +35 -0
  35. package/src/schema/email-sends.ts +34 -0
  36. package/src/schema/enums.ts +47 -0
  37. package/src/schema/import-jobs.ts +26 -0
  38. package/src/schema/index.ts +18 -0
  39. package/src/schema/journey-configs.ts +15 -0
  40. package/src/schema/journey-logs.ts +21 -0
  41. package/src/schema/journey-states.ts +54 -0
  42. package/src/schema/link-clicks.ts +21 -0
  43. package/src/schema/relations.ts +160 -0
  44. package/src/schema/tracked-links.ts +17 -0
  45. package/src/schema/user-events.ts +35 -0
  46. package/src/seed.ts +91 -0
  47. package/src/version.ts +162 -0
@@ -0,0 +1,35 @@
1
+ import {
2
+ boolean,
3
+ integer,
4
+ jsonb,
5
+ pgTable,
6
+ text,
7
+ timestamp,
8
+ uniqueIndex,
9
+ uuid,
10
+ } from "drizzle-orm/pg-core";
11
+ import { timestamps } from "./_shared.js";
12
+
13
+ export const emailPreferences = pgTable(
14
+ "email_preferences",
15
+ {
16
+ id: uuid("id").defaultRandom().primaryKey(),
17
+ userId: text("user_id").notNull(),
18
+ email: text("email").notNull(),
19
+ unsubscribedAll: boolean("unsubscribed_all").notNull().default(false),
20
+ suppressed: boolean("suppressed").notNull().default(false),
21
+ bounceCount: integer("bounce_count").notNull().default(0),
22
+ categories: jsonb("categories")
23
+ .$type<Record<string, boolean>>()
24
+ .default({}),
25
+ suppressedAt: timestamp("suppressed_at", { withTimezone: true }),
26
+ lastBounceAt: timestamp("last_bounce_at", { withTimezone: true }),
27
+ ...timestamps,
28
+ },
29
+ (table) => [
30
+ uniqueIndex("email_preferences_user_email_idx").on(
31
+ table.userId,
32
+ table.email,
33
+ ),
34
+ ],
35
+ );
@@ -0,0 +1,34 @@
1
+ import { index, pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core";
2
+ import { timestamps } from "./_shared.js";
3
+ import { emailSendStatusEnum } from "./enums.js";
4
+ import { journeyStates } from "./journey-states.js";
5
+
6
+ export const emailSends = pgTable(
7
+ "email_sends",
8
+ {
9
+ id: uuid("id").defaultRandom().primaryKey(),
10
+ organizationId: text("organization_id"),
11
+ journeyStateId: uuid("journey_state_id").references(() => journeyStates.id),
12
+ templateKey: text("template_key"),
13
+ resendId: text("resend_id"),
14
+ fromEmail: text("from_email").notNull(),
15
+ toEmail: text("to_email").notNull(),
16
+ subject: text("subject").notNull(),
17
+ category: text("category"),
18
+ status: emailSendStatusEnum("status").notNull().default("queued"),
19
+ sentAt: timestamp("sent_at", { withTimezone: true }),
20
+ deliveredAt: timestamp("delivered_at", { withTimezone: true }),
21
+ openedAt: timestamp("opened_at", { withTimezone: true }),
22
+ clickedAt: timestamp("clicked_at", { withTimezone: true }),
23
+ bouncedAt: timestamp("bounced_at", { withTimezone: true }),
24
+ complainedAt: timestamp("complained_at", { withTimezone: true }),
25
+ ...timestamps,
26
+ },
27
+ (table) => [
28
+ index("email_sends_to_email_idx").on(table.toEmail),
29
+ index("email_sends_template_key_idx").on(table.templateKey),
30
+ index("email_sends_status_idx").on(table.status),
31
+ index("email_sends_created_at_idx").on(table.createdAt),
32
+ index("email_sends_journey_state_id_idx").on(table.journeyStateId),
33
+ ],
34
+ );
@@ -0,0 +1,47 @@
1
+ import { pgEnum } from "drizzle-orm/pg-core";
2
+
3
+ export const journeyStatusEnum = pgEnum("journey_status", [
4
+ "active",
5
+ "waiting",
6
+ "completed",
7
+ "failed",
8
+ "exited",
9
+ ]);
10
+
11
+ export const emailSendStatusEnum = pgEnum("email_send_status", [
12
+ "queued",
13
+ "rendered",
14
+ "sent",
15
+ "delivered",
16
+ "opened",
17
+ "clicked",
18
+ "bounced",
19
+ "complained",
20
+ "failed",
21
+ ]);
22
+
23
+ export const importJobStatusEnum = pgEnum("import_job_status", [
24
+ "pending",
25
+ "processing",
26
+ "completed",
27
+ "failed",
28
+ ]);
29
+
30
+ export const alertRuleTypeEnum = pgEnum("alert_rule_type", [
31
+ "bounce_rate_exceeded",
32
+ "journey_failure_spike",
33
+ "delivery_issue",
34
+ "high_complaint_rate",
35
+ ]);
36
+
37
+ export const alertChannelEnum = pgEnum("alert_channel", [
38
+ "webhook",
39
+ "slack",
40
+ "email",
41
+ ]);
42
+
43
+ export const dlqStatusEnum = pgEnum("dlq_status", [
44
+ "pending",
45
+ "retried",
46
+ "discarded",
47
+ ]);
@@ -0,0 +1,26 @@
1
+ import {
2
+ index,
3
+ integer,
4
+ jsonb,
5
+ pgTable,
6
+ text,
7
+ uuid,
8
+ } from "drizzle-orm/pg-core";
9
+ import { timestamps } from "./_shared.js";
10
+ import { importJobStatusEnum } from "./enums.js";
11
+
12
+ export const importJobs = pgTable(
13
+ "import_jobs",
14
+ {
15
+ id: uuid("id").defaultRandom().primaryKey(),
16
+ fileName: text("file_name"),
17
+ format: text("format").notNull(),
18
+ status: importJobStatusEnum("status").notNull().default("pending"),
19
+ totalRows: integer("total_rows"),
20
+ processedRows: integer("processed_rows").notNull().default(0),
21
+ failedRows: integer("failed_rows").notNull().default(0),
22
+ errors: jsonb("errors").$type<Array<{ row: number; error: string }>>(),
23
+ ...timestamps,
24
+ },
25
+ (table) => [index("import_jobs_status_idx").on(table.status)],
26
+ );
@@ -0,0 +1,18 @@
1
+ export * from "./alert-history.js";
2
+ export * from "./alert-rules.js";
3
+ export * from "./api-keys.js";
4
+ export * from "./audit-logs.js";
5
+ export * from "./auth.js";
6
+ export * from "./contacts.js";
7
+ export * from "./dead-letter-queue.js";
8
+ export * from "./email-preferences.js";
9
+ export * from "./email-sends.js";
10
+ export * from "./enums.js";
11
+ export * from "./import-jobs.js";
12
+ export * from "./journey-configs.js";
13
+ export * from "./journey-logs.js";
14
+ export * from "./journey-states.js";
15
+ export * from "./link-clicks.js";
16
+ export * from "./relations.js";
17
+ export * from "./tracked-links.js";
18
+ export * from "./user-events.js";
@@ -0,0 +1,15 @@
1
+ import { boolean, pgTable, text, uniqueIndex, uuid } from "drizzle-orm/pg-core";
2
+ import { timestamps } from "./_shared.js";
3
+
4
+ export const journeyConfigs = pgTable(
5
+ "journey_configs",
6
+ {
7
+ id: uuid("id").defaultRandom().primaryKey(),
8
+ journeyId: text("journey_id").notNull(),
9
+ enabled: boolean("enabled").notNull().default(true),
10
+ ...timestamps,
11
+ },
12
+ (table) => [
13
+ uniqueIndex("journey_configs_journey_id_idx").on(table.journeyId),
14
+ ],
15
+ );
@@ -0,0 +1,21 @@
1
+ import { index, jsonb, pgTable, text, uuid } from "drizzle-orm/pg-core";
2
+ import { timestamps } from "./_shared.js";
3
+ import { journeyStates } from "./journey-states.js";
4
+
5
+ export const journeyLogs = pgTable(
6
+ "journey_logs",
7
+ {
8
+ id: uuid("id").defaultRandom().primaryKey(),
9
+ journeyStateId: uuid("journey_state_id")
10
+ .notNull()
11
+ .references(() => journeyStates.id, { onDelete: "cascade" }),
12
+ fromNodeId: text("from_node_id"),
13
+ toNodeId: text("to_node_id"),
14
+ action: text("action").notNull(),
15
+ detail: jsonb("detail").$type<Record<string, unknown>>(),
16
+ ...timestamps,
17
+ },
18
+ (table) => [
19
+ index("journey_logs_journey_state_id_idx").on(table.journeyStateId),
20
+ ],
21
+ );
@@ -0,0 +1,54 @@
1
+ import {
2
+ index,
3
+ integer,
4
+ jsonb,
5
+ pgTable,
6
+ text,
7
+ timestamp,
8
+ uniqueIndex,
9
+ uuid,
10
+ } from "drizzle-orm/pg-core";
11
+ import { timestamps } from "./_shared.js";
12
+ import { journeyStatusEnum } from "./enums.js";
13
+
14
+ export const journeyStates = pgTable(
15
+ "journey_states",
16
+ {
17
+ id: uuid("id").defaultRandom().primaryKey(),
18
+ organizationId: text("organization_id"),
19
+ userId: text("user_id").notNull(),
20
+ userEmail: text("user_email").notNull(),
21
+ journeyId: text("journey_id").notNull(),
22
+ currentNodeId: text("current_node_id").notNull(),
23
+ status: journeyStatusEnum("status").notNull().default("active"),
24
+ hatchetRunId: text("hatchet_run_id"),
25
+ context: jsonb("context").$type<Record<string, unknown>>().default({}),
26
+ errorMessage: text("error_message"),
27
+ entryCount: integer("entry_count").notNull().default(1),
28
+ completedAt: timestamp("completed_at", { withTimezone: true }),
29
+ exitedAt: timestamp("exited_at", { withTimezone: true }),
30
+ deletedAt: timestamp("deleted_at", { withTimezone: true }),
31
+ ...timestamps,
32
+ },
33
+ (table) => [
34
+ // NOTE: organizationId is intentionally NOT in this unique index yet. It is
35
+ // nullable (single-tenant today), and Postgres treats NULLs as DISTINCT in a
36
+ // unique index by default — so adding it now would silently stop enforcing
37
+ // one-active-journey-per-user for all existing rows. drizzle 0.45.2's
38
+ // uniqueIndex() can't express NULLS NOT DISTINCT. When multi-tenancy lands and
39
+ // organizationId is non-null, add it to this key (a cheap rebuild on this
40
+ // modest table). The nullable column is added now (the real cheap insurance).
41
+ uniqueIndex("uq_user_journey_active").on(
42
+ table.userId,
43
+ table.journeyId,
44
+ table.status,
45
+ ),
46
+ index("journey_states_status_idx").on(table.status),
47
+ index("journey_states_hatchet_run_idx").on(table.hatchetRunId),
48
+ index("journey_states_user_id_idx").on(table.userId),
49
+ index("journey_states_journey_id_status_idx").on(
50
+ table.journeyId,
51
+ table.status,
52
+ ),
53
+ ],
54
+ );
@@ -0,0 +1,21 @@
1
+ import { index, pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core";
2
+ import { trackedLinks } from "./tracked-links.js";
3
+
4
+ export const linkClicks = pgTable(
5
+ "link_clicks",
6
+ {
7
+ id: uuid("id").defaultRandom().primaryKey(),
8
+ trackedLinkId: uuid("tracked_link_id")
9
+ .notNull()
10
+ .references(() => trackedLinks.id, { onDelete: "cascade" }),
11
+ ipAddress: text("ip_address"),
12
+ userAgent: text("user_agent"),
13
+ clickedAt: timestamp("clicked_at", { withTimezone: true })
14
+ .defaultNow()
15
+ .notNull(),
16
+ },
17
+ (table) => [
18
+ index("link_clicks_tracked_link_id_idx").on(table.trackedLinkId),
19
+ index("link_clicks_clicked_at_idx").on(table.clickedAt),
20
+ ],
21
+ );
@@ -0,0 +1,160 @@
1
+ import { relations } from "drizzle-orm";
2
+ import { alertHistory } from "./alert-history.js";
3
+ import { alertRules } from "./alert-rules.js";
4
+ import { apiKeys } from "./api-keys.js";
5
+ import { auditLogs } from "./audit-logs.js";
6
+ import {
7
+ account,
8
+ invitation,
9
+ member,
10
+ organization,
11
+ session,
12
+ user,
13
+ } from "./auth.js";
14
+ import { contacts } from "./contacts.js";
15
+ import { deadLetterQueue } from "./dead-letter-queue.js";
16
+ import { emailPreferences } from "./email-preferences.js";
17
+ import { emailSends } from "./email-sends.js";
18
+ import { importJobs } from "./import-jobs.js";
19
+ import { journeyConfigs } from "./journey-configs.js";
20
+ import { journeyLogs } from "./journey-logs.js";
21
+ import { journeyStates } from "./journey-states.js";
22
+ import { linkClicks } from "./link-clicks.js";
23
+ import { trackedLinks } from "./tracked-links.js";
24
+ import { userEvents } from "./user-events.js";
25
+
26
+ export const alertRulesRelations = relations(alertRules, ({ many }) => ({
27
+ history: many(alertHistory),
28
+ }));
29
+
30
+ export const alertHistoryRelations = relations(alertHistory, ({ one }) => ({
31
+ rule: one(alertRules, {
32
+ fields: [alertHistory.alertRuleId],
33
+ references: [alertRules.id],
34
+ }),
35
+ }));
36
+
37
+ export const apiKeysRelations = relations(apiKeys, () => ({}));
38
+
39
+ export const auditLogsRelations = relations(auditLogs, () => ({}));
40
+
41
+ export const deadLetterQueueRelations = relations(deadLetterQueue, () => ({}));
42
+
43
+ export const importJobsRelations = relations(importJobs, () => ({}));
44
+
45
+ export const journeyConfigsRelations = relations(journeyConfigs, () => ({}));
46
+
47
+ export const contactsRelations = relations(contacts, ({ many }) => ({
48
+ emailPreferences: many(emailPreferences),
49
+ userEvents: many(userEvents),
50
+ journeyStates: many(journeyStates),
51
+ }));
52
+
53
+ export const emailPreferencesRelations = relations(
54
+ emailPreferences,
55
+ ({ one }) => ({
56
+ contact: one(contacts, {
57
+ fields: [emailPreferences.userId],
58
+ references: [contacts.externalId],
59
+ }),
60
+ }),
61
+ );
62
+
63
+ export const userEventsRelations = relations(userEvents, ({ one }) => ({
64
+ contact: one(contacts, {
65
+ fields: [userEvents.userId],
66
+ references: [contacts.externalId],
67
+ }),
68
+ }));
69
+
70
+ export const journeyStatesRelations = relations(
71
+ journeyStates,
72
+ ({ one, many }) => ({
73
+ contact: one(contacts, {
74
+ fields: [journeyStates.userId],
75
+ references: [contacts.externalId],
76
+ }),
77
+ logs: many(journeyLogs),
78
+ emailSends: many(emailSends),
79
+ }),
80
+ );
81
+
82
+ export const journeyLogsRelations = relations(journeyLogs, ({ one }) => ({
83
+ journeyState: one(journeyStates, {
84
+ fields: [journeyLogs.journeyStateId],
85
+ references: [journeyStates.id],
86
+ }),
87
+ }));
88
+
89
+ export const emailSendsRelations = relations(emailSends, ({ one, many }) => ({
90
+ journeyState: one(journeyStates, {
91
+ fields: [emailSends.journeyStateId],
92
+ references: [journeyStates.id],
93
+ }),
94
+ trackedLinks: many(trackedLinks),
95
+ }));
96
+
97
+ export const trackedLinksRelations = relations(
98
+ trackedLinks,
99
+ ({ one, many }) => ({
100
+ emailSend: one(emailSends, {
101
+ fields: [trackedLinks.emailSendId],
102
+ references: [emailSends.id],
103
+ }),
104
+ clicks: many(linkClicks),
105
+ }),
106
+ );
107
+
108
+ export const linkClicksRelations = relations(linkClicks, ({ one }) => ({
109
+ trackedLink: one(trackedLinks, {
110
+ fields: [linkClicks.trackedLinkId],
111
+ references: [trackedLinks.id],
112
+ }),
113
+ }));
114
+
115
+ export const userRelations = relations(user, ({ many }) => ({
116
+ sessions: many(session),
117
+ accounts: many(account),
118
+ memberships: many(member),
119
+ }));
120
+
121
+ export const sessionRelations = relations(session, ({ one }) => ({
122
+ user: one(user, {
123
+ fields: [session.userId],
124
+ references: [user.id],
125
+ }),
126
+ }));
127
+
128
+ export const accountRelations = relations(account, ({ one }) => ({
129
+ user: one(user, {
130
+ fields: [account.userId],
131
+ references: [user.id],
132
+ }),
133
+ }));
134
+
135
+ export const organizationRelations = relations(organization, ({ many }) => ({
136
+ members: many(member),
137
+ invitations: many(invitation),
138
+ }));
139
+
140
+ export const memberRelations = relations(member, ({ one }) => ({
141
+ organization: one(organization, {
142
+ fields: [member.organizationId],
143
+ references: [organization.id],
144
+ }),
145
+ user: one(user, {
146
+ fields: [member.userId],
147
+ references: [user.id],
148
+ }),
149
+ }));
150
+
151
+ export const invitationRelations = relations(invitation, ({ one }) => ({
152
+ organization: one(organization, {
153
+ fields: [invitation.organizationId],
154
+ references: [organization.id],
155
+ }),
156
+ inviter: one(user, {
157
+ fields: [invitation.inviterId],
158
+ references: [user.id],
159
+ }),
160
+ }));
@@ -0,0 +1,17 @@
1
+ import { index, integer, pgTable, text, uuid } from "drizzle-orm/pg-core";
2
+ import { timestamps } from "./_shared.js";
3
+ import { emailSends } from "./email-sends.js";
4
+
5
+ export const trackedLinks = pgTable(
6
+ "tracked_links",
7
+ {
8
+ id: uuid("id").defaultRandom().primaryKey(),
9
+ emailSendId: uuid("email_send_id")
10
+ .notNull()
11
+ .references(() => emailSends.id, { onDelete: "cascade" }),
12
+ originalUrl: text("original_url").notNull(),
13
+ clickCount: integer("click_count").notNull().default(0),
14
+ ...timestamps,
15
+ },
16
+ (table) => [index("tracked_links_email_send_id_idx").on(table.emailSendId)],
17
+ );
@@ -0,0 +1,35 @@
1
+ import {
2
+ index,
3
+ jsonb,
4
+ pgTable,
5
+ text,
6
+ timestamp,
7
+ uniqueIndex,
8
+ uuid,
9
+ } from "drizzle-orm/pg-core";
10
+
11
+ export const userEvents = pgTable(
12
+ "user_events",
13
+ {
14
+ id: uuid("id").defaultRandom().primaryKey(),
15
+ organizationId: text("organization_id"),
16
+ userId: text("user_id").notNull(),
17
+ event: text("event").notNull(),
18
+ properties: jsonb("properties").$type<Record<string, unknown>>(),
19
+ idempotencyKey: text("idempotency_key"),
20
+ occurredAt: timestamp("occurred_at", { withTimezone: true })
21
+ .defaultNow()
22
+ .notNull(),
23
+ },
24
+ (table) => [
25
+ index("user_events_user_id_idx").on(table.userId),
26
+ index("user_events_event_idx").on(table.event),
27
+ index("user_events_occurred_at_idx").on(table.occurredAt),
28
+ index("user_events_user_event_occurred_idx").on(
29
+ table.userId,
30
+ table.event,
31
+ table.occurredAt,
32
+ ),
33
+ uniqueIndex("user_events_idempotency_key_idx").on(table.idempotencyKey),
34
+ ],
35
+ );
package/src/seed.ts ADDED
@@ -0,0 +1,91 @@
1
+ import { sql } from "drizzle-orm";
2
+ import { drizzle } from "drizzle-orm/postgres-js";
3
+ import postgres from "postgres";
4
+ import * as schema from "./schema/index.js";
5
+
6
+ const databaseUrl = process.env.DATABASE_URL;
7
+ if (!databaseUrl) {
8
+ console.error("DATABASE_URL environment variable is required");
9
+ process.exit(1);
10
+ }
11
+
12
+ const client = postgres(databaseUrl, { max: 1 });
13
+ const db = drizzle(client, { schema });
14
+
15
+ async function seed() {
16
+ console.log("Seeding database...");
17
+
18
+ const demoUserId = "seed-user-001";
19
+ const demoOrgId = "seed-org-001";
20
+
21
+ await db
22
+ .insert(schema.user)
23
+ .values({
24
+ id: demoUserId,
25
+ name: "Demo User",
26
+ email: "demo@hogsend.dev",
27
+ emailVerified: true,
28
+ })
29
+ .onConflictDoNothing();
30
+
31
+ await db
32
+ .insert(schema.organization)
33
+ .values({
34
+ id: demoOrgId,
35
+ name: "Hogsend Demo",
36
+ slug: "hogsend-demo",
37
+ })
38
+ .onConflictDoNothing();
39
+
40
+ await db
41
+ .insert(schema.member)
42
+ .values({
43
+ id: "seed-member-001",
44
+ organizationId: demoOrgId,
45
+ userId: demoUserId,
46
+ role: "owner",
47
+ })
48
+ .onConflictDoNothing();
49
+
50
+ await db
51
+ .insert(schema.contacts)
52
+ .values({
53
+ externalId: demoUserId,
54
+ email: "demo@hogsend.dev",
55
+ properties: { plan: "free", name: "Demo User" },
56
+ })
57
+ .onConflictDoNothing();
58
+
59
+ await db
60
+ .insert(schema.emailPreferences)
61
+ .values({
62
+ userId: demoUserId,
63
+ email: "demo@hogsend.dev",
64
+ unsubscribedAll: false,
65
+ categories: {},
66
+ })
67
+ .onConflictDoNothing();
68
+
69
+ await db
70
+ .delete(schema.userEvents)
71
+ .where(sql`${schema.userEvents.properties}->>'source' = 'seed'`);
72
+
73
+ await db.insert(schema.userEvents).values([
74
+ {
75
+ userId: demoUserId,
76
+ event: "user.created",
77
+ properties: { plan: "free", source: "seed" },
78
+ },
79
+ {
80
+ userId: demoUserId,
81
+ event: "feature.used",
82
+ properties: { feature: "dashboard", source: "seed" },
83
+ },
84
+ ]);
85
+
86
+ console.log("Seeding complete.");
87
+ }
88
+
89
+ await seed();
90
+ await client.end();
91
+ process.exit(0);