77 lines
3.5 KiB
TypeScript
77 lines
3.5 KiB
TypeScript
import { pgTable, text, jsonb, integer, timestamp, uuid } from "drizzle-orm/pg-core";
|
|
|
|
/** Job lifecycle row. Status mirrors the queue; `cacheKey` ties it to the cache row.
|
|
* `options`/`timings` are jsonb (the service's typed shapes). */
|
|
export const jobs = pgTable("jobs", {
|
|
id: uuid("id").defaultRandom().primaryKey(),
|
|
kind: text("kind").notNull(), // "clone" | "clone_site"
|
|
url: text("url").notNull(),
|
|
options: jsonb("options").notNull().default({}),
|
|
status: text("status").notNull().default("queued"), // queued|running|succeeded|failed|cached
|
|
cacheKey: text("cache_key").notNull(),
|
|
attempts: integer("attempts").notNull().default(0),
|
|
error: text("error"),
|
|
compilerVersion: text("compiler_version"),
|
|
timings: jsonb("timings"),
|
|
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
|
startedAt: timestamp("started_at", { withTimezone: true }),
|
|
finishedAt: timestamp("finished_at", { withTimezone: true }),
|
|
});
|
|
|
|
/** The persisted result, written once on success. `fileManifest` holds the text
|
|
* files inline + binary metadata (the eager file map sans bytes); binaries live in
|
|
* blob storage referenced by `bundleS3Key` / per-file keys in the manifest. */
|
|
export const clones = pgTable("clones", {
|
|
jobId: uuid("job_id")
|
|
.primaryKey()
|
|
.references(() => jobs.id, { onDelete: "cascade" }),
|
|
url: text("url").notNull(),
|
|
routeCount: integer("route_count").notNull().default(1),
|
|
fileManifest: jsonb("file_manifest").notNull(),
|
|
bundleS3Key: text("bundle_s3_key"),
|
|
verify: jsonb("verify"),
|
|
captureMeta: jsonb("capture_meta").notNull(),
|
|
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
|
});
|
|
|
|
/** Freshness-bounded "recently cloned this URL+options" cache. A hit requires
|
|
* `now < expiresAt` and a matching compilerVersion. */
|
|
export const cache = pgTable("cache", {
|
|
cacheKey: text("cache_key").primaryKey(),
|
|
jobId: uuid("job_id").references(() => jobs.id, { onDelete: "cascade" }),
|
|
url: text("url").notNull(),
|
|
optionsHash: text("options_hash").notNull(),
|
|
compilerVersion: text("compiler_version").notNull(),
|
|
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
|
expiresAt: timestamp("expires_at", { withTimezone: true }).notNull(),
|
|
});
|
|
|
|
/** API keys (M6): only a hash is stored. `rateLimit` is requests/min (nullable = default). */
|
|
export const apiKeys = pgTable("api_keys", {
|
|
id: uuid("id").defaultRandom().primaryKey(),
|
|
keyHash: text("key_hash").notNull().unique(),
|
|
label: text("label"),
|
|
rateLimit: integer("rate_limit"),
|
|
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
|
revokedAt: timestamp("revoked_at", { withTimezone: true }),
|
|
});
|
|
|
|
/** One-time email verification tokens for public API-key signup. */
|
|
export const signupTokens = pgTable("signup_tokens", {
|
|
id: uuid("id").defaultRandom().primaryKey(),
|
|
email: text("email").notNull(),
|
|
tokenHash: text("token_hash").notNull().unique(),
|
|
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
|
expiresAt: timestamp("expires_at", { withTimezone: true }).notNull(),
|
|
consumedAt: timestamp("consumed_at", { withTimezone: true }),
|
|
});
|
|
|
|
export type Job = typeof jobs.$inferSelect;
|
|
export type NewJob = typeof jobs.$inferInsert;
|
|
export type Clone = typeof clones.$inferSelect;
|
|
export type NewClone = typeof clones.$inferInsert;
|
|
export type CacheRow = typeof cache.$inferSelect;
|
|
export type ApiKey = typeof apiKeys.$inferSelect;
|
|
export type SignupToken = typeof signupTokens.$inferSelect;
|
|
export type NewSignupToken = typeof signupTokens.$inferInsert;
|