feat(db): add drizzle schema for all tables + initial migration
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
09a9f5f348
commit
fa4970a76c
15
packages/db/drizzle.config.ts
Normal file
15
packages/db/drizzle.config.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { defineConfig } from "drizzle-kit";
|
||||
|
||||
const databaseUrl = process.env.DATABASE_URL;
|
||||
if (!databaseUrl) {
|
||||
throw new Error("DATABASE_URL must be set when running drizzle-kit");
|
||||
}
|
||||
|
||||
export default defineConfig({
|
||||
schema: "./src/schema.ts",
|
||||
out: "./migrations",
|
||||
dialect: "postgresql",
|
||||
dbCredentials: { url: databaseUrl },
|
||||
strict: true,
|
||||
verbose: true,
|
||||
});
|
||||
201
packages/db/migrations/0000_conscious_tarantula.sql
Normal file
201
packages/db/migrations/0000_conscious_tarantula.sql
Normal file
@ -0,0 +1,201 @@
|
||||
CREATE TABLE IF NOT EXISTS "audit_log" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"operator_id" uuid,
|
||||
"source" text NOT NULL,
|
||||
"action" text NOT NULL,
|
||||
"target_type" text,
|
||||
"target_id" uuid,
|
||||
"payload" jsonb DEFAULT '{}'::jsonb NOT NULL,
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE IF NOT EXISTS "auth_sessions" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"operator_id" uuid NOT NULL,
|
||||
"token_hash" text NOT NULL,
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"expires_at" timestamp with time zone NOT NULL,
|
||||
"last_used_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"ip_address" "inet",
|
||||
"user_agent" text,
|
||||
CONSTRAINT "auth_sessions_token_hash_unique" UNIQUE("token_hash")
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE IF NOT EXISTS "media_files" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"operator_id" uuid NOT NULL,
|
||||
"filename_original" text NOT NULL,
|
||||
"mime_type" text NOT NULL,
|
||||
"size_bytes" bigint NOT NULL,
|
||||
"sha256" text NOT NULL,
|
||||
"storage_path" text NOT NULL,
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE IF NOT EXISTS "operators" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"telegram_user_id" bigint NOT NULL,
|
||||
"display_name" text NOT NULL,
|
||||
"role" text DEFAULT 'admin' NOT NULL,
|
||||
"default_timezone" text DEFAULT 'Asia/Kuala_Lumpur' NOT NULL,
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE IF NOT EXISTS "reminder_messages" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"reminder_id" uuid NOT NULL,
|
||||
"position" integer NOT NULL,
|
||||
"kind" text NOT NULL,
|
||||
"text_content" text,
|
||||
"media_id" uuid
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE IF NOT EXISTS "reminder_run_targets" (
|
||||
"run_id" uuid NOT NULL,
|
||||
"group_id" uuid NOT NULL,
|
||||
"status" text NOT NULL,
|
||||
"wa_message_id" text,
|
||||
"error" text,
|
||||
"latency_ms" integer,
|
||||
CONSTRAINT "reminder_run_targets_run_id_group_id_pk" PRIMARY KEY("run_id","group_id")
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE IF NOT EXISTS "reminder_runs" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"reminder_id" uuid NOT NULL,
|
||||
"fired_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"status" text NOT NULL,
|
||||
"error_summary" text
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE IF NOT EXISTS "reminder_targets" (
|
||||
"reminder_id" uuid NOT NULL,
|
||||
"group_id" uuid NOT NULL,
|
||||
"position" integer DEFAULT 0 NOT NULL,
|
||||
CONSTRAINT "reminder_targets_reminder_id_group_id_pk" PRIMARY KEY("reminder_id","group_id")
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE IF NOT EXISTS "reminders" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"account_id" uuid NOT NULL,
|
||||
"name" text NOT NULL,
|
||||
"schedule_kind" text NOT NULL,
|
||||
"scheduled_at" timestamp with time zone,
|
||||
"rrule" text,
|
||||
"timezone" text NOT NULL,
|
||||
"ends_at" timestamp with time zone,
|
||||
"max_runs" integer,
|
||||
"status" text DEFAULT 'active' NOT NULL,
|
||||
"created_by" uuid NOT NULL,
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE IF NOT EXISTS "whatsapp_accounts" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"operator_id" uuid NOT NULL,
|
||||
"label" text NOT NULL,
|
||||
"phone_number" text,
|
||||
"status" text DEFAULT 'pending' NOT NULL,
|
||||
"last_connected_at" timestamp with time zone,
|
||||
"last_qr_at" timestamp with time zone,
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE IF NOT EXISTS "whatsapp_groups" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"account_id" uuid NOT NULL,
|
||||
"wa_group_jid" text NOT NULL,
|
||||
"name" text NOT NULL,
|
||||
"participant_count" integer DEFAULT 0 NOT NULL,
|
||||
"is_archived" boolean DEFAULT false NOT NULL,
|
||||
"last_synced_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "audit_log" ADD CONSTRAINT "audit_log_operator_id_operators_id_fk" FOREIGN KEY ("operator_id") REFERENCES "public"."operators"("id") ON DELETE no action ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "auth_sessions" ADD CONSTRAINT "auth_sessions_operator_id_operators_id_fk" FOREIGN KEY ("operator_id") REFERENCES "public"."operators"("id") ON DELETE no action ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "media_files" ADD CONSTRAINT "media_files_operator_id_operators_id_fk" FOREIGN KEY ("operator_id") REFERENCES "public"."operators"("id") ON DELETE no action ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "reminder_messages" ADD CONSTRAINT "reminder_messages_reminder_id_reminders_id_fk" FOREIGN KEY ("reminder_id") REFERENCES "public"."reminders"("id") ON DELETE cascade ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "reminder_messages" ADD CONSTRAINT "reminder_messages_media_id_media_files_id_fk" FOREIGN KEY ("media_id") REFERENCES "public"."media_files"("id") ON DELETE no action ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "reminder_run_targets" ADD CONSTRAINT "reminder_run_targets_run_id_reminder_runs_id_fk" FOREIGN KEY ("run_id") REFERENCES "public"."reminder_runs"("id") ON DELETE cascade ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "reminder_run_targets" ADD CONSTRAINT "reminder_run_targets_group_id_whatsapp_groups_id_fk" FOREIGN KEY ("group_id") REFERENCES "public"."whatsapp_groups"("id") ON DELETE no action ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "reminder_runs" ADD CONSTRAINT "reminder_runs_reminder_id_reminders_id_fk" FOREIGN KEY ("reminder_id") REFERENCES "public"."reminders"("id") ON DELETE no action ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "reminder_targets" ADD CONSTRAINT "reminder_targets_reminder_id_reminders_id_fk" FOREIGN KEY ("reminder_id") REFERENCES "public"."reminders"("id") ON DELETE cascade ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "reminder_targets" ADD CONSTRAINT "reminder_targets_group_id_whatsapp_groups_id_fk" FOREIGN KEY ("group_id") REFERENCES "public"."whatsapp_groups"("id") ON DELETE no action ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "reminders" ADD CONSTRAINT "reminders_account_id_whatsapp_accounts_id_fk" FOREIGN KEY ("account_id") REFERENCES "public"."whatsapp_accounts"("id") ON DELETE no action ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "reminders" ADD CONSTRAINT "reminders_created_by_operators_id_fk" FOREIGN KEY ("created_by") REFERENCES "public"."operators"("id") ON DELETE no action ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "whatsapp_accounts" ADD CONSTRAINT "whatsapp_accounts_operator_id_operators_id_fk" FOREIGN KEY ("operator_id") REFERENCES "public"."operators"("id") ON DELETE no action ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "whatsapp_groups" ADD CONSTRAINT "whatsapp_groups_account_id_whatsapp_accounts_id_fk" FOREIGN KEY ("account_id") REFERENCES "public"."whatsapp_accounts"("id") ON DELETE no action ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "operators_telegram_user_id_uq" ON "operators" USING btree ("telegram_user_id");--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "whatsapp_accounts_operator_label_uq" ON "whatsapp_accounts" USING btree ("operator_id","label");--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "whatsapp_groups_account_jid_uq" ON "whatsapp_groups" USING btree ("account_id","wa_group_jid");
|
||||
933
packages/db/migrations/meta/0000_snapshot.json
Normal file
933
packages/db/migrations/meta/0000_snapshot.json
Normal file
@ -0,0 +1,933 @@
|
||||
{
|
||||
"id": "980c003a-039d-4a53-a6ee-a38c556579d9",
|
||||
"prevId": "00000000-0000-0000-0000-000000000000",
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
"tables": {
|
||||
"public.audit_log": {
|
||||
"name": "audit_log",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"operator_id": {
|
||||
"name": "operator_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"source": {
|
||||
"name": "source",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"action": {
|
||||
"name": "action",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"target_type": {
|
||||
"name": "target_type",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"target_id": {
|
||||
"name": "target_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"payload": {
|
||||
"name": "payload",
|
||||
"type": "jsonb",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "'{}'::jsonb"
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"audit_log_operator_id_operators_id_fk": {
|
||||
"name": "audit_log_operator_id_operators_id_fk",
|
||||
"tableFrom": "audit_log",
|
||||
"tableTo": "operators",
|
||||
"columnsFrom": [
|
||||
"operator_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.auth_sessions": {
|
||||
"name": "auth_sessions",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"operator_id": {
|
||||
"name": "operator_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"token_hash": {
|
||||
"name": "token_hash",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"expires_at": {
|
||||
"name": "expires_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"last_used_at": {
|
||||
"name": "last_used_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"ip_address": {
|
||||
"name": "ip_address",
|
||||
"type": "inet",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"user_agent": {
|
||||
"name": "user_agent",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"auth_sessions_operator_id_operators_id_fk": {
|
||||
"name": "auth_sessions_operator_id_operators_id_fk",
|
||||
"tableFrom": "auth_sessions",
|
||||
"tableTo": "operators",
|
||||
"columnsFrom": [
|
||||
"operator_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {
|
||||
"auth_sessions_token_hash_unique": {
|
||||
"name": "auth_sessions_token_hash_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": [
|
||||
"token_hash"
|
||||
]
|
||||
}
|
||||
},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.media_files": {
|
||||
"name": "media_files",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"operator_id": {
|
||||
"name": "operator_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"filename_original": {
|
||||
"name": "filename_original",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"mime_type": {
|
||||
"name": "mime_type",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"size_bytes": {
|
||||
"name": "size_bytes",
|
||||
"type": "bigint",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"sha256": {
|
||||
"name": "sha256",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"storage_path": {
|
||||
"name": "storage_path",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"media_files_operator_id_operators_id_fk": {
|
||||
"name": "media_files_operator_id_operators_id_fk",
|
||||
"tableFrom": "media_files",
|
||||
"tableTo": "operators",
|
||||
"columnsFrom": [
|
||||
"operator_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.operators": {
|
||||
"name": "operators",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"telegram_user_id": {
|
||||
"name": "telegram_user_id",
|
||||
"type": "bigint",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"display_name": {
|
||||
"name": "display_name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"role": {
|
||||
"name": "role",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "'admin'"
|
||||
},
|
||||
"default_timezone": {
|
||||
"name": "default_timezone",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "'Asia/Kuala_Lumpur'"
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"operators_telegram_user_id_uq": {
|
||||
"name": "operators_telegram_user_id_uq",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "telegram_user_id",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": true,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.reminder_messages": {
|
||||
"name": "reminder_messages",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"reminder_id": {
|
||||
"name": "reminder_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"position": {
|
||||
"name": "position",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"kind": {
|
||||
"name": "kind",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"text_content": {
|
||||
"name": "text_content",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"media_id": {
|
||||
"name": "media_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"reminder_messages_reminder_id_reminders_id_fk": {
|
||||
"name": "reminder_messages_reminder_id_reminders_id_fk",
|
||||
"tableFrom": "reminder_messages",
|
||||
"tableTo": "reminders",
|
||||
"columnsFrom": [
|
||||
"reminder_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
"reminder_messages_media_id_media_files_id_fk": {
|
||||
"name": "reminder_messages_media_id_media_files_id_fk",
|
||||
"tableFrom": "reminder_messages",
|
||||
"tableTo": "media_files",
|
||||
"columnsFrom": [
|
||||
"media_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.reminder_run_targets": {
|
||||
"name": "reminder_run_targets",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"run_id": {
|
||||
"name": "run_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"group_id": {
|
||||
"name": "group_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"status": {
|
||||
"name": "status",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"wa_message_id": {
|
||||
"name": "wa_message_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"error": {
|
||||
"name": "error",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"latency_ms": {
|
||||
"name": "latency_ms",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"reminder_run_targets_run_id_reminder_runs_id_fk": {
|
||||
"name": "reminder_run_targets_run_id_reminder_runs_id_fk",
|
||||
"tableFrom": "reminder_run_targets",
|
||||
"tableTo": "reminder_runs",
|
||||
"columnsFrom": [
|
||||
"run_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
"reminder_run_targets_group_id_whatsapp_groups_id_fk": {
|
||||
"name": "reminder_run_targets_group_id_whatsapp_groups_id_fk",
|
||||
"tableFrom": "reminder_run_targets",
|
||||
"tableTo": "whatsapp_groups",
|
||||
"columnsFrom": [
|
||||
"group_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {
|
||||
"reminder_run_targets_run_id_group_id_pk": {
|
||||
"name": "reminder_run_targets_run_id_group_id_pk",
|
||||
"columns": [
|
||||
"run_id",
|
||||
"group_id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.reminder_runs": {
|
||||
"name": "reminder_runs",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"reminder_id": {
|
||||
"name": "reminder_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"fired_at": {
|
||||
"name": "fired_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"status": {
|
||||
"name": "status",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"error_summary": {
|
||||
"name": "error_summary",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"reminder_runs_reminder_id_reminders_id_fk": {
|
||||
"name": "reminder_runs_reminder_id_reminders_id_fk",
|
||||
"tableFrom": "reminder_runs",
|
||||
"tableTo": "reminders",
|
||||
"columnsFrom": [
|
||||
"reminder_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.reminder_targets": {
|
||||
"name": "reminder_targets",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"reminder_id": {
|
||||
"name": "reminder_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"group_id": {
|
||||
"name": "group_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"position": {
|
||||
"name": "position",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": 0
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"reminder_targets_reminder_id_reminders_id_fk": {
|
||||
"name": "reminder_targets_reminder_id_reminders_id_fk",
|
||||
"tableFrom": "reminder_targets",
|
||||
"tableTo": "reminders",
|
||||
"columnsFrom": [
|
||||
"reminder_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
"reminder_targets_group_id_whatsapp_groups_id_fk": {
|
||||
"name": "reminder_targets_group_id_whatsapp_groups_id_fk",
|
||||
"tableFrom": "reminder_targets",
|
||||
"tableTo": "whatsapp_groups",
|
||||
"columnsFrom": [
|
||||
"group_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {
|
||||
"reminder_targets_reminder_id_group_id_pk": {
|
||||
"name": "reminder_targets_reminder_id_group_id_pk",
|
||||
"columns": [
|
||||
"reminder_id",
|
||||
"group_id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.reminders": {
|
||||
"name": "reminders",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"account_id": {
|
||||
"name": "account_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"schedule_kind": {
|
||||
"name": "schedule_kind",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"scheduled_at": {
|
||||
"name": "scheduled_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"rrule": {
|
||||
"name": "rrule",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"timezone": {
|
||||
"name": "timezone",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"ends_at": {
|
||||
"name": "ends_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"max_runs": {
|
||||
"name": "max_runs",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"status": {
|
||||
"name": "status",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "'active'"
|
||||
},
|
||||
"created_by": {
|
||||
"name": "created_by",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"reminders_account_id_whatsapp_accounts_id_fk": {
|
||||
"name": "reminders_account_id_whatsapp_accounts_id_fk",
|
||||
"tableFrom": "reminders",
|
||||
"tableTo": "whatsapp_accounts",
|
||||
"columnsFrom": [
|
||||
"account_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
"reminders_created_by_operators_id_fk": {
|
||||
"name": "reminders_created_by_operators_id_fk",
|
||||
"tableFrom": "reminders",
|
||||
"tableTo": "operators",
|
||||
"columnsFrom": [
|
||||
"created_by"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.whatsapp_accounts": {
|
||||
"name": "whatsapp_accounts",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"operator_id": {
|
||||
"name": "operator_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"label": {
|
||||
"name": "label",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"phone_number": {
|
||||
"name": "phone_number",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"status": {
|
||||
"name": "status",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "'pending'"
|
||||
},
|
||||
"last_connected_at": {
|
||||
"name": "last_connected_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"last_qr_at": {
|
||||
"name": "last_qr_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"whatsapp_accounts_operator_label_uq": {
|
||||
"name": "whatsapp_accounts_operator_label_uq",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "operator_id",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
},
|
||||
{
|
||||
"expression": "label",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": true,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"whatsapp_accounts_operator_id_operators_id_fk": {
|
||||
"name": "whatsapp_accounts_operator_id_operators_id_fk",
|
||||
"tableFrom": "whatsapp_accounts",
|
||||
"tableTo": "operators",
|
||||
"columnsFrom": [
|
||||
"operator_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.whatsapp_groups": {
|
||||
"name": "whatsapp_groups",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"account_id": {
|
||||
"name": "account_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"wa_group_jid": {
|
||||
"name": "wa_group_jid",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"participant_count": {
|
||||
"name": "participant_count",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": 0
|
||||
},
|
||||
"is_archived": {
|
||||
"name": "is_archived",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": false
|
||||
},
|
||||
"last_synced_at": {
|
||||
"name": "last_synced_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"whatsapp_groups_account_jid_uq": {
|
||||
"name": "whatsapp_groups_account_jid_uq",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "account_id",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
},
|
||||
{
|
||||
"expression": "wa_group_jid",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": true,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"whatsapp_groups_account_id_whatsapp_accounts_id_fk": {
|
||||
"name": "whatsapp_groups_account_id_whatsapp_accounts_id_fk",
|
||||
"tableFrom": "whatsapp_groups",
|
||||
"tableTo": "whatsapp_accounts",
|
||||
"columnsFrom": [
|
||||
"account_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
}
|
||||
},
|
||||
"enums": {},
|
||||
"schemas": {},
|
||||
"sequences": {},
|
||||
"roles": {},
|
||||
"policies": {},
|
||||
"views": {},
|
||||
"_meta": {
|
||||
"columns": {},
|
||||
"schemas": {},
|
||||
"tables": {}
|
||||
}
|
||||
}
|
||||
13
packages/db/migrations/meta/_journal.json
Normal file
13
packages/db/migrations/meta/_journal.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
"entries": [
|
||||
{
|
||||
"idx": 0,
|
||||
"version": "7",
|
||||
"when": 1778311164225,
|
||||
"tag": "0000_conscious_tarantula",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
32
packages/db/package.json
Normal file
32
packages/db/package.json
Normal file
@ -0,0 +1,32 @@
|
||||
{
|
||||
"name": "@cmbot/db",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./schema": "./src/schema.ts"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc -p tsconfig.json",
|
||||
"test": "echo 'no unit tests'",
|
||||
"lint": "echo 'lint placeholder'",
|
||||
"typecheck": "tsc -p tsconfig.json --noEmit",
|
||||
"generate": "drizzle-kit generate",
|
||||
"migrate": "tsx src/migrate.ts",
|
||||
"studio": "drizzle-kit studio --host 0.0.0.0",
|
||||
"seed": "tsx src/seed.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"drizzle-orm": "^0.36.0",
|
||||
"pg": "^8.13.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/pg": "^8.11.10",
|
||||
"drizzle-kit": "^0.28.0",
|
||||
"tsx": "^4.19.0",
|
||||
"typescript": "^5.5.0"
|
||||
}
|
||||
}
|
||||
13
packages/db/src/index.ts
Normal file
13
packages/db/src/index.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { drizzle, type NodePgDatabase } from "drizzle-orm/node-postgres";
|
||||
import { Pool } from "pg";
|
||||
import * as schema from "./schema.js";
|
||||
|
||||
export * from "./schema.js";
|
||||
|
||||
export type DB = NodePgDatabase<typeof schema>;
|
||||
|
||||
export function createClient(databaseUrl: string): { db: DB; pool: Pool } {
|
||||
const pool = new Pool({ connectionString: databaseUrl });
|
||||
const db = drizzle(pool, { schema });
|
||||
return { db, pool };
|
||||
}
|
||||
14
packages/db/src/migrate.ts
Normal file
14
packages/db/src/migrate.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { migrate } from "drizzle-orm/node-postgres/migrator";
|
||||
import { createClient } from "./index.js";
|
||||
|
||||
const databaseUrl = process.env.DATABASE_URL;
|
||||
if (!databaseUrl) {
|
||||
console.error("DATABASE_URL not set");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const { db, pool } = createClient(databaseUrl);
|
||||
console.log("Applying migrations...");
|
||||
await migrate(db, { migrationsFolder: "./migrations" });
|
||||
console.log("Migrations applied.");
|
||||
await pool.end();
|
||||
163
packages/db/src/schema.ts
Normal file
163
packages/db/src/schema.ts
Normal file
@ -0,0 +1,163 @@
|
||||
import {
|
||||
pgTable,
|
||||
uuid,
|
||||
text,
|
||||
bigint,
|
||||
integer,
|
||||
boolean,
|
||||
timestamp,
|
||||
jsonb,
|
||||
primaryKey,
|
||||
uniqueIndex,
|
||||
inet,
|
||||
} from "drizzle-orm/pg-core";
|
||||
|
||||
export const operators = pgTable(
|
||||
"operators",
|
||||
{
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
telegramUserId: bigint("telegram_user_id", { mode: "number" }).notNull(),
|
||||
displayName: text("display_name").notNull(),
|
||||
role: text("role").notNull().default("admin"),
|
||||
defaultTimezone: text("default_timezone").notNull().default("Asia/Kuala_Lumpur"),
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
},
|
||||
(t) => ({
|
||||
telegramUserIdUnique: uniqueIndex("operators_telegram_user_id_uq").on(t.telegramUserId),
|
||||
}),
|
||||
);
|
||||
|
||||
export const whatsappAccounts = pgTable(
|
||||
"whatsapp_accounts",
|
||||
{
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
operatorId: uuid("operator_id").notNull().references(() => operators.id),
|
||||
label: text("label").notNull(),
|
||||
phoneNumber: text("phone_number"),
|
||||
status: text("status").notNull().default("pending"),
|
||||
lastConnectedAt: timestamp("last_connected_at", { withTimezone: true }),
|
||||
lastQrAt: timestamp("last_qr_at", { withTimezone: true }),
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
},
|
||||
(t) => ({
|
||||
operatorLabelUnique: uniqueIndex("whatsapp_accounts_operator_label_uq").on(t.operatorId, t.label),
|
||||
}),
|
||||
);
|
||||
|
||||
export const whatsappGroups = pgTable(
|
||||
"whatsapp_groups",
|
||||
{
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
accountId: uuid("account_id").notNull().references(() => whatsappAccounts.id),
|
||||
waGroupJid: text("wa_group_jid").notNull(),
|
||||
name: text("name").notNull(),
|
||||
participantCount: integer("participant_count").notNull().default(0),
|
||||
isArchived: boolean("is_archived").notNull().default(false),
|
||||
lastSyncedAt: timestamp("last_synced_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
},
|
||||
(t) => ({
|
||||
accountJidUnique: uniqueIndex("whatsapp_groups_account_jid_uq").on(t.accountId, t.waGroupJid),
|
||||
}),
|
||||
);
|
||||
|
||||
export const mediaFiles = pgTable("media_files", {
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
operatorId: uuid("operator_id").notNull().references(() => operators.id),
|
||||
filenameOriginal: text("filename_original").notNull(),
|
||||
mimeType: text("mime_type").notNull(),
|
||||
sizeBytes: bigint("size_bytes", { mode: "number" }).notNull(),
|
||||
sha256: text("sha256").notNull(),
|
||||
storagePath: text("storage_path").notNull(),
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
});
|
||||
|
||||
export const reminders = pgTable("reminders", {
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
accountId: uuid("account_id").notNull().references(() => whatsappAccounts.id),
|
||||
name: text("name").notNull(),
|
||||
scheduleKind: text("schedule_kind").notNull(),
|
||||
scheduledAt: timestamp("scheduled_at", { withTimezone: true }),
|
||||
rrule: text("rrule"),
|
||||
timezone: text("timezone").notNull(),
|
||||
endsAt: timestamp("ends_at", { withTimezone: true }),
|
||||
maxRuns: integer("max_runs"),
|
||||
status: text("status").notNull().default("active"),
|
||||
createdBy: uuid("created_by").notNull().references(() => operators.id),
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
});
|
||||
|
||||
export const reminderTargets = pgTable(
|
||||
"reminder_targets",
|
||||
{
|
||||
reminderId: uuid("reminder_id").notNull().references(() => reminders.id, { onDelete: "cascade" }),
|
||||
groupId: uuid("group_id").notNull().references(() => whatsappGroups.id),
|
||||
position: integer("position").notNull().default(0),
|
||||
},
|
||||
(t) => ({
|
||||
pk: primaryKey({ columns: [t.reminderId, t.groupId] }),
|
||||
}),
|
||||
);
|
||||
|
||||
export const reminderMessages = pgTable("reminder_messages", {
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
reminderId: uuid("reminder_id").notNull().references(() => reminders.id, { onDelete: "cascade" }),
|
||||
position: integer("position").notNull(),
|
||||
kind: text("kind").notNull(),
|
||||
textContent: text("text_content"),
|
||||
mediaId: uuid("media_id").references(() => mediaFiles.id),
|
||||
});
|
||||
|
||||
export const reminderRuns = pgTable("reminder_runs", {
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
reminderId: uuid("reminder_id").notNull().references(() => reminders.id),
|
||||
firedAt: timestamp("fired_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
status: text("status").notNull(),
|
||||
errorSummary: text("error_summary"),
|
||||
});
|
||||
|
||||
export const reminderRunTargets = pgTable(
|
||||
"reminder_run_targets",
|
||||
{
|
||||
runId: uuid("run_id").notNull().references(() => reminderRuns.id, { onDelete: "cascade" }),
|
||||
groupId: uuid("group_id").notNull().references(() => whatsappGroups.id),
|
||||
status: text("status").notNull(),
|
||||
waMessageId: text("wa_message_id"),
|
||||
error: text("error"),
|
||||
latencyMs: integer("latency_ms"),
|
||||
},
|
||||
(t) => ({
|
||||
pk: primaryKey({ columns: [t.runId, t.groupId] }),
|
||||
}),
|
||||
);
|
||||
|
||||
export const auditLog = pgTable("audit_log", {
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
operatorId: uuid("operator_id").references(() => operators.id),
|
||||
source: text("source").notNull(),
|
||||
action: text("action").notNull(),
|
||||
targetType: text("target_type"),
|
||||
targetId: uuid("target_id"),
|
||||
payload: jsonb("payload").notNull().default({}),
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
});
|
||||
|
||||
export const authSessions = pgTable("auth_sessions", {
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
operatorId: uuid("operator_id").notNull().references(() => operators.id),
|
||||
tokenHash: text("token_hash").notNull().unique(),
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
expiresAt: timestamp("expires_at", { withTimezone: true }).notNull(),
|
||||
lastUsedAt: timestamp("last_used_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
ipAddress: inet("ip_address"),
|
||||
userAgent: text("user_agent"),
|
||||
});
|
||||
|
||||
export type Operator = typeof operators.$inferSelect;
|
||||
export type NewOperator = typeof operators.$inferInsert;
|
||||
export type WhatsappAccount = typeof whatsappAccounts.$inferSelect;
|
||||
export type NewWhatsappAccount = typeof whatsappAccounts.$inferInsert;
|
||||
export type WhatsappGroup = typeof whatsappGroups.$inferSelect;
|
||||
export type NewWhatsappGroup = typeof whatsappGroups.$inferInsert;
|
||||
export type AuditLogEntry = typeof auditLog.$inferSelect;
|
||||
export type NewAuditLogEntry = typeof auditLog.$inferInsert;
|
||||
29
packages/db/src/seed.ts
Normal file
29
packages/db/src/seed.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import { createClient, operators } from "./index.js";
|
||||
|
||||
const databaseUrl = process.env.DATABASE_URL;
|
||||
const operatorTelegramId = process.env.SEED_OPERATOR_TELEGRAM_ID;
|
||||
const operatorName = process.env.SEED_OPERATOR_NAME ?? "Operator";
|
||||
|
||||
if (!databaseUrl) {
|
||||
console.error("DATABASE_URL not set");
|
||||
process.exit(1);
|
||||
}
|
||||
if (!operatorTelegramId || operatorTelegramId === "0") {
|
||||
console.error("SEED_OPERATOR_TELEGRAM_ID not set");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const { db, pool } = createClient(databaseUrl);
|
||||
|
||||
await db
|
||||
.insert(operators)
|
||||
.values({
|
||||
telegramUserId: Number(operatorTelegramId),
|
||||
displayName: operatorName,
|
||||
role: "admin",
|
||||
defaultTimezone: "Asia/Kuala_Lumpur",
|
||||
})
|
||||
.onConflictDoNothing();
|
||||
|
||||
console.log(`Seeded operator with telegram_user_id=${operatorTelegramId}`);
|
||||
await pool.end();
|
||||
8
packages/db/tsconfig.json
Normal file
8
packages/db/tsconfig.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src"
|
||||
},
|
||||
"include": ["src/**/*"]
|
||||
}
|
||||
1095
pnpm-lock.yaml
generated
1095
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user