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
@@ -0,0 +1 @@
ALTER TABLE "user" ADD COLUMN "bookmarkedTemplates" text[] DEFAULT ARRAY[]::text[];
File diff suppressed because it is too large Load Diff
+7
View File
@@ -1114,6 +1114,13 @@
"when": 1775270343231, "when": 1775270343231,
"tag": "0158_amused_synch", "tag": "0158_amused_synch",
"breakpoints": true "breakpoints": true
},
{
"idx": 159,
"version": "7",
"when": 1775274158009,
"tag": "0159_polite_puppet_master",
"breakpoints": true
} }
] ]
} }
+20 -21
View File
@@ -22,7 +22,7 @@ import {
apiUpdateUser, apiUpdateUser,
invitation, invitation,
member, member,
userTemplateBookmarks, user,
} from "@dokploy/server/db/schema"; } from "@dokploy/server/db/schema";
import { import {
hasPermission, hasPermission,
@@ -642,12 +642,12 @@ export const userRouter = createTRPCRouter({
}), }),
getBookmarkedTemplates: protectedProcedure.query(async ({ ctx }) => { getBookmarkedTemplates: protectedProcedure.query(async ({ ctx }) => {
const bookmarked = await db.query.userTemplateBookmarks.findMany({ const result = await db.query.user.findFirst({
where: eq(userTemplateBookmarks.userId, ctx.user.id), where: eq(user.id, ctx.user.id),
orderBy: [asc(userTemplateBookmarks.createdAt)], columns: { bookmarkedTemplates: true },
}); });
return bookmarked.map((b) => b.templateId); return result?.bookmarkedTemplates ?? [];
}), }),
toggleTemplateBookmark: protectedProcedure toggleTemplateBookmark: protectedProcedure
@@ -657,24 +657,23 @@ export const userRouter = createTRPCRouter({
}), }),
) )
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
const existing = await db.query.userTemplateBookmarks.findFirst({ const result = await db.query.user.findFirst({
where: and( where: eq(user.id, ctx.user.id),
eq(userTemplateBookmarks.userId, ctx.user.id), columns: { bookmarkedTemplates: true },
eq(userTemplateBookmarks.templateId, input.templateId),
),
}); });
if (existing) { const current = result?.bookmarkedTemplates ?? [];
await db const isBookmarked = current.includes(input.templateId);
.delete(userTemplateBookmarks)
.where(eq(userTemplateBookmarks.id, existing.id));
return { isBookmarked: false };
}
await db.insert(userTemplateBookmarks).values({ const updated = isBookmarked
userId: ctx.user.id, ? current.filter((id) => id !== input.templateId)
templateId: input.templateId, : [...current, input.templateId];
});
return { isBookmarked: true }; await db
.update(user)
.set({ bookmarkedTemplates: updated })
.where(eq(user.id, ctx.user.id));
return { isBookmarked: !isBookmarked };
}), }),
}); });
+5 -31
View File
@@ -1,12 +1,11 @@
import { paths } from "@dokploy/server/constants"; import { paths } from "@dokploy/server/constants";
import { relations } from "drizzle-orm"; import { relations, sql } from "drizzle-orm";
import { import {
boolean, boolean,
integer, integer,
pgTable, pgTable,
text, text,
timestamp, timestamp,
unique,
} from "drizzle-orm/pg-core"; } from "drizzle-orm/pg-core";
import { createInsertSchema } from "drizzle-zod"; import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid"; import { nanoid } from "nanoid";
@@ -67,26 +66,11 @@ export const user = pgTable("user", {
stripeSubscriptionId: text("stripeSubscriptionId"), stripeSubscriptionId: text("stripeSubscriptionId"),
serversQuantity: integer("serversQuantity").notNull().default(0), serversQuantity: integer("serversQuantity").notNull().default(0),
trustedOrigins: text("trustedOrigins").array(), 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 }) => ({ export const usersRelations = relations(user, ({ one, many }) => ({
account: one(account, { account: one(account, {
fields: [user.id], fields: [user.id],
@@ -98,25 +82,15 @@ export const usersRelations = relations(user, ({ one, many }) => ({
ssoProviders: many(ssoProvider), ssoProviders: many(ssoProvider),
backups: many(backups), backups: many(backups),
schedules: many(schedules), 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, { const createSchema = createInsertSchema(user, {
id: z.string().min(1), id: z.string().min(1),
isRegistered: z.boolean().optional(), isRegistered: z.boolean().optional(),
}).omit({ }).omit({
role: true, role: true,
trustedOrigins: true, trustedOrigins: true,
bookmarkedTemplates: true,
isValidEnterpriseLicense: true, isValidEnterpriseLicense: true,
}); });
+1 -1
View File
@@ -272,7 +272,7 @@ const parseSizeToBytes = (size: string): number => {
const match = size.match(/^([\d.]+)\s*([KMGT]?B)$/i); const match = size.match(/^([\d.]+)\s*([KMGT]?B)$/i);
if (!match) return 0; if (!match) return 0;
const value = Number.parseFloat(match[1] as string); 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> = { const multipliers: Record<string, number> = {
B: 1, B: 1,
KB: 1024, KB: 1024,