feat(user): add bookmarkedTemplates column to user table and update related API methods

- Introduced a new column `bookmarkedTemplates` to the user table to store user-specific template bookmarks.
- Updated API methods to manage bookmarked templates, replacing the deprecated user_template_bookmarks table.
- Adjusted queries to retrieve and toggle bookmarks directly from the user record.
This commit is contained in:
Mauricio Siu
2026-04-03 21:50:12 -06:00
parent d8e15a60f0
commit 2eb460ba63
6 changed files with 8311 additions and 53 deletions

View File

@@ -0,0 +1 @@
ALTER TABLE "user" ADD COLUMN "bookmarkedTemplates" text[] DEFAULT ARRAY[]::text[];

File diff suppressed because it is too large Load Diff

View File

@@ -1114,6 +1114,13 @@
"when": 1775270343231,
"tag": "0158_amused_synch",
"breakpoints": true
},
{
"idx": 159,
"version": "7",
"when": 1775274158009,
"tag": "0159_polite_puppet_master",
"breakpoints": true
}
]
}

View File

@@ -22,7 +22,7 @@ import {
apiUpdateUser,
invitation,
member,
userTemplateBookmarks,
user,
} from "@dokploy/server/db/schema";
import {
hasPermission,
@@ -642,12 +642,12 @@ export const userRouter = createTRPCRouter({
}),
getBookmarkedTemplates: protectedProcedure.query(async ({ ctx }) => {
const bookmarked = await db.query.userTemplateBookmarks.findMany({
where: eq(userTemplateBookmarks.userId, ctx.user.id),
orderBy: [asc(userTemplateBookmarks.createdAt)],
const result = await db.query.user.findFirst({
where: eq(user.id, ctx.user.id),
columns: { bookmarkedTemplates: true },
});
return bookmarked.map((b) => b.templateId);
return result?.bookmarkedTemplates ?? [];
}),
toggleTemplateBookmark: protectedProcedure
@@ -657,24 +657,23 @@ export const userRouter = createTRPCRouter({
}),
)
.mutation(async ({ input, ctx }) => {
const existing = await db.query.userTemplateBookmarks.findFirst({
where: and(
eq(userTemplateBookmarks.userId, ctx.user.id),
eq(userTemplateBookmarks.templateId, input.templateId),
),
const result = await db.query.user.findFirst({
where: eq(user.id, ctx.user.id),
columns: { bookmarkedTemplates: true },
});
if (existing) {
await db
.delete(userTemplateBookmarks)
.where(eq(userTemplateBookmarks.id, existing.id));
return { isBookmarked: false };
}
const current = result?.bookmarkedTemplates ?? [];
const isBookmarked = current.includes(input.templateId);
await db.insert(userTemplateBookmarks).values({
userId: ctx.user.id,
templateId: input.templateId,
});
return { isBookmarked: true };
const updated = isBookmarked
? current.filter((id) => id !== input.templateId)
: [...current, input.templateId];
await db
.update(user)
.set({ bookmarkedTemplates: updated })
.where(eq(user.id, ctx.user.id));
return { isBookmarked: !isBookmarked };
}),
});

View File

@@ -1,12 +1,11 @@
import { paths } from "@dokploy/server/constants";
import { relations } from "drizzle-orm";
import { relations, sql } from "drizzle-orm";
import {
boolean,
integer,
pgTable,
text,
timestamp,
unique,
} from "drizzle-orm/pg-core";
import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid";
@@ -67,26 +66,11 @@ export const user = pgTable("user", {
stripeSubscriptionId: text("stripeSubscriptionId"),
serversQuantity: integer("serversQuantity").notNull().default(0),
trustedOrigins: text("trustedOrigins").array(),
bookmarkedTemplates: text("bookmarkedTemplates")
.array()
.default(sql`ARRAY[]::text[]`),
});
export const userTemplateBookmarks = pgTable(
"user_template_bookmarks",
{
id: text("id")
.notNull()
.primaryKey()
.$defaultFn(() => nanoid()),
userId: text("userId")
.notNull()
.references(() => user.id, { onDelete: "cascade" }),
templateId: text("templateId").notNull(),
createdAt: timestamp("createdAt").notNull().defaultNow(),
},
(table) => ({
uniqueUserTemplate: unique().on(table.userId, table.templateId),
}),
);
export const usersRelations = relations(user, ({ one, many }) => ({
account: one(account, {
fields: [user.id],
@@ -98,25 +82,15 @@ export const usersRelations = relations(user, ({ one, many }) => ({
ssoProviders: many(ssoProvider),
backups: many(backups),
schedules: many(schedules),
templateBookmarks: many(userTemplateBookmarks),
}));
export const userTemplateBookmarksRelations = relations(
userTemplateBookmarks,
({ one }) => ({
user: one(user, {
fields: [userTemplateBookmarks.userId],
references: [user.id],
}),
}),
);
const createSchema = createInsertSchema(user, {
id: z.string().min(1),
isRegistered: z.boolean().optional(),
}).omit({
role: true,
trustedOrigins: true,
bookmarkedTemplates: true,
isValidEnterpriseLicense: true,
});

View File

@@ -272,7 +272,7 @@ const parseSizeToBytes = (size: string): number => {
const match = size.match(/^([\d.]+)\s*([KMGT]?B)$/i);
if (!match) return 0;
const value = Number.parseFloat(match[1] as string);
const unit = match[2].toUpperCase();
const unit = (match[2] as string).toUpperCase();
const multipliers: Record<string, number> = {
B: 1,
KB: 1024,