mirror of
https://github.com/Hestia-Homes/assessment-model.git
synced 2026-07-12 13:28:55 +00:00
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:
commit
ce6a6ffa2d
4 changed files with 12171 additions and 0 deletions
27
src/app/db/migrations/0264_yummy_master_chief.sql
Normal file
27
src/app/db/migrations/0264_yummy_master_chief.sql
Normal 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");
|
||||
12020
src/app/db/migrations/meta/0264_snapshot.json
Normal file
12020
src/app/db/migrations/meta/0264_snapshot.json
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
117
src/app/db/schema/user_portfolio_config.ts
Normal file
117
src/app/db/schema/user_portfolio_config.ts
Normal 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"
|
||||
>;
|
||||
Loading…
Add table
Reference in a new issue