@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.
- package/LICENSE +93 -0
- package/README.md +14 -0
- package/drizzle/0000_nifty_songbird.sql +188 -0
- package/drizzle/0001_minor_shockwave.sql +13 -0
- package/drizzle/0002_early_owl.sql +1 -0
- package/drizzle/0003_bizarre_annihilus.sql +9 -0
- package/drizzle/0004_brave_betty_brant.sql +1 -0
- package/drizzle/0005_groovy_princess_powerful.sql +8 -0
- package/drizzle/0006_groovy_charles_xavier.sql +100 -0
- package/drizzle/0007_serious_captain_universe.sql +1 -0
- package/drizzle/0008_demonic_agent_brand.sql +5 -0
- package/drizzle/meta/0000_snapshot.json +1264 -0
- package/drizzle/meta/0001_snapshot.json +1353 -0
- package/drizzle/meta/0002_snapshot.json +1380 -0
- package/drizzle/meta/0003_snapshot.json +1443 -0
- package/drizzle/meta/0004_snapshot.json +1464 -0
- package/drizzle/meta/0005_snapshot.json +1588 -0
- package/drizzle/meta/0006_snapshot.json +2331 -0
- package/drizzle/meta/0007_snapshot.json +2346 -0
- package/drizzle/meta/0008_snapshot.json +2449 -0
- package/drizzle/meta/_journal.json +69 -0
- package/package.json +49 -0
- package/src/index.ts +35 -0
- package/src/migrate-client.ts +56 -0
- package/src/migrate.ts +173 -0
- package/src/schema/_shared.ts +10 -0
- package/src/schema/alert-history.ts +21 -0
- package/src/schema/alert-rules.ts +36 -0
- package/src/schema/api-keys.ts +30 -0
- package/src/schema/audit-logs.ts +22 -0
- package/src/schema/auth.ts +89 -0
- package/src/schema/contacts.ts +31 -0
- package/src/schema/dead-letter-queue.ts +31 -0
- package/src/schema/email-preferences.ts +35 -0
- package/src/schema/email-sends.ts +34 -0
- package/src/schema/enums.ts +47 -0
- package/src/schema/import-jobs.ts +26 -0
- package/src/schema/index.ts +18 -0
- package/src/schema/journey-configs.ts +15 -0
- package/src/schema/journey-logs.ts +21 -0
- package/src/schema/journey-states.ts +54 -0
- package/src/schema/link-clicks.ts +21 -0
- package/src/schema/relations.ts +160 -0
- package/src/schema/tracked-links.ts +17 -0
- package/src/schema/user-events.ts +35 -0
- package/src/seed.ts +91 -0
- 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
|
+
);
|