registri dei piani in git (AI-OS#309)
Refs gitea:Adriano/AI-OS#310 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,159 @@
|
||||
### Task 6: GUID esposto — resolve, componenti e GET singolo
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/lib/content.ts`
|
||||
- Modify: `src/components/content/T.astro`
|
||||
- Modify: `src/components/content/TImg.astro`
|
||||
- Create: `src/pages/api/admin/content/[tag]/index.ts`
|
||||
- Test: `tests/content.test.ts`, `tests/content-api.test.ts`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `content_blocks.guid` (Task 1), `seedByTag` da `content-seed.ts`.
|
||||
- Produces:
|
||||
- `resolveContent(tag)` ritorna `{ value, type, classes, guid }` (`guid: string` — stringa vuota se seed-only)
|
||||
- `GET /api/admin/content/[tag]` → `{ tag, guid, type, value, styleOptions }`
|
||||
|
||||
- [ ] **Step 1: Test su resolve + GET singolo**
|
||||
|
||||
In `tests/content.test.ts` aggiungi:
|
||||
|
||||
```typescript
|
||||
it('resolveContent restituisce il guid dal DB', () => {
|
||||
const { resolveContent } = require('../src/lib/content');
|
||||
const r = resolveContent('home.hero.title');
|
||||
expect(typeof r.guid).toBe('string');
|
||||
expect(r.guid.length).toBeGreaterThan(10);
|
||||
});
|
||||
```
|
||||
|
||||
In `tests/content-api.test.ts` aggiungi un test per il GET singolo seguendo il pattern già usato nel file per gli altri endpoint (stessa costruzione di `context`/`locals.user` degli altri test del file). Struttura:
|
||||
|
||||
```typescript
|
||||
it('GET /api/admin/content/[tag] ritorna tag, guid, type, value, styleOptions', async () => {
|
||||
const { GET } = await import('../src/pages/api/admin/content/[tag]/index');
|
||||
const res = await GET({ params: { tag: 'home.hero.title' } } as any);
|
||||
expect(res.status).toBe(200);
|
||||
const body = await res.json();
|
||||
expect(body.tag).toBe('home.hero.title');
|
||||
expect(typeof body.guid).toBe('string');
|
||||
expect(body.type).toBeDefined();
|
||||
expect(Array.isArray(body.styleOptions)).toBe(true);
|
||||
});
|
||||
```
|
||||
|
||||
Se `content-api.test.ts` usa un DB di test isolato via `getDb`, riusa lo stesso setup già presente nel file (non reinventarlo).
|
||||
|
||||
- [ ] **Step 2: Esegui e verifica il fallimento**
|
||||
|
||||
Run: `npm test -- tests/content.test.ts tests/content-api.test.ts`
|
||||
Expected: FAIL — `guid` assente / modulo `[tag]/index` inesistente.
|
||||
|
||||
- [ ] **Step 3: Espandi lo store in `content.ts`**
|
||||
|
||||
Aggiorna `interface Entry` e la `resolve`:
|
||||
|
||||
```typescript
|
||||
interface Entry { value: string; type: ContentType; styles: Record<string, string>; guid: string }
|
||||
```
|
||||
|
||||
Nel `load()`, cambia la query e la costruzione:
|
||||
|
||||
```typescript
|
||||
const rows = dbGetter().prepare('SELECT tag, type, value, styles, guid FROM content_blocks').all() as
|
||||
{ tag: string; type: ContentType; value: string; styles: string; guid: string | null }[];
|
||||
cache = new Map(rows.map((r) => {
|
||||
let styles: Record<string, string> = {};
|
||||
try { styles = JSON.parse(r.styles); } catch { /* styles corrotto: si ignora */ }
|
||||
if (typeof styles !== 'object' || styles === null) styles = {};
|
||||
return [r.tag, { value: r.value, type: r.type, styles, guid: r.guid ?? '' }];
|
||||
}));
|
||||
```
|
||||
|
||||
Nel metodo `resolve`, aggiorna i tre `return`:
|
||||
|
||||
```typescript
|
||||
resolve(tag: string): { value: string; type: ContentType; classes: string; guid: string } {
|
||||
const hit = load().get(tag);
|
||||
if (hit) return { value: hit.value, type: hit.type, classes: stylesToClasses(hit.styles), guid: hit.guid };
|
||||
const seed = seedByTag.get(tag);
|
||||
if (seed) return { value: seed.value, type: seed.type, classes: '', guid: '' };
|
||||
console.warn(`[content] tag sconosciuto: ${tag}`);
|
||||
return { value: '', type: 'text', classes: '', guid: '' };
|
||||
},
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Emetti `data-guid` nei componenti**
|
||||
|
||||
In `src/components/content/T.astro`, dopo `const c = resolveContent(tag);` usa `c.guid`:
|
||||
|
||||
```astro
|
||||
{c.type === 'html'
|
||||
? <Tag data-tag={tag} data-guid={c.guid || undefined} class={classes} set:html={c.value} />
|
||||
: <Tag data-tag={tag} data-guid={c.guid || undefined} class={classes}>{c.value}</Tag>}
|
||||
```
|
||||
|
||||
In `src/components/content/TImg.astro`, aggiungi `data-guid` in entrambi i rami. Il ramo override usa `getImageOverride` che non dà il guid; recupera il guid da `resolveContent`:
|
||||
|
||||
```astro
|
||||
---
|
||||
import { Image } from 'astro:assets';
|
||||
import type { ImageMetadata } from 'astro';
|
||||
import { resolveContent } from '../../lib/content';
|
||||
interface Props {
|
||||
tag: string; src: ImageMetadata; alt: string;
|
||||
class?: string; loading?: 'lazy' | 'eager'; widths?: number[]; sizes?: string; height?: number;
|
||||
}
|
||||
const { tag, src, alt, class: cls, ...rest } = Astro.props;
|
||||
const c = resolveContent(tag);
|
||||
const override = c.type === 'image' && c.value ? `/uploads/${c.value}` : null;
|
||||
const guid = c.guid || undefined;
|
||||
---
|
||||
{override
|
||||
? <img src={override} alt={alt} class={cls} data-tag={tag} data-guid={guid} loading={rest.loading} />
|
||||
: <Image src={src} alt={alt} class={cls} data-tag={tag} data-guid={guid} {...rest} />}
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Crea l'endpoint GET singolo**
|
||||
|
||||
Crea `src/pages/api/admin/content/[tag]/index.ts`:
|
||||
|
||||
```typescript
|
||||
import type { APIRoute } from 'astro';
|
||||
import { getDb } from '../../../../../lib/db';
|
||||
import { seedByTag } from '../../../../../data/content-seed';
|
||||
export const prerender = false;
|
||||
|
||||
const json = (status: number, body: object) =>
|
||||
new Response(JSON.stringify(body), { status, headers: { 'Content-Type': 'application/json' } });
|
||||
|
||||
export const GET: APIRoute = ({ params }) => {
|
||||
const tag = params.tag ?? '';
|
||||
const row = getDb().prepare('SELECT tag, type, value, guid FROM content_blocks WHERE tag = ?').get(tag) as
|
||||
{ tag: string; type: string; value: string; guid: string | null } | undefined;
|
||||
if (!row) return json(404, { error: 'Tag inesistente.' });
|
||||
return json(200, {
|
||||
tag: row.tag,
|
||||
guid: row.guid ?? '',
|
||||
type: row.type,
|
||||
value: row.value,
|
||||
styleOptions: seedByTag.get(row.tag)?.styleOptions ?? [],
|
||||
});
|
||||
};
|
||||
```
|
||||
|
||||
Nota: convive con `PUT` di `src/pages/api/admin/content/[tag].ts` e con `POST/DELETE` di `[tag]/image.ts` — Astro risolve `/api/admin/content/<tag>` sull'endpoint `[tag].ts` (GET qui aggiunto come `[tag]/index.ts` risponde allo stesso path). **Verifica in Step 6** che il GET risponda; se il routing preferisce `[tag].ts`, sposta l'handler `GET` dentro `src/pages/api/admin/content/[tag].ts` accanto al `PUT` (stessa logica) ed elimina il file index. Decidi in base all'esito del test.
|
||||
|
||||
- [ ] **Step 6: Esegui e verifica il successo**
|
||||
|
||||
Run: `npm test -- tests/content.test.ts tests/content-api.test.ts && npm run build`
|
||||
Expected: PASS + build OK. Se il test del GET fallisce per routing, applica la nota dello Step 5 (sposta `GET` in `[tag].ts`) e rilancia.
|
||||
|
||||
- [ ] **Step 7: Commit**
|
||||
|
||||
```bash
|
||||
git add src/lib/content.ts src/components/content/T.astro src/components/content/TImg.astro src/pages/api/admin/content tests/content.test.ts tests/content-api.test.ts
|
||||
git commit -m "feat(content): guid in resolve/componenti + GET /api/admin/content/[tag]"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
Reference in New Issue
Block a user