Merge pull request #363 from Hestia-Homes/feature/user-portfolio-config

feat(db): user portfolio config + folders (home-page redesign schema)
This commit is contained in:
KhalimCK 2026-07-07 14:53:41 +01:00 committed by GitHub
commit ce6a6ffa2d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 12171 additions and 0 deletions

View file

@ -0,0 +1,27 @@
CREATE TABLE "user_portfolio_config" (
"id" bigserial PRIMARY KEY NOT NULL,
"user_id" bigint NOT NULL,
"portfolio_id" bigint NOT NULL,
"starred_at" timestamp (6) with time zone,
"folder_id" bigint,
"created_at" timestamp (6) with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp (6) with time zone DEFAULT now() NOT NULL,
CONSTRAINT "user_portfolio_config_user_portfolio_unique" UNIQUE("user_id","portfolio_id")
);
--> statement-breakpoint
CREATE TABLE "user_portfolio_folders" (
"id" bigserial PRIMARY KEY NOT NULL,
"user_id" bigint NOT NULL,
"name" text NOT NULL,
"position" integer NOT NULL,
"created_at" timestamp (6) with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp (6) with time zone DEFAULT now() NOT NULL,
CONSTRAINT "user_portfolio_folders_id_user_unique" UNIQUE("id","user_id")
);
--> statement-breakpoint
ALTER TABLE "user_portfolio_config" ADD CONSTRAINT "user_portfolio_config_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "user_portfolio_config" ADD CONSTRAINT "user_portfolio_config_portfolio_id_portfolio_id_fk" FOREIGN KEY ("portfolio_id") REFERENCES "public"."portfolio"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "user_portfolio_config" ADD CONSTRAINT "user_portfolio_config_folder_owner_fk" FOREIGN KEY ("folder_id","user_id") REFERENCES "public"."user_portfolio_folders"("id","user_id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "user_portfolio_folders" ADD CONSTRAINT "user_portfolio_folders_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
CREATE INDEX "idx_user_portfolio_config_folder" ON "user_portfolio_config" USING btree ("folder_id");--> statement-breakpoint
CREATE INDEX "idx_user_portfolio_folders_user_position" ON "user_portfolio_folders" USING btree ("user_id","position");

File diff suppressed because it is too large Load diff

View file

@ -1842,6 +1842,13 @@
"when": 1783425021407,
"tag": "0263_add_property_certificate_number",
"breakpoints": true
},
{
"idx": 264,
"version": "7",
"when": 1783430551377,
"tag": "0264_yummy_master_chief",
"breakpoints": true
}
]
}

View file

@ -0,0 +1,117 @@
import {
bigserial,
bigint,
text,
integer,
timestamp,
pgTable,
unique,
index,
foreignKey,
} from "drizzle-orm/pg-core";
import { InferModel } from "drizzle-orm";
import { user } from "./users";
import { portfolio } from "./portfolio";
// Per-user folders for organising portfolios on the home page. Folders are
// personal: two users on the same portfolio each keep their own grouping and
// ordering. Reordering renumbers position (0..n-1) in a single transaction.
export const userPortfolioFolders = pgTable(
"user_portfolio_folders",
{
id: bigserial("id", { mode: "bigint" }).primaryKey(),
userId: bigint("user_id", { mode: "bigint" })
.notNull()
.references(() => user.id, { onDelete: "cascade" }),
name: text("name").notNull(),
position: integer("position").notNull(),
createdAt: timestamp("created_at", {
precision: 6,
withTimezone: true,
})
.defaultNow()
.notNull(),
updatedAt: timestamp("updated_at", {
precision: 6,
withTimezone: true,
})
.defaultNow()
.notNull(),
},
(t) => [
// Composite target for the same-owner FK on userPortfolioConfig.
unique("user_portfolio_folders_id_user_unique").on(t.id, t.userId),
index("idx_user_portfolio_folders_user_position").on(t.userId, t.position),
],
);
// One row per (user, portfolio): that user's personal configuration for the
// portfolio. Rows are created lazily on first write (upsert on the unique
// pair); no row means "all defaults". Future per-user concerns (archived,
// last_viewed_at, notification prefs) become columns here — this table is the
// user-config layer, distinct from portfolioUsers which is access control.
export const userPortfolioConfig = pgTable(
"user_portfolio_config",
{
id: bigserial("id", { mode: "bigint" }).primaryKey(),
userId: bigint("user_id", { mode: "bigint" })
.notNull()
.references(() => user.id, { onDelete: "cascade" }),
portfolioId: bigint("portfolio_id", { mode: "bigint" })
.notNull()
.references(() => portfolio.id, { onDelete: "cascade" }),
// Starred iff non-null; the timestamp gives "recently starred" ordering.
starredAt: timestamp("starred_at", {
precision: 6,
withTimezone: true,
}),
folderId: bigint("folder_id", { mode: "bigint" }),
createdAt: timestamp("created_at", {
precision: 6,
withTimezone: true,
})
.defaultNow()
.notNull(),
updatedAt: timestamp("updated_at", {
precision: 6,
withTimezone: true,
})
.defaultNow()
.notNull(),
},
(t) => [
unique("user_portfolio_config_user_portfolio_unique").on(
t.userId,
t.portfolioId,
),
// A portfolio can only be filed into a folder owned by the same user: the
// FK pairs folder_id with user_id against (id, user_id) on folders.
// Deliberately NO ACTION on delete: composite FKs can't SET NULL just
// folder_id (it would null user_id too), so folder deletion must unfile
// its portfolios (SET folder_id = NULL) and delete in one transaction. A
// raw DELETE bypassing that fails loudly instead of corrupting rows.
foreignKey({
columns: [t.folderId, t.userId],
foreignColumns: [userPortfolioFolders.id, userPortfolioFolders.userId],
name: "user_portfolio_config_folder_owner_fk",
}),
index("idx_user_portfolio_config_folder").on(t.folderId),
],
);
export type UserPortfolioFolder = InferModel<
typeof userPortfolioFolders,
"select"
>;
export type NewUserPortfolioFolder = InferModel<
typeof userPortfolioFolders,
"insert"
>;
export type UserPortfolioConfig = InferModel<
typeof userPortfolioConfig,
"select"
>;
export type NewUserPortfolioConfig = InferModel<
typeof userPortfolioConfig,
"insert"
>;