@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,69 @@
1
+ {
2
+ "version": "7",
3
+ "dialect": "postgresql",
4
+ "entries": [
5
+ {
6
+ "idx": 0,
7
+ "version": "7",
8
+ "when": 1779641370049,
9
+ "tag": "0000_nifty_songbird",
10
+ "breakpoints": true
11
+ },
12
+ {
13
+ "idx": 1,
14
+ "version": "7",
15
+ "when": 1779658680434,
16
+ "tag": "0001_minor_shockwave",
17
+ "breakpoints": true
18
+ },
19
+ {
20
+ "idx": 2,
21
+ "version": "7",
22
+ "when": 1779716216920,
23
+ "tag": "0002_early_owl",
24
+ "breakpoints": true
25
+ },
26
+ {
27
+ "idx": 3,
28
+ "version": "7",
29
+ "when": 1779720417749,
30
+ "tag": "0003_bizarre_annihilus",
31
+ "breakpoints": true
32
+ },
33
+ {
34
+ "idx": 4,
35
+ "version": "7",
36
+ "when": 1779720793723,
37
+ "tag": "0004_brave_betty_brant",
38
+ "breakpoints": true
39
+ },
40
+ {
41
+ "idx": 5,
42
+ "version": "7",
43
+ "when": 1779722442726,
44
+ "tag": "0005_groovy_princess_powerful",
45
+ "breakpoints": true
46
+ },
47
+ {
48
+ "idx": 6,
49
+ "version": "7",
50
+ "when": 1779731629096,
51
+ "tag": "0006_groovy_charles_xavier",
52
+ "breakpoints": true
53
+ },
54
+ {
55
+ "idx": 7,
56
+ "version": "7",
57
+ "when": 1779788398665,
58
+ "tag": "0007_serious_captain_universe",
59
+ "breakpoints": true
60
+ },
61
+ {
62
+ "idx": 8,
63
+ "version": "7",
64
+ "when": 1780308366878,
65
+ "tag": "0008_demonic_agent_brand",
66
+ "breakpoints": true
67
+ }
68
+ ]
69
+ }
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@hogsend/db",
3
+ "version": "0.0.1",
4
+ "type": "module",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/dougwithseismic/hogsend.git",
9
+ "directory": "packages/db"
10
+ },
11
+ "sideEffects": false,
12
+ "main": "./src/index.ts",
13
+ "types": "./src/index.ts",
14
+ "exports": {
15
+ ".": "./src/index.ts",
16
+ "./schema": "./src/schema/index.ts"
17
+ },
18
+ "files": [
19
+ "src",
20
+ "drizzle",
21
+ "README.md"
22
+ ],
23
+ "publishConfig": {
24
+ "access": "public"
25
+ },
26
+ "peerDependencies": {
27
+ "drizzle-orm": ">=0.45.0"
28
+ },
29
+ "dependencies": {
30
+ "drizzle-orm": "^0.45.2",
31
+ "postgres": "^3.4.9"
32
+ },
33
+ "devDependencies": {
34
+ "@types/node": "^25.9.1",
35
+ "drizzle-kit": "^0.31.10",
36
+ "tsx": "^4.22.3",
37
+ "@repo/typescript-config": "^0.0.0"
38
+ },
39
+ "scripts": {
40
+ "db:generate": "drizzle-kit generate",
41
+ "db:migrate": "tsx src/migrate.ts",
42
+ "db:migrate:client": "tsx src/migrate-client.ts",
43
+ "db:push": "drizzle-kit push",
44
+ "db:studio": "drizzle-kit studio",
45
+ "db:pull": "drizzle-kit pull",
46
+ "db:seed": "tsx src/seed.ts",
47
+ "check-types": "tsc --noEmit"
48
+ }
49
+ }
package/src/index.ts ADDED
@@ -0,0 +1,35 @@
1
+ import { drizzle } from "drizzle-orm/postgres-js";
2
+ import postgres from "postgres";
3
+ import * as schema from "./schema/index.js";
4
+
5
+ export function createDatabase(opts: { url: string }) {
6
+ const client = postgres(opts.url, {
7
+ max: 10,
8
+ idle_timeout: 20,
9
+ connect_timeout: 10,
10
+ });
11
+
12
+ const db = drizzle(client, { schema });
13
+
14
+ return { db, client };
15
+ }
16
+
17
+ export type Database = ReturnType<typeof createDatabase>["db"];
18
+ export type DatabaseClient = ReturnType<typeof postgres>;
19
+
20
+ export { migrateClient, migrateEngine, migrateTrack } from "./migrate.js";
21
+ export * from "./schema/index.js";
22
+ export {
23
+ CLIENT_MIGRATIONS_SCHEMA,
24
+ CLIENT_MIGRATIONS_TABLE,
25
+ ENGINE_MIGRATIONS_SCHEMA,
26
+ ENGINE_MIGRATIONS_TABLE,
27
+ getBundledMigrations,
28
+ getClientSchemaVersion,
29
+ getEngineSchemaVersion,
30
+ getSchemaVersion,
31
+ type JournalShape,
32
+ type MigrationEntry,
33
+ type SchemaVersion,
34
+ } from "./version.js";
35
+ export { schema };
@@ -0,0 +1,56 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { migrateClient } from "./migrate.js";
4
+ import type { JournalShape } from "./version.js";
5
+
6
+ // Client-track migrate CLI shim. Reads the client repo's migrations folder from
7
+ // `CLIENT_MIGRATIONS_FOLDER` (set per service to the client repo's `migrations`
8
+ // dir), loads its journal, and applies it into `drizzle.__client_migrations`.
9
+ //
10
+ // Skips gracefully when the folder is unset/absent/empty so the same railway
11
+ // `preDeployCommand` works for this dogfood repo (no client migrations) and for
12
+ // scaffolded client repos.
13
+ async function run(): Promise<void> {
14
+ const databaseUrl = process.env.DATABASE_URL;
15
+ if (!databaseUrl) {
16
+ console.error("DATABASE_URL environment variable is required");
17
+ process.exit(1);
18
+ }
19
+
20
+ const folder = process.env.CLIENT_MIGRATIONS_FOLDER;
21
+ if (!folder) {
22
+ console.log(
23
+ "[client] CLIENT_MIGRATIONS_FOLDER not set — no client migrations to apply, skipping.",
24
+ );
25
+ return;
26
+ }
27
+ if (!existsSync(folder)) {
28
+ console.log(`[client] Migrations folder ${folder} not found — skipping.`);
29
+ return;
30
+ }
31
+
32
+ const journalPath = join(folder, "meta", "_journal.json");
33
+ if (!existsSync(journalPath)) {
34
+ console.log(
35
+ `[client] No meta/_journal.json in ${folder} — empty client track, skipping.`,
36
+ );
37
+ return;
38
+ }
39
+
40
+ const journal = JSON.parse(readFileSync(journalPath, "utf8")) as JournalShape;
41
+ if (!journal.entries || journal.entries.length === 0) {
42
+ console.log("[client] Empty journal — nothing to apply, skipping.");
43
+ return;
44
+ }
45
+
46
+ await migrateClient(databaseUrl, folder, journal);
47
+ }
48
+
49
+ run()
50
+ .then(() => {
51
+ process.exit(0);
52
+ })
53
+ .catch((err) => {
54
+ console.error("Client migration failed:", err);
55
+ process.exit(1);
56
+ });
package/src/migrate.ts ADDED
@@ -0,0 +1,173 @@
1
+ import { existsSync } from "node:fs";
2
+ import { pathToFileURL } from "node:url";
3
+ import { drizzle } from "drizzle-orm/postgres-js";
4
+ import { migrate } from "drizzle-orm/postgres-js/migrator";
5
+ import postgres from "postgres";
6
+ import {
7
+ CLIENT_MIGRATIONS_SCHEMA,
8
+ CLIENT_MIGRATIONS_TABLE,
9
+ ENGINE_MIGRATIONS_SCHEMA,
10
+ ENGINE_MIGRATIONS_TABLE,
11
+ getClientSchemaVersion,
12
+ getEngineSchemaVersion,
13
+ type JournalShape,
14
+ type SchemaVersion,
15
+ } from "./version.js";
16
+
17
+ // Stable advisory-lock key so two concurrent deploys / replicas can never run
18
+ // migrations at the same time — the second blocks on the lock. Both tracks use
19
+ // the same key: a client migrate cannot race an engine migrate on the same DB
20
+ // (the desired serialization, not a bug), and sequential engine-then-client
21
+ // calls each acquire+release so there is no deadlock.
22
+ const ADVISORY_LOCK_KEY = 4812007;
23
+
24
+ type Db = ReturnType<typeof drizzle>;
25
+ type Client = ReturnType<typeof postgres>;
26
+
27
+ export interface MigrateTrackOptions {
28
+ /** Already-constructed postgres-js client (max:1, idle_timeout:0). */
29
+ client: Client;
30
+ /** drizzle(client) instance bound to that client. */
31
+ db: Db;
32
+ migrationsFolder: string;
33
+ migrationsTable: string;
34
+ migrationsSchema: string;
35
+ /** Per-track version probe so logging reports the right pending set. */
36
+ version: (db: Db) => Promise<SchemaVersion>;
37
+ /** Label for log lines, e.g. "engine" / "client". */
38
+ label: string;
39
+ }
40
+
41
+ /**
42
+ * Apply one migration track under the shared advisory lock + statement guards.
43
+ * Generalized from the original single-track `run()`.
44
+ */
45
+ export async function migrateTrack(opts: MigrateTrackOptions): Promise<void> {
46
+ const { client, db, migrationsFolder, migrationsTable, migrationsSchema } =
47
+ opts;
48
+
49
+ // Fail fast instead of queueing forever behind a lock on a busy table; a
50
+ // migration that can't get its lock in 10s is safer aborted and retried.
51
+ await client`SET lock_timeout = '10s'`;
52
+ // Cap any single statement. Long-running DDL or bulk UPDATEs against a live
53
+ // table belong in a Hatchet backfill job, not in a migration (UPGRADING.md).
54
+ await client`SET statement_timeout = '15min'`;
55
+
56
+ // Serialize migrations across concurrent deploys / multiple replicas.
57
+ await client`SELECT pg_advisory_lock(${ADVISORY_LOCK_KEY})`;
58
+ try {
59
+ const before = await opts.version(db);
60
+ if (before.inSync) {
61
+ console.log(
62
+ `[${opts.label}] Schema already up to date at ${before.applied ?? "(empty)"} — nothing to apply.`,
63
+ );
64
+ return;
65
+ }
66
+ console.log(
67
+ `[${opts.label}] Applying ${before.pending.length} migration(s): ${before.pending.join(", ")}`,
68
+ );
69
+ await migrate(db, { migrationsFolder, migrationsTable, migrationsSchema });
70
+ const after = await opts.version(db);
71
+ console.log(
72
+ `[${opts.label}] Migrations complete. Schema now at ${after.applied}.`,
73
+ );
74
+ } finally {
75
+ await client`SELECT pg_advisory_unlock(${ADVISORY_LOCK_KEY})`;
76
+ }
77
+ }
78
+
79
+ function createMigrateClient(databaseUrl: string): { client: Client; db: Db } {
80
+ // Single dedicated connection. `idle_timeout: 0` keeps it from dropping
81
+ // mid-run.
82
+ const client = postgres(databaseUrl, {
83
+ max: 1,
84
+ idle_timeout: 0,
85
+ connection: { application_name: "hogsend-migrate" },
86
+ });
87
+ return { client, db: drizzle(client) };
88
+ }
89
+
90
+ /** Engine track: bundled `@hogsend/db/drizzle` folder + default ledger. */
91
+ export async function migrateEngine(databaseUrl: string): Promise<void> {
92
+ const migrationsFolder = new URL("../drizzle", import.meta.url).pathname;
93
+ if (!existsSync(migrationsFolder)) {
94
+ console.log("[engine] No migrations folder found, skipping.");
95
+ return;
96
+ }
97
+ const { client, db } = createMigrateClient(databaseUrl);
98
+ try {
99
+ await migrateTrack({
100
+ client,
101
+ db,
102
+ migrationsFolder,
103
+ migrationsTable: ENGINE_MIGRATIONS_TABLE,
104
+ migrationsSchema: ENGINE_MIGRATIONS_SCHEMA,
105
+ version: getEngineSchemaVersion,
106
+ label: "engine",
107
+ });
108
+ } finally {
109
+ await client.end();
110
+ }
111
+ }
112
+
113
+ /**
114
+ * Client track: the caller's migrations folder + `__client_migrations` ledger.
115
+ * The caller also supplies the client journal so the version probe can
116
+ * short-circuit on an already-in-sync ledger.
117
+ */
118
+ export async function migrateClient(
119
+ databaseUrl: string,
120
+ migrationsFolder: string,
121
+ journal: JournalShape,
122
+ ): Promise<void> {
123
+ if (!existsSync(migrationsFolder)) {
124
+ console.log("[client] No migrations folder found, skipping.");
125
+ return;
126
+ }
127
+ const { client, db } = createMigrateClient(databaseUrl);
128
+ try {
129
+ await migrateTrack({
130
+ client,
131
+ db,
132
+ migrationsFolder,
133
+ migrationsTable: CLIENT_MIGRATIONS_TABLE,
134
+ migrationsSchema: CLIENT_MIGRATIONS_SCHEMA,
135
+ version: (d) => getClientSchemaVersion(d, journal),
136
+ label: "client",
137
+ });
138
+ } finally {
139
+ await client.end();
140
+ }
141
+ }
142
+
143
+ // --- CLI entrypoint (the `db:migrate` script) -----------------------------
144
+ //
145
+ // Engine only. `@hogsend/db` has no knowledge of any client repo's migrations
146
+ // folder, so the client track is invoked separately via `migrate-client.ts`
147
+ // (the `db:migrate:client` script).
148
+ async function run(): Promise<void> {
149
+ const databaseUrl = process.env.DATABASE_URL;
150
+ if (!databaseUrl) {
151
+ console.error("DATABASE_URL environment variable is required");
152
+ process.exit(1);
153
+ }
154
+ await migrateEngine(databaseUrl);
155
+ }
156
+
157
+ // Only run when invoked directly (e.g. `tsx src/migrate.ts`). Guard so that
158
+ // re-exporting `migrateEngine`/`migrateClient` from `index.ts` does not fire a
159
+ // real migration + `process.exit` at import time.
160
+ const invokedDirectly =
161
+ process.argv[1] !== undefined &&
162
+ import.meta.url === pathToFileURL(process.argv[1]).href;
163
+
164
+ if (invokedDirectly) {
165
+ run()
166
+ .then(() => {
167
+ process.exit(0);
168
+ })
169
+ .catch((err) => {
170
+ console.error("Migration failed:", err);
171
+ process.exit(1);
172
+ });
173
+ }
@@ -0,0 +1,10 @@
1
+ import { timestamp } from "drizzle-orm/pg-core";
2
+
3
+ export const timestamps = {
4
+ createdAt: timestamp("created_at", { withTimezone: true })
5
+ .defaultNow()
6
+ .notNull(),
7
+ updatedAt: timestamp("updated_at", { withTimezone: true })
8
+ .defaultNow()
9
+ .notNull(),
10
+ };
@@ -0,0 +1,21 @@
1
+ import { index, jsonb, pgTable, text, uuid } from "drizzle-orm/pg-core";
2
+ import { timestamps } from "./_shared.js";
3
+ import { alertRules } from "./alert-rules.js";
4
+
5
+ export const alertHistory = pgTable(
6
+ "alert_history",
7
+ {
8
+ id: uuid("id").defaultRandom().primaryKey(),
9
+ alertRuleId: uuid("alert_rule_id")
10
+ .notNull()
11
+ .references(() => alertRules.id),
12
+ payload: jsonb("payload").$type<Record<string, unknown>>(),
13
+ deliveryStatus: text("delivery_status").notNull(),
14
+ error: text("error"),
15
+ ...timestamps,
16
+ },
17
+ (table) => [
18
+ index("alert_history_rule_id_idx").on(table.alertRuleId),
19
+ index("alert_history_created_at_idx").on(table.createdAt),
20
+ ],
21
+ );
@@ -0,0 +1,36 @@
1
+ import {
2
+ boolean,
3
+ index,
4
+ integer,
5
+ jsonb,
6
+ pgTable,
7
+ text,
8
+ timestamp,
9
+ uuid,
10
+ } from "drizzle-orm/pg-core";
11
+ import { timestamps } from "./_shared.js";
12
+ import { alertChannelEnum, alertRuleTypeEnum } from "./enums.js";
13
+
14
+ export const alertRules = pgTable(
15
+ "alert_rules",
16
+ {
17
+ id: uuid("id").defaultRandom().primaryKey(),
18
+ name: text("name").notNull(),
19
+ type: alertRuleTypeEnum("type").notNull(),
20
+ threshold: jsonb("threshold").$type<Record<string, number>>().notNull(),
21
+ channel: alertChannelEnum("channel").notNull(),
22
+ channelConfig: jsonb("channel_config")
23
+ .$type<Record<string, string>>()
24
+ .notNull(),
25
+ enabled: boolean("enabled").notNull().default(true),
26
+ cooldownMinutes: integer("cooldown_minutes").notNull().default(60),
27
+ lastFiredAt: timestamp("last_fired_at", {
28
+ withTimezone: true,
29
+ }),
30
+ ...timestamps,
31
+ },
32
+ (table) => [
33
+ index("alert_rules_type_idx").on(table.type),
34
+ index("alert_rules_enabled_idx").on(table.enabled),
35
+ ],
36
+ );
@@ -0,0 +1,30 @@
1
+ import {
2
+ index,
3
+ jsonb,
4
+ pgTable,
5
+ text,
6
+ timestamp,
7
+ uuid,
8
+ } from "drizzle-orm/pg-core";
9
+ import { timestamps } from "./_shared.js";
10
+
11
+ export const apiKeys = pgTable(
12
+ "api_keys",
13
+ {
14
+ id: uuid("id").defaultRandom().primaryKey(),
15
+ organizationId: text("organization_id"),
16
+ name: text("name").notNull(),
17
+ keyPrefix: text("key_prefix").notNull(),
18
+ keyHash: text("key_hash").notNull().unique(),
19
+ scopes: jsonb("scopes").$type<string[]>().notNull().default(["read"]),
20
+ createdBy: text("created_by"),
21
+ lastUsedAt: timestamp("last_used_at", { withTimezone: true }),
22
+ revokedAt: timestamp("revoked_at", { withTimezone: true }),
23
+ expiresAt: timestamp("expires_at", { withTimezone: true }),
24
+ ...timestamps,
25
+ },
26
+ (table) => [
27
+ index("api_keys_key_hash_idx").on(table.keyHash),
28
+ index("api_keys_revoked_at_idx").on(table.revokedAt),
29
+ ],
30
+ );
@@ -0,0 +1,22 @@
1
+ import { index, jsonb, pgTable, text, uuid } from "drizzle-orm/pg-core";
2
+ import { timestamps } from "./_shared.js";
3
+
4
+ export const auditLogs = pgTable(
5
+ "audit_logs",
6
+ {
7
+ id: uuid("id").defaultRandom().primaryKey(),
8
+ actor: text("actor").notNull(),
9
+ actorKeyId: uuid("actor_key_id"),
10
+ action: text("action").notNull(),
11
+ resource: text("resource").notNull(),
12
+ resourceId: text("resource_id"),
13
+ detail: jsonb("detail").$type<Record<string, unknown>>(),
14
+ ipAddress: text("ip_address"),
15
+ ...timestamps,
16
+ },
17
+ (table) => [
18
+ index("audit_logs_actor_idx").on(table.actor),
19
+ index("audit_logs_resource_idx").on(table.resource, table.resourceId),
20
+ index("audit_logs_created_at_idx").on(table.createdAt),
21
+ ],
22
+ );
@@ -0,0 +1,89 @@
1
+ import { boolean, pgTable, text, timestamp } from "drizzle-orm/pg-core";
2
+ import { timestamps } from "./_shared.js";
3
+
4
+ export const user = pgTable("user", {
5
+ id: text("id").primaryKey(),
6
+ name: text("name").notNull(),
7
+ email: text("email").notNull().unique(),
8
+ emailVerified: boolean("email_verified").notNull().default(false),
9
+ image: text("image"),
10
+ ...timestamps,
11
+ });
12
+
13
+ export const session = pgTable("session", {
14
+ id: text("id").primaryKey(),
15
+ expiresAt: timestamp("expires_at", { withTimezone: true }).notNull(),
16
+ token: text("token").notNull().unique(),
17
+ ipAddress: text("ip_address"),
18
+ userAgent: text("user_agent"),
19
+ userId: text("user_id")
20
+ .notNull()
21
+ .references(() => user.id, { onDelete: "cascade" }),
22
+ activeOrganizationId: text("active_organization_id"),
23
+ ...timestamps,
24
+ });
25
+
26
+ export const account = pgTable("account", {
27
+ id: text("id").primaryKey(),
28
+ accountId: text("account_id").notNull(),
29
+ providerId: text("provider_id").notNull(),
30
+ userId: text("user_id")
31
+ .notNull()
32
+ .references(() => user.id, { onDelete: "cascade" }),
33
+ accessToken: text("access_token"),
34
+ refreshToken: text("refresh_token"),
35
+ idToken: text("id_token"),
36
+ accessTokenExpiresAt: timestamp("access_token_expires_at", {
37
+ withTimezone: true,
38
+ }),
39
+ refreshTokenExpiresAt: timestamp("refresh_token_expires_at", {
40
+ withTimezone: true,
41
+ }),
42
+ scope: text("scope"),
43
+ password: text("password"),
44
+ ...timestamps,
45
+ });
46
+
47
+ export const verification = pgTable("verification", {
48
+ id: text("id").primaryKey(),
49
+ identifier: text("identifier").notNull(),
50
+ value: text("value").notNull(),
51
+ expiresAt: timestamp("expires_at", { withTimezone: true }).notNull(),
52
+ ...timestamps,
53
+ });
54
+
55
+ export const organization = pgTable("organization", {
56
+ id: text("id").primaryKey(),
57
+ name: text("name").notNull(),
58
+ slug: text("slug").unique(),
59
+ logo: text("logo"),
60
+ metadata: text("metadata"),
61
+ ...timestamps,
62
+ });
63
+
64
+ export const member = pgTable("member", {
65
+ id: text("id").primaryKey(),
66
+ organizationId: text("organization_id")
67
+ .notNull()
68
+ .references(() => organization.id, { onDelete: "cascade" }),
69
+ userId: text("user_id")
70
+ .notNull()
71
+ .references(() => user.id, { onDelete: "cascade" }),
72
+ role: text("role").notNull().default("member"),
73
+ ...timestamps,
74
+ });
75
+
76
+ export const invitation = pgTable("invitation", {
77
+ id: text("id").primaryKey(),
78
+ organizationId: text("organization_id")
79
+ .notNull()
80
+ .references(() => organization.id, { onDelete: "cascade" }),
81
+ email: text("email").notNull(),
82
+ role: text("role"),
83
+ status: text("status").notNull().default("pending"),
84
+ expiresAt: timestamp("expires_at", { withTimezone: true }).notNull(),
85
+ inviterId: text("inviter_id")
86
+ .notNull()
87
+ .references(() => user.id, { onDelete: "cascade" }),
88
+ ...timestamps,
89
+ });
@@ -0,0 +1,31 @@
1
+ import {
2
+ index,
3
+ jsonb,
4
+ pgTable,
5
+ text,
6
+ timestamp,
7
+ uuid,
8
+ } from "drizzle-orm/pg-core";
9
+ import { timestamps } from "./_shared.js";
10
+
11
+ export const contacts = pgTable(
12
+ "contacts",
13
+ {
14
+ id: uuid("id").defaultRandom().primaryKey(),
15
+ organizationId: text("organization_id"),
16
+ externalId: text("external_id").notNull().unique(),
17
+ email: text("email"),
18
+ properties: jsonb("properties")
19
+ .$type<Record<string, unknown>>()
20
+ .default({}),
21
+ firstSeenAt: timestamp("first_seen_at", { withTimezone: true })
22
+ .defaultNow()
23
+ .notNull(),
24
+ lastSeenAt: timestamp("last_seen_at", { withTimezone: true })
25
+ .defaultNow()
26
+ .notNull(),
27
+ deletedAt: timestamp("deleted_at", { withTimezone: true }),
28
+ ...timestamps,
29
+ },
30
+ (table) => [index("contacts_email_idx").on(table.email)],
31
+ );
@@ -0,0 +1,31 @@
1
+ import {
2
+ index,
3
+ integer,
4
+ jsonb,
5
+ pgTable,
6
+ text,
7
+ timestamp,
8
+ uuid,
9
+ } from "drizzle-orm/pg-core";
10
+ import { timestamps } from "./_shared.js";
11
+ import { dlqStatusEnum } from "./enums.js";
12
+
13
+ export const deadLetterQueue = pgTable(
14
+ "dead_letter_queue",
15
+ {
16
+ id: uuid("id").defaultRandom().primaryKey(),
17
+ source: text("source").notNull(),
18
+ sourceId: text("source_id"),
19
+ payload: jsonb("payload").$type<Record<string, unknown>>().notNull(),
20
+ error: text("error").notNull(),
21
+ retryCount: integer("retry_count").notNull().default(0),
22
+ status: dlqStatusEnum("status").notNull().default("pending"),
23
+ retriedAt: timestamp("retried_at", { withTimezone: true }),
24
+ ...timestamps,
25
+ },
26
+ (table) => [
27
+ index("dlq_source_idx").on(table.source),
28
+ index("dlq_status_idx").on(table.status),
29
+ index("dlq_created_at_idx").on(table.createdAt),
30
+ ],
31
+ );