|
|
@@ -12,7 +12,7 @@ export interface DomesticSnapshotProvider {
|
|
|
}
|
|
|
|
|
|
const syncRequestSchema = z.object({
|
|
|
- workspaceId: z.string().min(1).default('demashi'),
|
|
|
+ workspaceId: z.string().min(1).optional(),
|
|
|
platform: z.literal('jd').default('jd'),
|
|
|
productIds: z.array(z.string().min(1)).min(1).max(100),
|
|
|
scopes: z.array(z.enum(['product', 'reviews'])).min(1).default(['product', 'reviews']),
|
|
|
@@ -20,7 +20,7 @@ const syncRequestSchema = z.object({
|
|
|
});
|
|
|
|
|
|
const snapshotQuerySchema = z.object({
|
|
|
- workspaceId: z.string().min(1).default('demashi'),
|
|
|
+ workspaceId: z.string().min(1).optional(),
|
|
|
platform: z.literal('jd').default('jd'),
|
|
|
});
|
|
|
|
|
|
@@ -30,18 +30,21 @@ export function createDomesticVocRouter(dependencies: {
|
|
|
snapshot: DomesticSnapshotProvider;
|
|
|
catalog: PlatformRepository;
|
|
|
access: WorkspaceAccessService;
|
|
|
+ defaultWorkspaceId: string;
|
|
|
}): Router {
|
|
|
const router = Router();
|
|
|
|
|
|
router.post('/sync', async (request, response, next) => {
|
|
|
try {
|
|
|
const input = syncRequestSchema.parse(request.body);
|
|
|
- await dependencies.access.require(request, input.workspaceId, 'data:sync');
|
|
|
+ const workspaceId = input.workspaceId ?? dependencies.defaultWorkspaceId;
|
|
|
+ await dependencies.access.require(request, workspaceId, 'data:sync');
|
|
|
const headerIdempotencyKey = request.header('Idempotency-Key')?.trim();
|
|
|
const { idempotencyKey: bodyIdempotencyKey, ...requiredInput } = input;
|
|
|
const idempotencyKey = headerIdempotencyKey || bodyIdempotencyKey;
|
|
|
const syncInput = {
|
|
|
...requiredInput,
|
|
|
+ workspaceId,
|
|
|
...(idempotencyKey ? { idempotencyKey } : {}),
|
|
|
};
|
|
|
const job = await dependencies.sync.enqueue(syncInput);
|
|
|
@@ -50,7 +53,7 @@ export function createDomesticVocRouter(dependencies: {
|
|
|
return;
|
|
|
}
|
|
|
await dependencies.catalog.appendAudit({
|
|
|
- workspaceId: input.workspaceId,
|
|
|
+ workspaceId,
|
|
|
actorUserId: getPrincipal(request).userId,
|
|
|
action: 'sync.requested',
|
|
|
entityType: 'sync_job',
|
|
|
@@ -82,11 +85,64 @@ export function createDomesticVocRouter(dependencies: {
|
|
|
}
|
|
|
});
|
|
|
|
|
|
+ router.post('/jobs/:id/retry', async (request, response, next) => {
|
|
|
+ try {
|
|
|
+ const id = z.uuid().parse(request.params.id);
|
|
|
+ const current = await dependencies.jobs.findByPublicId(id);
|
|
|
+ if (!current) {
|
|
|
+ response.status(404).json({ error: 'job_not_found' });
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ await dependencies.access.require(request, current.workspaceId, 'data:sync');
|
|
|
+ const job = await dependencies.jobs.retry(current.workspaceId, id);
|
|
|
+ if (!job) {
|
|
|
+ response.status(409).json({ error: 'sync_job_not_retryable' });
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ await dependencies.catalog.appendAudit({
|
|
|
+ workspaceId: job.workspaceId,
|
|
|
+ actorUserId: getPrincipal(request).userId,
|
|
|
+ action: 'sync.retried',
|
|
|
+ entityType: 'sync_job',
|
|
|
+ entityId: job.id,
|
|
|
+ metadata: { status: job.status },
|
|
|
+ });
|
|
|
+ response.status(202).json({ job });
|
|
|
+ } catch (error) { next(error); }
|
|
|
+ });
|
|
|
+
|
|
|
+ router.post('/jobs/:id/cancel', async (request, response, next) => {
|
|
|
+ try {
|
|
|
+ const id = z.uuid().parse(request.params.id);
|
|
|
+ const current = await dependencies.jobs.findByPublicId(id);
|
|
|
+ if (!current) {
|
|
|
+ response.status(404).json({ error: 'job_not_found' });
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ await dependencies.access.require(request, current.workspaceId, 'data:sync');
|
|
|
+ const job = await dependencies.jobs.cancel(current.workspaceId, id);
|
|
|
+ if (!job) {
|
|
|
+ response.status(409).json({ error: 'sync_job_not_cancellable' });
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ await dependencies.catalog.appendAudit({
|
|
|
+ workspaceId: job.workspaceId,
|
|
|
+ actorUserId: getPrincipal(request).userId,
|
|
|
+ action: 'sync.cancelled',
|
|
|
+ entityType: 'sync_job',
|
|
|
+ entityId: job.id,
|
|
|
+ metadata: { status: job.status },
|
|
|
+ });
|
|
|
+ response.json({ job });
|
|
|
+ } catch (error) { next(error); }
|
|
|
+ });
|
|
|
+
|
|
|
router.get('/snapshot', async (request, response, next) => {
|
|
|
try {
|
|
|
const query = snapshotQuerySchema.parse(request.query);
|
|
|
- await dependencies.access.require(request, query.workspaceId, 'workspace:read');
|
|
|
- const snapshot = await dependencies.snapshot.getSnapshot(query.workspaceId, query.platform);
|
|
|
+ const workspaceId = query.workspaceId ?? dependencies.defaultWorkspaceId;
|
|
|
+ await dependencies.access.require(request, workspaceId, 'workspace:read');
|
|
|
+ const snapshot = await dependencies.snapshot.getSnapshot(workspaceId, query.platform);
|
|
|
if (!snapshot) {
|
|
|
response.status(404).json({ error: 'workspace_not_found' });
|
|
|
return;
|
|
|
@@ -109,9 +165,10 @@ export function createDomesticVocRouter(dependencies: {
|
|
|
role: z.enum(['own', 'competitor']).optional(),
|
|
|
category: z.string().max(200).default(''),
|
|
|
}).parse(request.query);
|
|
|
- await dependencies.access.require(request, query.workspaceId, 'workspace:read');
|
|
|
+ const workspaceId = query.workspaceId ?? dependencies.defaultWorkspaceId;
|
|
|
+ await dependencies.access.require(request, workspaceId, 'workspace:read');
|
|
|
response.json(await dependencies.catalog.listProducts({
|
|
|
- workspaceId: query.workspaceId,
|
|
|
+ workspaceId,
|
|
|
platform: query.platform,
|
|
|
limit: query.limit,
|
|
|
cursor: query.cursor ?? null,
|
|
|
@@ -126,9 +183,10 @@ export function createDomesticVocRouter(dependencies: {
|
|
|
try {
|
|
|
const query = catalogQuerySchema.parse(request.query);
|
|
|
const productId = z.string().min(1).max(200).parse(request.params.productId);
|
|
|
- await dependencies.access.require(request, query.workspaceId, 'workspace:read');
|
|
|
+ const workspaceId = query.workspaceId ?? dependencies.defaultWorkspaceId;
|
|
|
+ await dependencies.access.require(request, workspaceId, 'workspace:read');
|
|
|
response.json(await dependencies.catalog.listReviews({
|
|
|
- workspaceId: query.workspaceId, platform: query.platform, productId,
|
|
|
+ workspaceId, platform: query.platform, productId,
|
|
|
limit: query.limit, cursor: query.cursor ?? null,
|
|
|
}));
|
|
|
} catch (error) { next(error); }
|
|
|
@@ -138,8 +196,9 @@ export function createDomesticVocRouter(dependencies: {
|
|
|
try {
|
|
|
const query = snapshotQuerySchema.parse(request.query);
|
|
|
const productId = z.string().min(1).max(200).parse(request.params.productId);
|
|
|
- await dependencies.access.require(request, query.workspaceId, 'workspace:read');
|
|
|
- const product = await dependencies.catalog.getProduct(query.workspaceId, query.platform, productId);
|
|
|
+ const workspaceId = query.workspaceId ?? dependencies.defaultWorkspaceId;
|
|
|
+ await dependencies.access.require(request, workspaceId, 'workspace:read');
|
|
|
+ const product = await dependencies.catalog.getProduct(workspaceId, query.platform, productId);
|
|
|
if (!product) {
|
|
|
response.status(404).json({ error: 'product_not_found' });
|
|
|
return;
|
|
|
@@ -151,9 +210,10 @@ export function createDomesticVocRouter(dependencies: {
|
|
|
router.get('/relations', async (request, response, next) => {
|
|
|
try {
|
|
|
const query = catalogQuerySchema.parse(request.query);
|
|
|
- await dependencies.access.require(request, query.workspaceId, 'workspace:read');
|
|
|
+ const workspaceId = query.workspaceId ?? dependencies.defaultWorkspaceId;
|
|
|
+ await dependencies.access.require(request, workspaceId, 'workspace:read');
|
|
|
response.json(await dependencies.catalog.listRelations({
|
|
|
- workspaceId: query.workspaceId, platform: query.platform,
|
|
|
+ workspaceId, platform: query.platform,
|
|
|
limit: query.limit, cursor: query.cursor ?? null,
|
|
|
}));
|
|
|
} catch (error) { next(error); }
|
|
|
@@ -161,10 +221,13 @@ export function createDomesticVocRouter(dependencies: {
|
|
|
|
|
|
router.get('/jobs', async (request, response, next) => {
|
|
|
try {
|
|
|
- const query = catalogQuerySchema.extend({ status: z.string().max(40).default('') }).parse(request.query);
|
|
|
- await dependencies.access.require(request, query.workspaceId, 'workspace:read');
|
|
|
+ const query = catalogQuerySchema.extend({
|
|
|
+ status: z.enum(['pending', 'processing', 'completed', 'partial', 'failed', 'cancelled']).or(z.literal('')).default(''),
|
|
|
+ }).parse(request.query);
|
|
|
+ const workspaceId = query.workspaceId ?? dependencies.defaultWorkspaceId;
|
|
|
+ await dependencies.access.require(request, workspaceId, 'workspace:read');
|
|
|
response.json(await dependencies.catalog.listJobs({
|
|
|
- workspaceId: query.workspaceId, limit: query.limit, cursor: query.cursor ?? null, status: query.status,
|
|
|
+ workspaceId, limit: query.limit, cursor: query.cursor ?? null, status: query.status,
|
|
|
}));
|
|
|
} catch (error) { next(error); }
|
|
|
});
|
|
|
@@ -173,8 +236,9 @@ export function createDomesticVocRouter(dependencies: {
|
|
|
try {
|
|
|
const query = snapshotQuerySchema.pick({ workspaceId: true }).parse(request.query);
|
|
|
const id = z.uuid().parse(request.params.id);
|
|
|
- await dependencies.access.require(request, query.workspaceId, 'workspace:read');
|
|
|
- response.json({ items: await dependencies.catalog.listJobEvents(query.workspaceId, id) });
|
|
|
+ const workspaceId = query.workspaceId ?? dependencies.defaultWorkspaceId;
|
|
|
+ await dependencies.access.require(request, workspaceId, 'workspace:read');
|
|
|
+ response.json({ items: await dependencies.catalog.listJobEvents(workspaceId, id) });
|
|
|
} catch (error) { next(error); }
|
|
|
});
|
|
|
|