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,225 @@
|
||||
# Review package: c3267c9..5ce10da
|
||||
|
||||
## Commits
|
||||
5ce10da feat(content): guid in resolve/componenti + GET /api/admin/content/[tag]
|
||||
|
||||
## Files changed
|
||||
src/components/content/T.astro | 4 ++--
|
||||
src/components/content/TImg.astro | 10 ++++++----
|
||||
src/lib/content.ts | 16 ++++++++--------
|
||||
src/pages/api/admin/content/[tag].ts | 15 +++++++++++++++
|
||||
tests/content-api.test.ts | 13 +++++++++++++
|
||||
tests/content.test.ts | 8 +++++++-
|
||||
6 files changed, 51 insertions(+), 15 deletions(-)
|
||||
|
||||
## Diff
|
||||
diff --git a/src/components/content/T.astro b/src/components/content/T.astro
|
||||
index 41cefd3..b11d23e 100644
|
||||
--- a/src/components/content/T.astro
|
||||
+++ b/src/components/content/T.astro
|
||||
@@ -1,11 +1,11 @@
|
||||
---
|
||||
import { resolveContent } from '../../lib/content';
|
||||
interface Props { tag: string; as?: string; class?: string }
|
||||
const { tag, as = 'span', class: cls } = Astro.props;
|
||||
const Tag = as;
|
||||
const c = resolveContent(tag);
|
||||
const classes = [cls, c.classes].filter(Boolean).join(' ') || undefined;
|
||||
---
|
||||
{c.type === 'html'
|
||||
- ? <Tag data-tag={tag} class={classes} set:html={c.value} />
|
||||
- : <Tag data-tag={tag} class={classes}>{c.value}</Tag>}
|
||||
+ ? <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>}
|
||||
diff --git a/src/components/content/TImg.astro b/src/components/content/TImg.astro
|
||||
index 3fcb915..b1f6ac3 100644
|
||||
--- a/src/components/content/TImg.astro
|
||||
+++ b/src/components/content/TImg.astro
|
||||
@@ -1,14 +1,16 @@
|
||||
---
|
||||
import { Image } from 'astro:assets';
|
||||
import type { ImageMetadata } from 'astro';
|
||||
-import { getImageOverride } from '../../lib/content';
|
||||
+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 override = getImageOverride(tag);
|
||||
+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} loading={rest.loading} />
|
||||
- : <Image src={src} alt={alt} class={cls} data-tag={tag} {...rest} />}
|
||||
+ ? <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} />}
|
||||
diff --git a/src/lib/content.ts b/src/lib/content.ts
|
||||
index dbdafee..db2a539 100644
|
||||
--- a/src/lib/content.ts
|
||||
+++ b/src/lib/content.ts
|
||||
@@ -1,48 +1,48 @@
|
||||
import type Database from 'better-sqlite3';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import { getDb } from './db';
|
||||
import { contentSeed, seedByTag, type ContentType } from '../data/content-seed';
|
||||
import { stylesToClasses } from './style-presets';
|
||||
|
||||
-interface Entry { value: string; type: ContentType; styles: Record<string, string> }
|
||||
+interface Entry { value: string; type: ContentType; styles: Record<string, string>; guid: string }
|
||||
|
||||
export function syncSeed(db: Database.Database): void {
|
||||
const ins = db.prepare('INSERT OR IGNORE INTO content_blocks (tag, type, value, guid) VALUES (?, ?, ?, ?)');
|
||||
const tx = db.transaction(() => { for (const e of contentSeed) ins.run(e.tag, e.type, e.value, randomUUID()); });
|
||||
tx();
|
||||
}
|
||||
|
||||
export function makeContentStore(dbGetter: () => Database.Database) {
|
||||
let cache: Map<string, Entry> | null = null;
|
||||
|
||||
const load = (): Map<string, Entry> => {
|
||||
if (cache) return cache;
|
||||
- const rows = dbGetter().prepare('SELECT tag, type, value, styles FROM content_blocks').all() as
|
||||
- { tag: string; type: ContentType; value: string; styles: string }[];
|
||||
+ 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 }];
|
||||
+ return [r.tag, { value: r.value, type: r.type, styles, guid: r.guid ?? '' }];
|
||||
}));
|
||||
return cache;
|
||||
};
|
||||
|
||||
return {
|
||||
- resolve(tag: string): { value: string; type: ContentType; classes: string } {
|
||||
+ 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) };
|
||||
+ 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: '' };
|
||||
+ if (seed) return { value: seed.value, type: seed.type, classes: '', guid: '' };
|
||||
console.warn(`[content] tag sconosciuto: ${tag}`);
|
||||
- return { value: '', type: 'text', classes: '' };
|
||||
+ return { value: '', type: 'text', classes: '', guid: '' };
|
||||
},
|
||||
invalidate(): void { cache = null; },
|
||||
};
|
||||
}
|
||||
|
||||
const store = makeContentStore(getDb);
|
||||
|
||||
export const resolveContent = (tag: string) => store.resolve(tag);
|
||||
export const t = (tag: string): string => store.resolve(tag).value;
|
||||
export const invalidateContentCache = (): void => store.invalidate();
|
||||
diff --git a/src/pages/api/admin/content/[tag].ts b/src/pages/api/admin/content/[tag].ts
|
||||
index 1fd7c0b..8ad2488 100644
|
||||
--- a/src/pages/api/admin/content/[tag].ts
|
||||
+++ b/src/pages/api/admin/content/[tag].ts
|
||||
@@ -1,19 +1,34 @@
|
||||
import type { APIRoute } from 'astro';
|
||||
import { getDb } from '../../../../lib/db';
|
||||
import { applyContentUpdate } from '../../../../lib/content-update';
|
||||
import { invalidateContentCache } from '../../../../lib/content';
|
||||
+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 ?? [],
|
||||
+ });
|
||||
+};
|
||||
+
|
||||
export const PUT: APIRoute = async ({ params, request, locals }) => {
|
||||
let data: Record<string, unknown>;
|
||||
try { data = await request.json(); } catch { return json(400, { error: 'Dati non validi.' }); }
|
||||
if (typeof data !== 'object' || data === null) return json(400, { error: 'Dati non validi.' });
|
||||
const r = applyContentUpdate(getDb(), params.tag ?? '', data, locals.user!.username);
|
||||
if (!r.ok) return json(r.status, { error: r.error });
|
||||
invalidateContentCache();
|
||||
// Il value salvato può differire da quello inviato (sanitizeInline sui tag html):
|
||||
// lo restituiamo così il pannello mostra il dato reale.
|
||||
const row = getDb().prepare('SELECT value FROM content_blocks WHERE tag = ?').get(params.tag) as { value: string };
|
||||
diff --git a/tests/content-api.test.ts b/tests/content-api.test.ts
|
||||
index 6d61853..66075dc 100644
|
||||
--- a/tests/content-api.test.ts
|
||||
+++ b/tests/content-api.test.ts
|
||||
@@ -28,10 +28,23 @@ describe('applyContentUpdate', () => {
|
||||
if (htmlTag) {
|
||||
applyContentUpdate(db, htmlTag, { value: 'ciao <script>alert(1)</script><strong>ok</strong>' }, 'a');
|
||||
const row = db.prepare('SELECT value FROM content_blocks WHERE tag = ?').get(htmlTag) as any;
|
||||
expect(row.value).not.toContain('<script>');
|
||||
expect(row.value).toContain('<strong>ok</strong>');
|
||||
}
|
||||
const imgTag = contentSeed.find((e) => e.type === 'image')!.tag;
|
||||
expect(applyContentUpdate(db, imgTag, { value: 'x' }, 'a')).toMatchObject({ ok: false, status: 400 });
|
||||
});
|
||||
});
|
||||
+
|
||||
+describe('GET /api/admin/content/[tag]', () => {
|
||||
+ it('GET /api/admin/content/[tag] ritorna tag, guid, type, value, styleOptions', async () => {
|
||||
+ const { GET } = await import('../src/pages/api/admin/content/[tag]');
|
||||
+ const res = await GET({ params: { tag: 'home.hero.cta' } } as any);
|
||||
+ expect(res.status).toBe(200);
|
||||
+ const body = await res.json();
|
||||
+ expect(body.tag).toBe('home.hero.cta');
|
||||
+ expect(typeof body.guid).toBe('string');
|
||||
+ expect(body.type).toBeDefined();
|
||||
+ expect(Array.isArray(body.styleOptions)).toBe(true);
|
||||
+ });
|
||||
+});
|
||||
diff --git a/tests/content.test.ts b/tests/content.test.ts
|
||||
index a5e8e53..40c8773 100644
|
||||
--- a/tests/content.test.ts
|
||||
+++ b/tests/content.test.ts
|
||||
@@ -1,15 +1,15 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import type Database from 'better-sqlite3';
|
||||
import { createDb } from '../src/lib/db';
|
||||
import { contentSeed } from '../src/data/content-seed';
|
||||
-import { makeContentStore, syncSeed } from '../src/lib/content';
|
||||
+import { makeContentStore, syncSeed, resolveContent } from '../src/lib/content';
|
||||
|
||||
let db: Database.Database;
|
||||
beforeEach(() => { db = createDb(':memory:'); });
|
||||
|
||||
describe('seed', () => {
|
||||
it('tag univoci, validi e valori non vuoti', () => {
|
||||
const tags = contentSeed.map((e) => e.tag);
|
||||
expect(new Set(tags).size).toBe(tags.length);
|
||||
for (const e of contentSeed) {
|
||||
expect(e.tag).toMatch(/^[a-z0-9-]+(\.[a-z0-9-]+)+$/);
|
||||
@@ -51,11 +51,17 @@ describe('content store', () => {
|
||||
expect(store.resolve(contentSeed[0].tag).value).toBe(contentSeed[0].value);
|
||||
expect(store.resolve('tag.inesistente').value).toBe('');
|
||||
});
|
||||
|
||||
it('styles corrotto nel DB viene ignorato', () => {
|
||||
const store = makeContentStore(() => db);
|
||||
db.prepare(`UPDATE content_blocks SET styles = 'non-json' WHERE tag = ?`).run(contentSeed[0].tag);
|
||||
store.invalidate();
|
||||
expect(store.resolve(contentSeed[0].tag).classes).toBe('');
|
||||
});
|
||||
+
|
||||
+ it('resolveContent restituisce il guid dal DB', () => {
|
||||
+ const r = resolveContent('home.hero.cta');
|
||||
+ expect(typeof r.guid).toBe('string');
|
||||
+ expect(r.guid.length).toBeGreaterThan(10);
|
||||
+ });
|
||||
});
|
||||
Reference in New Issue
Block a user