203 lines
6.6 KiB
TypeScript
203 lines
6.6 KiB
TypeScript
import {
|
||
boolean,
|
||
doublePrecision,
|
||
index,
|
||
integer,
|
||
jsonb,
|
||
pgTable,
|
||
primaryKey,
|
||
text,
|
||
timestamp,
|
||
uuid,
|
||
} from "drizzle-orm/pg-core";
|
||
import {
|
||
createdAt,
|
||
creatorLevelEnum,
|
||
eventTypeEnum,
|
||
notificationTypeEnum,
|
||
profileVisibilityEnum,
|
||
updatedAt,
|
||
} from "./_shared.js";
|
||
import { households } from "./households.js";
|
||
import { users } from "./users.js";
|
||
|
||
/**
|
||
* Domänhändelser (spec §55) i outbox-mönster: skrivs transaktionellt med
|
||
* affärsdata och publiceras asynkront till konsumenter (analytics, memory,
|
||
* recommendations, training).
|
||
*/
|
||
export const domainEvents = pgTable(
|
||
"domain_events",
|
||
{
|
||
id: uuid("id").primaryKey().defaultRandom(),
|
||
type: eventTypeEnum("type").notNull(),
|
||
userId: uuid("user_id"),
|
||
householdId: uuid("household_id"),
|
||
payload: jsonb("payload").notNull(),
|
||
correlationId: text("correlation_id"),
|
||
occurredAt: timestamp("occurred_at", { withTimezone: true }).notNull().defaultNow(),
|
||
publishedAt: timestamp("published_at", { withTimezone: true }),
|
||
},
|
||
(t) => [
|
||
index("domain_events_unpublished_idx").on(t.publishedAt, t.occurredAt),
|
||
index("domain_events_type_idx").on(t.type, t.occurredAt),
|
||
index("domain_events_household_idx").on(t.householdId, t.occurredAt),
|
||
],
|
||
);
|
||
|
||
export const featureFlags = pgTable("feature_flags", {
|
||
key: text("key").primaryKey(),
|
||
enabled: boolean("enabled").notNull().default(false),
|
||
descriptionSv: text("description_sv"),
|
||
rolloutPercent: integer("rollout_percent").notNull().default(100),
|
||
updatedAt: updatedAt(),
|
||
});
|
||
|
||
/** Audit log (spec §56): vem gjorde vad, när, mot vad. */
|
||
export const auditLogs = pgTable(
|
||
"audit_logs",
|
||
{
|
||
id: uuid("id").primaryKey().defaultRandom(),
|
||
actorUserId: uuid("actor_user_id"),
|
||
actorType: text("actor_type").notNull().default("user"),
|
||
action: text("action").notNull(),
|
||
targetType: text("target_type"),
|
||
targetId: text("target_id"),
|
||
metadata: jsonb("metadata"),
|
||
ip: text("ip"),
|
||
correlationId: text("correlation_id"),
|
||
createdAt: createdAt(),
|
||
},
|
||
(t) => [
|
||
index("audit_logs_actor_idx").on(t.actorUserId, t.createdAt),
|
||
index("audit_logs_action_idx").on(t.action, t.createdAt),
|
||
],
|
||
);
|
||
|
||
/** Idempotency-nycklar för säkra POST-retries (spec §56, §59). */
|
||
export const idempotencyKeys = pgTable(
|
||
"idempotency_keys",
|
||
{
|
||
userId: uuid("user_id")
|
||
.notNull()
|
||
.references(() => users.id, { onDelete: "cascade" }),
|
||
key: text("key").notNull(),
|
||
endpoint: text("endpoint").notNull(),
|
||
responseStatus: integer("response_status"),
|
||
responseBody: jsonb("response_body"),
|
||
createdAt: createdAt(),
|
||
},
|
||
(t) => [primaryKey({ columns: [t.userId, t.key, t.endpoint] })],
|
||
);
|
||
|
||
/** Notiser (spec §40). */
|
||
export const notifications = pgTable(
|
||
"notifications",
|
||
{
|
||
id: uuid("id").primaryKey().defaultRandom(),
|
||
userId: uuid("user_id")
|
||
.notNull()
|
||
.references(() => users.id, { onDelete: "cascade" }),
|
||
type: notificationTypeEnum("type").notNull(),
|
||
titleSv: text("title_sv").notNull(),
|
||
bodySv: text("body_sv").notNull(),
|
||
data: jsonb("data"),
|
||
/** i18n-spec §26: mall + variabler; titleSv/bodySv är renderad cache. */
|
||
templateKey: text("template_key"),
|
||
variables: jsonb("variables"),
|
||
locale: text("locale"),
|
||
scheduledFor: timestamp("scheduled_for", { withTimezone: true }),
|
||
sentAt: timestamp("sent_at", { withTimezone: true }),
|
||
readAt: timestamp("read_at", { withTimezone: true }),
|
||
createdAt: createdAt(),
|
||
},
|
||
(t) => [index("notifications_user_idx").on(t.userId, t.createdAt)],
|
||
);
|
||
|
||
/** Push-tokens för Expo push. */
|
||
export const pushTokens = pgTable(
|
||
"push_tokens",
|
||
{
|
||
userId: uuid("user_id")
|
||
.notNull()
|
||
.references(() => users.id, { onDelete: "cascade" }),
|
||
token: text("token").notNull(),
|
||
platform: text("platform").notNull(),
|
||
updatedAt: updatedAt(),
|
||
},
|
||
(t) => [primaryKey({ columns: [t.userId, t.token] })],
|
||
);
|
||
|
||
/** Creator-statistik (spec §37). */
|
||
export const creatorStats = pgTable("creator_stats", {
|
||
userId: uuid("user_id")
|
||
.primaryKey()
|
||
.references(() => users.id, { onDelete: "cascade" }),
|
||
visibility: profileVisibilityEnum("visibility").notNull().default("private"),
|
||
level: creatorLevelEnum("level").notNull().default("beginner"),
|
||
publishedRecipes: integer("published_recipes").notNull().default(0),
|
||
followers: integer("followers").notNull().default(0),
|
||
totalCooks: integer("total_cooks").notNull().default(0),
|
||
totalFavorites: integer("total_favorites").notNull().default(0),
|
||
averageRating: doublePrecision("average_rating"),
|
||
verifiedRecipes: integer("verified_recipes").notNull().default(0),
|
||
badges: text("badges").array().notNull().default([]),
|
||
updatedAt: updatedAt(),
|
||
});
|
||
|
||
export const creatorFollows = pgTable(
|
||
"creator_follows",
|
||
{
|
||
followerUserId: uuid("follower_user_id")
|
||
.notNull()
|
||
.references(() => users.id, { onDelete: "cascade" }),
|
||
creatorUserId: uuid("creator_user_id")
|
||
.notNull()
|
||
.references(() => users.id, { onDelete: "cascade" }),
|
||
createdAt: createdAt(),
|
||
},
|
||
(t) => [primaryKey({ columns: [t.followerUserId, t.creatorUserId] })],
|
||
);
|
||
|
||
/** AI-evals (spec §34): fast testbibliotek + körningar. Ingen modelländring utan eval. */
|
||
export const aiEvalCases = pgTable("ai_eval_cases", {
|
||
id: uuid("id").primaryKey().defaultRandom(),
|
||
taskType: text("task_type").notNull(),
|
||
nameSv: text("name_sv").notNull(),
|
||
inputRef: jsonb("input_ref").notNull(),
|
||
expectedOutput: jsonb("expected_output").notNull(),
|
||
category: text("category").notNull().default("standard"),
|
||
active: boolean("active").notNull().default(true),
|
||
createdAt: createdAt(),
|
||
});
|
||
|
||
export const aiEvalRuns = pgTable(
|
||
"ai_eval_runs",
|
||
{
|
||
id: uuid("id").primaryKey().defaultRandom(),
|
||
taskType: text("task_type").notNull(),
|
||
modelVersion: text("model_version").notNull(),
|
||
promptVersion: text("prompt_version").notNull(),
|
||
metrics: jsonb("metrics").notNull(),
|
||
passed: boolean("passed").notNull(),
|
||
notes: text("notes"),
|
||
createdAt: createdAt(),
|
||
},
|
||
(t) => [index("ai_eval_runs_task_idx").on(t.taskType, t.createdAt)],
|
||
);
|
||
|
||
/** Householdsvinn per vecka – aggregat för budget/matsvinnsvyer (spec §26). */
|
||
export const wasteSummaries = pgTable(
|
||
"waste_summaries",
|
||
{
|
||
householdId: uuid("household_id")
|
||
.notNull()
|
||
.references(() => households.id, { onDelete: "cascade" }),
|
||
weekStartDate: text("week_start_date").notNull(),
|
||
discardedCount: integer("discarded_count").notNull().default(0),
|
||
discardedValueMinor: integer("discarded_value_minor").notNull().default(0),
|
||
updatedAt: updatedAt(),
|
||
},
|
||
(t) => [primaryKey({ columns: [t.householdId, t.weekStartDate] })],
|
||
);
|