@@ -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",
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user