Files
Cibello-app/packages/database/src/schema/platform.ts
T
2026-08-05 19:21:11 +07:00

203 lines
6.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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] })],
);