feat: add seller saved views
Quality / test (push) Canceled after 0s

This commit is contained in:
2026-09-10 01:36:15 -04:00
parent ec322df25c
commit ffd239d27c
19 changed files with 364 additions and 0 deletions
+5
View File
@@ -22,6 +22,7 @@ import { registerAdminPricingRoutes } from "../modules/pricing/routes/admin-pric
import { registerAdminProviderMappingRoutes } from "../modules/pricing/routes/admin-provider-mapping-routes.js";
import { registerAdminExportRoutes } from "../modules/exports/routes/admin-export-routes.js";
import { registerAdminSettingsRoutes } from "../modules/settings/routes/admin-settings-routes.js";
import { registerAdminSavedViewRoutes } from "../modules/search/routes/admin-saved-view-routes.js";
export function buildApp(dependencies) {
const app = Fastify();
@@ -98,6 +99,10 @@ export function buildApp(dependencies) {
...dependencies,
prefix: "/api/v1/admin",
});
app.register(registerAdminSavedViewRoutes, {
...dependencies,
prefix: "/api/v1/admin",
});
app.register(registerPublicSearchRoutes, {
...dependencies,
prefix: "/api/v1",
+6
View File
@@ -56,6 +56,8 @@ import { createAdminExportService } from "../modules/exports/services/admin-expo
import { createInventoryExportJobRepository } from "../modules/exports/repositories/inventory-export-job-repository.js";
import { createSellerSettingsRepository } from "../modules/settings/repositories/seller-settings-repository.js";
import { createSellerSettingsService } from "../modules/settings/services/seller-settings-service.js";
import { createSavedViewRepository } from "../modules/search/repositories/saved-view-repository.js";
import { createSavedViewService } from "../modules/search/services/saved-view-service.js";
function parsePort(value) {
const port = Number.parseInt(value ?? "3000", 10);
@@ -96,6 +98,9 @@ async function startServer() {
const sellerSettingsService = createSellerSettingsService({
sellerSettingsRepository: createSellerSettingsRepository(database.db),
});
const savedViewService = createSavedViewService({
savedViewRepository: createSavedViewRepository(database.db),
});
const providers = [];
if (process.env.PRICING_FIXTURE_PROVIDER_ENABLED === "true")
providers.push(createFixturePricingProvider());
@@ -171,6 +176,7 @@ async function startServer() {
inventoryItemService,
}),
sellerSettingsService,
savedViewService,
catalogLookupRepository,
publicImageService,
publicInventoryService: createPublicInventoryService({
@@ -90,6 +90,20 @@ export const inventoryExportJobs = pgTable(
],
);
export const savedViews = pgTable(
"saved_views",
{
id: uuid("id").defaultRandom().primaryKey(),
sellerId: uuid("seller_id")
.notNull()
.references(() => sellers.id, { onDelete: "cascade" }),
name: text("name").notNull(),
filters: jsonb("filters").default({}).notNull(),
...timestamps,
},
(table) => [uniqueIndex("saved_views_seller_name_unique").on(table.sellerId, table.name)],
);
export const sessions = pgTable(
"sessions",
{
@@ -0,0 +1,32 @@
import { and, asc, eq } from 'drizzle-orm';
import { savedViews } from '../../../infrastructure/database/schema.js';
export function createSavedViewRepository(db) {
return {
async listBySellerId(sellerId) {
return db.select().from(savedViews).where(eq(savedViews.sellerId, sellerId)).orderBy(asc(savedViews.name));
},
async create(sellerId, view) {
const [created] = await db.insert(savedViews).values({ sellerId, ...view }).returning();
return created;
},
async updateForSeller(id, sellerId, view) {
const [updated] = await db.update(savedViews).set({ ...view, updatedAt: new Date() }).where(and(
eq(savedViews.id, id),
eq(savedViews.sellerId, sellerId),
)).returning();
return updated ?? null;
},
async deleteForSeller(id, sellerId) {
const [deleted] = await db.delete(savedViews).where(and(
eq(savedViews.id, id),
eq(savedViews.sellerId, sellerId),
)).returning();
return deleted ?? null;
},
};
}
@@ -0,0 +1,44 @@
import { savedViewSchema } from '@pokemon-inventory/validation/saved-view';
import { createApplicationError } from '../../../shared/errors/application-error.js';
export async function registerAdminSavedViewRoutes(app, options) {
const {
authenticateAdmin = async () => {
throw createApplicationError('UNAUTHORIZED', 'Authentication is required.', 401);
},
authorizeUnsafeRequest,
sessionCookieName = 'pokemon_inventory_session',
savedViewService,
} = options;
app.addHook('preHandler', async (request) => {
const admin = await authenticateAdmin(request);
if (!admin?.sellerId) throw createApplicationError('UNAUTHORIZED', 'Authentication is required.', 401);
request.admin = admin;
});
app.get('/saved-views', async (request) => ({
data: await savedViewService.list({ sellerId: request.admin.sellerId }),
}));
app.post('/saved-views', async (request, reply) => {
await authorizeUnsafeRequest?.(request.cookies[sessionCookieName], request.headers['x-csrf-token']);
const result = savedViewSchema.safeParse(request.body);
if (!result.success) return reply.code(400).send({ error: { code: 'VALIDATION_ERROR', message: 'Request validation failed.' } });
return reply.code(201).send({ data: await savedViewService.create({ sellerId: request.admin.sellerId, view: result.data }) });
});
app.patch('/saved-views/:id', async (request, reply) => {
await authorizeUnsafeRequest?.(request.cookies[sessionCookieName], request.headers['x-csrf-token']);
const result = savedViewSchema.safeParse(request.body);
if (!result.success) return reply.code(400).send({ error: { code: 'VALIDATION_ERROR', message: 'Request validation failed.' } });
return reply.send({ data: await savedViewService.update({ sellerId: request.admin.sellerId, viewId: request.params.id, view: result.data }) });
});
app.delete('/saved-views/:id', async (request, reply) => {
await authorizeUnsafeRequest?.(request.cookies[sessionCookieName], request.headers['x-csrf-token']);
await savedViewService.delete({ sellerId: request.admin.sellerId, viewId: request.params.id });
return reply.code(204).send();
});
}
@@ -0,0 +1,27 @@
import { savedViewSchema } from '@pokemon-inventory/validation/saved-view';
import { createApplicationError } from '../../../shared/errors/application-error.js';
export function createSavedViewService({ savedViewRepository }) {
return {
async list({ sellerId }) {
return savedViewRepository.listBySellerId(sellerId);
},
async create({ sellerId, view }) {
return savedViewRepository.create(sellerId, savedViewSchema.parse(view));
},
async update({ sellerId, viewId, view }) {
const updated = await savedViewRepository.updateForSeller(viewId, sellerId, savedViewSchema.parse(view));
if (!updated) throw createApplicationError('SAVED_VIEW_NOT_FOUND', 'Saved view was not found.', 404);
return updated;
},
async delete({ sellerId, viewId }) {
const deleted = await savedViewRepository.deleteForSeller(viewId, sellerId);
if (!deleted) throw createApplicationError('SAVED_VIEW_NOT_FOUND', 'Saved view was not found.', 404);
return deleted;
},
};
}