import { boolean, index, integer, jsonb, pgEnum, pgTable, text, timestamp, uniqueIndex, uuid, } from "drizzle-orm/pg-core"; import { sql } from "drizzle-orm"; export const instanceStatus = pgEnum("instance_status", ["active", "blocked", "disabled"]); export const payloadMode = pgEnum("payload_mode", ["minimal", "rich"]); export const devicePlatform = pgEnum("device_platform", ["web", "ios", "android"]); export const deviceStatus = pgEnum("device_status", ["active", "disabled", "invalid"]); export const deliveryStatus = pgEnum("delivery_status", ["accepted", "processing", "completed", "failed", "partial"]); export const attemptStatus = pgEnum("attempt_status", ["pending", "sent", "failed", "skipped"]); export const attemptProvider = pgEnum("attempt_provider", ["web_push", "apns", "fcm"]); export const pushInstances = pgTable( "push_instances", { id: uuid("id").primaryKey().defaultRandom(), instanceId: text("instance_id").notNull().unique(), name: text("name").notNull(), baseUrl: text("base_url").notNull(), status: instanceStatus("status").notNull().default("active"), mode: payloadMode("payload_mode").notNull().default("minimal"), capabilities: jsonb("capabilities").$type().notNull().default(sql`'[]'::jsonb`), rateLimitPerMinute: integer("rate_limit_per_minute").notNull().default(120), dailyQuota: integer("daily_quota").notNull().default(10000), currentSecretEncrypted: text("current_secret_encrypted").notNull(), currentSecretPreview: text("current_secret_preview").notNull(), nextSecretEncrypted: text("next_secret_encrypted"), nextSecretPreview: text("next_secret_preview"), notes: text("notes"), lastHeartbeatAt: timestamp("last_heartbeat_at", { withTimezone: true }), lastHeartbeatVersion: text("last_heartbeat_version"), lastHeartbeatIp: text("last_heartbeat_ip"), createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(), updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(), }, (table) => ({ instanceIdIdx: uniqueIndex("push_instances_instance_id_idx").on(table.instanceId), statusIdx: index("push_instances_status_idx").on(table.status), }), ); export const pushDevices = pgTable( "push_devices", { id: uuid("id").primaryKey().defaultRandom(), centralDeviceId: text("central_device_id").notNull().unique(), instanceId: uuid("instance_id") .notNull() .references(() => pushInstances.id, { onDelete: "cascade" }), localDeviceId: text("local_device_id").notNull(), platform: devicePlatform("platform").notNull(), status: deviceStatus("status").notNull().default("active"), providerTokenEncrypted: text("provider_token_encrypted"), webPushSubscription: jsonb("web_push_subscription").$type>(), meta: jsonb("meta").$type>().notNull().default(sql`'{}'::jsonb`), lastSeenAt: timestamp("last_seen_at", { withTimezone: true }).notNull().defaultNow(), disabledAt: timestamp("disabled_at", { withTimezone: true }), createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(), updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(), }, (table) => ({ centralDeviceIdIdx: uniqueIndex("push_devices_central_device_id_idx").on(table.centralDeviceId), instanceLocalDeviceIdx: uniqueIndex("push_devices_instance_local_device_idx").on(table.instanceId, table.localDeviceId), instanceStatusIdx: index("push_devices_instance_status_idx").on(table.instanceId, table.status), }), ); export const deliveryJobs = pgTable( "delivery_jobs", { id: uuid("id").primaryKey().defaultRandom(), deliveryJobId: text("delivery_job_id").notNull().unique(), instanceId: uuid("instance_id") .notNull() .references(() => pushInstances.id, { onDelete: "cascade" }), idempotencyKey: text("idempotency_key").notNull(), status: deliveryStatus("status").notNull().default("accepted"), priority: text("priority").notNull().default("normal"), collapseKey: text("collapse_key"), ttlSeconds: integer("ttl_seconds").notNull().default(3600), acceptedCount: integer("accepted_count").notNull().default(0), rejectedCount: integer("rejected_count").notNull().default(0), sentCount: integer("sent_count").notNull().default(0), failedCount: integer("failed_count").notNull().default(0), lastErrorCode: text("last_error_code"), lastErrorMessage: text("last_error_message"), completedAt: timestamp("completed_at", { withTimezone: true }), createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(), updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(), }, (table) => ({ deliveryJobIdIdx: uniqueIndex("delivery_jobs_delivery_job_id_idx").on(table.deliveryJobId), instanceIdempotencyIdx: uniqueIndex("delivery_jobs_instance_idempotency_idx").on(table.instanceId, table.idempotencyKey), instanceCreatedIdx: index("delivery_jobs_instance_created_idx").on(table.instanceId, table.createdAt), }), ); export const deliveryAttempts = pgTable( "delivery_attempts", { id: uuid("id").primaryKey().defaultRandom(), deliveryJobId: uuid("delivery_job_id") .notNull() .references(() => deliveryJobs.id, { onDelete: "cascade" }), deviceId: uuid("device_id") .notNull() .references(() => pushDevices.id, { onDelete: "cascade" }), provider: attemptProvider("provider").notNull(), status: attemptStatus("status").notNull().default("pending"), providerMessageId: text("provider_message_id"), errorCode: text("error_code"), errorMessage: text("error_message"), sentAt: timestamp("sent_at", { withTimezone: true }), createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(), }, (table) => ({ jobIdx: index("delivery_attempts_job_idx").on(table.deliveryJobId), deviceIdx: index("delivery_attempts_device_idx").on(table.deviceId), }), ); export const auditLogs = pgTable( "audit_logs", { id: uuid("id").primaryKey().defaultRandom(), actor: text("actor").notNull(), action: text("action").notNull(), instanceId: uuid("instance_id").references(() => pushInstances.id, { onDelete: "set null" }), meta: jsonb("meta").$type>().notNull().default(sql`'{}'::jsonb`), createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(), }, (table) => ({ instanceCreatedIdx: index("audit_logs_instance_created_idx").on(table.instanceId, table.createdAt), }), ); export type PushInstance = typeof pushInstances.$inferSelect; export type NewPushInstance = typeof pushInstances.$inferInsert; export type PushDevice = typeof pushDevices.$inferSelect; export type NewPushDevice = typeof pushDevices.$inferInsert; export type DeliveryJob = typeof deliveryJobs.$inferSelect; export type NewDeliveryJob = typeof deliveryJobs.$inferInsert; export type DeliveryAttempt = typeof deliveryAttempts.$inferSelect; export type NewDeliveryAttempt = typeof deliveryAttempts.$inferInsert;