add item by url 1st step

This commit is contained in:
Jurgis Sakalauskas
2025-10-13 15:46:04 +03:00
parent afc3da3e65
commit ff18d0daeb
7 changed files with 448 additions and 105 deletions
@@ -11,6 +11,7 @@ import { resolveUrlId } from './url-helpers.js';
import { parsePaginationParams, parseUuid } from '../validation/common.js';
import {
parseItemCreatePayload,
parseItemImportPayload,
parseItemStatusFilter,
parseItemUpdatePayload
} from '../validation/items.js';
@@ -120,3 +121,64 @@ export const deleteItem = async (req: Request, res: Response) => {
res.status(204).send();
};
export const importItemFromUrl = async (req: Request, res: Response) => {
await new Promise((resolve) => {
setTimeout(resolve, 1000);
})
const projectId = parseUuid(req.params.projectId, 'projectId');
// projectId is validated but not otherwise used for the mock response.
void projectId;
const { url } = parseItemImportPayload(req.body);
const loweredUrl = url.toLowerCase();
if (loweredUrl.includes('fail') || loweredUrl.includes('error')) {
throw new HttpError(
422,
'We could not read that URL. Please enter the item details manually.'
);
}
let hostname: string | null = null;
let lastPathSegment: string | null = null;
try {
const parsed = new URL(url);
hostname = parsed.hostname.replace(/^www\./, '');
const pathSegments = parsed.pathname.split('/').filter(Boolean);
if (pathSegments.length) {
lastPathSegment = pathSegments[pathSegments.length - 1];
}
} catch {
// non-standard URLs fall back to generic mock content
}
const manufacturerBase = hostname ? hostname.split('.')[0] : 'imported';
const manufacturer =
manufacturerBase.charAt(0).toUpperCase() + manufacturerBase.slice(1);
const cleanedSegment =
lastPathSegment?.replace(/[-_]/g, ' ').replace(/\s+/g, ' ').trim() ?? 'Item';
const model = cleanedSegment.length ? cleanedSegment : 'Imported Item';
const attributes: Record<string, unknown> = {
originUrl: url,
importedAt: new Date().toISOString(),
condition: 'New',
availability: 'In stock',
dpi: 1200,
};
if (hostname) {
attributes.domain = hostname;
}
res.json({
data: {
manufacturer,
model,
note: null,
attributes
}
});
};
+2
View File
@@ -3,6 +3,7 @@ import {
createItem,
deleteItem,
getItem,
importItemFromUrl,
listItems,
updateItem
} from '../controllers/items-controller.js';
@@ -33,6 +34,7 @@ apiRouter.delete('/projects/:projectId', asyncHandler(deleteProject));
// Project items
apiRouter.get('/projects/:projectId/items', asyncHandler(listItems));
apiRouter.post('/projects/:projectId/items/import', asyncHandler(importItemFromUrl));
apiRouter.post('/projects/:projectId/items', asyncHandler(createItem));
// Items
+18
View File
@@ -183,3 +183,21 @@ export const parseItemStatusFilter = (
}
return value as 'active' | 'rejected';
};
export interface ItemImportPayload {
url: string;
}
export const parseItemImportPayload = (payload: unknown): ItemImportPayload => {
if (payload === null || typeof payload !== 'object') {
throw new HttpError(400, 'Body must be an object');
}
const { url } = payload as Record<string, unknown>;
if (typeof url !== 'string' || url.trim().length === 0) {
throw new HttpError(400, 'url must be a non-empty string');
}
return { url: url.trim() };
};