Spaces:
Sleeping
Sleeping
Download artifacts/api-server/src/routes/articles.ts from Tahasaif3/DataExtract: direct link, hf CLI and curl.
- Browser
- Download file 6 kB
-
https://huggingface.co/spaces/Tahasaif3/DataExtract/resolve/main/artifacts/api-server/src/routes/articles.ts
- Command line
-
hf download hf://spaces/Tahasaif3/DataExtract/artifacts/api-server/src/routes/articles.ts
-
curl -L -o articles.ts https://huggingface.co/spaces/Tahasaif3/DataExtract/resolve/main/artifacts/api-server/src/routes/articles.ts
6 kB
| import { Router, raw, type Request, type Response } from 'express'; | |
| import { and, desc, eq, inArray } from 'drizzle-orm'; | |
| import { db, articlesTable } from '@workspace/db'; | |
| import { SaveArticleBody } from '@workspace/api-zod'; | |
| import sanitizeHtml from 'sanitize-html'; | |
| import { articleImageFile, saveArticleImage } from '../lib/articleImages'; | |
| export const publicArticlesRouter = Router(); | |
| export const adminArticlesRouter = Router(); | |
| const samples = ['s1', 's2', 's3', 's4', 's5', 's6', 's7', 's8']; | |
| const sampleSlugs = ['shipping-policy-that-builds-trust', 'find-products-customers-love', 'first-100-orders', 'choosing-a-dropshipping-supplier', 'international-delivery-basics', 'small-shop-product-photography', 'choosing-an-ecommerce-platform', 'supplier-sample-checklist']; | |
| publicArticlesRouter.get('/articles', async (_req, res) => { | |
| res.setHeader('Cache-Control', 'no-store'); | |
| const articles = await db.select().from(articlesTable).where(eq(articlesTable.status, 'Published')).orderBy(desc(articlesTable.publishedAt)); | |
| const overrides = await db.select({ id: articlesTable.id }).from(articlesTable).where(inArray(articlesTable.id, samples)); | |
| res.json({ articles, overriddenSamples: overrides.map(row => row.id) }); | |
| }); | |
| publicArticlesRouter.get('/article-images/:id', async (req, res) => { | |
| if (!/^[a-f0-9-]{36}$/.test(req.params.id)) { res.sendStatus(404); return; } | |
| const file = articleImageFile(req.params.id); | |
| try { | |
| const [metadata] = await file.getMetadata(); | |
| res.setHeader('Content-Type', metadata.contentType || 'application/octet-stream'); | |
| res.setHeader('X-Content-Type-Options', 'nosniff'); | |
| res.setHeader('Cache-Control', 'public, max-age=31536000, immutable'); | |
| file.createReadStream().on('error', () => { if (!res.headersSent) res.sendStatus(404); else res.destroy(); }).pipe(res); | |
| } catch { | |
| res.sendStatus(404); | |
| } | |
| }); | |
| // Mounted only after the existing Clerk admin-role guard. | |
| adminArticlesRouter.get('/articles', async (_req, res) => { | |
| res.json(await db.select().from(articlesTable).orderBy(desc(articlesTable.updatedAt))); | |
| }); | |
| adminArticlesRouter.put('/articles/:id', async (req, res) => { | |
| const id = req.params.id; | |
| if (!/^(?:s[1-8]|[a-f0-9-]{36})$/.test(id)) { res.status(400).json({ error: 'Invalid article identifier.' }); return; } | |
| const parsed = SaveArticleBody.safeParse(req.body); | |
| if (!parsed.success) { res.status(400).json({ error: 'Check the title, slug, author and article fields.' }); return; } | |
| const input = parsed.data; | |
| if (sampleSlugs.includes(input.slug) && samples[sampleSlugs.indexOf(input.slug)] !== id) { | |
| res.status(409).json({ error: 'That slug belongs to an existing sample article. Choose a different slug.' }); return; | |
| } | |
| if (!input.title.trim() || !input.author.trim()) { res.status(400).json({ error: 'Title and author are required.' }); return; } | |
| const content = sanitizeHtml(input.content, { | |
| allowedTags: ['p', 'br', 'strong', 'b', 'em', 'i', 'u', 's', 'strike', 'h1', 'h2', 'h3', 'ul', 'ol', 'li', 'blockquote', 'hr', 'a', 'span', 'mark', 'pre', 'code'], | |
| allowedAttributes: { '*': ['style'], a: ['href', 'title', 'rel', 'target'], ol: ['start'], mark: ['data-color'] }, | |
| allowedSchemes: ['http', 'https', 'mailto', 'tel'], | |
| allowedStyles: { '*': { | |
| 'text-align': [/^(left|center|right|justify)$/], | |
| 'color': [/^(#[a-f0-9]{3,8}|[a-z]+|rgba?\([\d., %]+\))$/i], | |
| 'background-color': [/^(#[a-f0-9]{3,8}|[a-z]+|rgba?\([\d., %]+\))$/i], | |
| 'font-size': [/^\d{1,3}(px|pt|rem|em)$/], | |
| 'font-family': [/^[a-zA-Z ,'"-]+$/], | |
| } }, | |
| }); | |
| if (input.status === 'Published' && !sanitizeHtml(content, { allowedTags: [], allowedAttributes: {} }).trim()) { | |
| res.status(400).json({ error: 'Add article content before publishing.' }); return; | |
| } | |
| // Only durable uploaded images or existing bundled sample assets are accepted. | |
| if (input.image && !/^\/api\/article-images\/[a-f0-9-]{36}$/.test(input.image) | |
| && !(samples.includes(id) && /^\/(?:@fs\/|assets\/|src\/)/.test(input.image))) { | |
| res.status(400).json({ error: 'Upload the featured image before saving.' }); return; | |
| } | |
| const [existing] = await db.select().from(articlesTable).where(eq(articlesTable.id, id)); | |
| if ((existing?.version || 0) !== input.version) { | |
| res.status(409).json({ error: 'This article changed in another session. Copy your changes before reloading.' }); return; | |
| } | |
| const values = { | |
| ...input, content, title: input.title.trim(), author: input.author.trim(), | |
| version: input.version + 1, updatedAt: new Date(), | |
| publishedAt: input.status === 'Published' ? existing?.publishedAt || new Date() : null, | |
| }; | |
| try { | |
| const [article] = existing | |
| ? await db.update(articlesTable).set(values).where(and(eq(articlesTable.id, id), eq(articlesTable.version, input.version))).returning() | |
| : await db.insert(articlesTable).values({ ...values, id }).returning(); | |
| if (!article) { res.status(409).json({ error: 'Another session saved this article. Copy your changes before reloading.' }); return; } | |
| res.json(article); | |
| } catch (error) { | |
| const code = (error as { cause?: { code?: string }; code?: string }).cause?.code || (error as { code?: string }).code; | |
| if (code === '23505') { res.status(409).json({ error: 'That article slug already exists. Choose a different slug.' }); return; } | |
| req.log.error('Article save failed.'); | |
| res.status(503).json({ error: 'Article could not be saved. Your changes remain in the editor. Please retry.' }); | |
| } | |
| }); | |
| adminArticlesRouter.post('/article-images', raw({ type: ['image/jpeg', 'image/png', 'image/webp', 'image/gif'], limit: '5mb' }), async (req: Request, res: Response) => { | |
| if (!Buffer.isBuffer(req.body) || !req.body.length) { res.status(400).json({ error: 'Choose a supported image up to 5 MB.' }); return; } | |
| try { | |
| res.json({ image: await saveArticleImage(req.body) }); | |
| } catch { | |
| res.status(400).json({ error: 'Image upload failed. Check the file and try again.' }); | |
| } | |
| }); | |