@beechcms/core 0.4.0-preview.1 → 0.4.0-preview.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/policies.ts DELETED
@@ -1,32 +0,0 @@
1
- import type { Branch } from './types.js'
2
-
3
- export async function sha256hex(value: string): Promise<string> {
4
- const buf = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(value))
5
- return Array.from(new Uint8Array(buf))
6
- .map((b) => b.toString(16).padStart(2, '0'))
7
- .join('')
8
- }
9
-
10
- export async function verifyHashField(stored: string, candidate: string): Promise<boolean> {
11
- return stored === (await sha256hex(candidate))
12
- }
13
-
14
- /**
15
- * Risolve le policy di un branch applicando i valori di default.
16
- * Tutta la logica di accesso ai campi deve passare per questa funzione,
17
- * mai con inline `branch.policies?.x ?? default`.
18
- */
19
- export function resolvePolicies(branch: Branch): Required<NonNullable<Branch['policies']>> {
20
- const privacy = branch.policies?.privacy ?? 'plain'
21
- // Non-plain privacy implies hidden by default: the CMS hashes/encrypts on write,
22
- // so returning the stored value would leak the digest to readers.
23
- const defaultVisibility = privacy !== 'plain' ? 'hidden' : 'full'
24
- return {
25
- privacy,
26
- visibility: branch.policies?.visibility ?? defaultVisibility,
27
- search: branch.policies?.search ?? true,
28
- filter: branch.policies?.filter ?? true,
29
- sort: branch.policies?.sort ?? true,
30
- public: branch.policies?.public ?? true,
31
- }
32
- }
@@ -1,87 +0,0 @@
1
- import { generateHTML } from '@tiptap/html'
2
- import type { JSONContent } from '@tiptap/core'
3
- import Highlight from '@tiptap/extension-highlight'
4
- import Image from '@tiptap/extension-image'
5
- import Link from '@tiptap/extension-link'
6
- import Subscript from '@tiptap/extension-subscript'
7
- import Superscript from '@tiptap/extension-superscript'
8
- import { Table, TableCell, TableHeader, TableRow } from '@tiptap/extension-table'
9
- import TextAlign from '@tiptap/extension-text-align'
10
- import { Mathematics } from '@tiptap/extension-mathematics'
11
- import StarterKit from '@tiptap/starter-kit'
12
-
13
- import { isRichtextEnvelopeV1 } from './richtext.js'
14
-
15
- /**
16
- * Allinea l'output HTML allo schema TipTap usato dall'editor dashboard.
17
- * Mantieni sincronizzato con `apps/dashboard/src/features/richtext-editor/extensions/build-editor-extensions.ts`.
18
- */
19
- function createRichTextHtmlExtensions() {
20
- return [
21
- StarterKit.configure({
22
- link: false,
23
- codeBlock: {
24
- HTMLAttributes: {
25
- class: 'richtext-code-block',
26
- },
27
- },
28
- }),
29
- Link.configure({
30
- openOnClick: false,
31
- autolink: true,
32
- defaultProtocol: 'https',
33
- }),
34
- Mathematics.configure({
35
- katexOptions: {
36
- throwOnError: false,
37
- },
38
- }),
39
- Highlight,
40
- Superscript,
41
- Subscript,
42
- Image.configure({
43
- allowBase64: false,
44
- }),
45
- TextAlign.configure({
46
- types: ['heading', 'paragraph'],
47
- }),
48
- Table.configure({
49
- resizable: false,
50
- }),
51
- TableRow,
52
- TableHeader,
53
- TableCell,
54
- ]
55
- }
56
-
57
- /**
58
- * Accetta JSON TipTap (`{ type: 'doc', ... }`), envelope v1, o stringa HTML legacy.
59
- */
60
- export function normalizeRichtextForRender(value: unknown): JSONContent | string | null {
61
- if (value == null || value === '') return null
62
- if (typeof value === 'string') return value
63
- if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
64
- const o = value as Record<string, unknown>
65
- if (isRichtextEnvelopeV1(value)) {
66
- return o.doc as JSONContent
67
- }
68
- if (o.type === 'doc') {
69
- return value as JSONContent
70
- }
71
- }
72
- return null
73
- }
74
-
75
- /**
76
- * Render deterministico JSON → HTML (per display, anteprime, API pubblica).
77
- * Per stringhe HTML legacy restituisce la stringa sanificata come pass-through (nessun parse TipTap).
78
- */
79
- export function renderRichText(value: unknown): string {
80
- const normalized = normalizeRichtextForRender(value)
81
- if (normalized == null) return ''
82
- if (typeof normalized === 'string') {
83
- return normalized
84
- }
85
- const extensions = createRichTextHtmlExtensions()
86
- return generateHTML(normalized, extensions)
87
- }
package/src/richtext.ts DELETED
@@ -1,16 +0,0 @@
1
- /**
2
- * Convenzione storage richtext TipTap nel Content Engine.
3
- * @see docs/Sprints/tiptap-elevation.md
4
- */
5
- export const RICHTEXT_SCHEMA_VERSION = 1 as const
6
-
7
- export type RichtextEnvelopeV1 = {
8
- schemaVersion: typeof RICHTEXT_SCHEMA_VERSION
9
- doc: Record<string, unknown>
10
- }
11
-
12
- export function isRichtextEnvelopeV1(value: unknown): value is RichtextEnvelopeV1 {
13
- if (typeof value !== 'object' || value === null || Array.isArray(value)) return false
14
- const o = value as Record<string, unknown>
15
- return o.schemaVersion === RICHTEXT_SCHEMA_VERSION && typeof o.doc === 'object' && o.doc !== null
16
- }
package/src/seeds.ts DELETED
@@ -1,194 +0,0 @@
1
- /**
2
- * Seed Registry: configurazione degli schemi di contenuto.
3
- * In v0.4.0 ogni Seed genera una tabella `content_{slug}` con colonne reali.
4
- * `branch.alias` è il nome della colonna SQL.
5
- */
6
- import type { Seed } from './types.js'
7
-
8
- /** Articolo: contenuto editoriale con richtext, immagine e SEO */
9
- export const ARTICOLO_SEED: Seed = {
10
- slug: 'articoli',
11
- label: 'Articolo',
12
- labelPlural: 'Articoli',
13
- displayNameAlias: 'title',
14
- allowPublicRead: true,
15
- allowDrafts: true,
16
- branches: [
17
- { alias: 'title', label: 'Titolo', type: 'text' },
18
- { alias: 'publishedAt', label: 'Data pubblicazione', type: 'date' },
19
- { alias: 'coverImage', label: 'Immagine copertina', type: 'file' },
20
- { alias: 'tags', label: 'Tag', type: 'json', options: ['cms', 'tutorial', 'release', 'annuncio', 'guida', 'news', 'aggiornamento'] },
21
- { alias: 'body', label: 'Corpo articolo', type: 'richtext' },
22
- { alias: 'metaTitle', label: 'Meta titolo (SEO)', type: 'text' },
23
- { alias: 'metaDescription', label: 'Meta descrizione (SEO)', type: 'text' },
24
- ],
25
- }
26
-
27
- /** Prodotto: scheda prodotto e-commerce con richtext, galleria e SEO */
28
- export const PRODOTTO_SEED: Seed = {
29
- slug: 'prodotti',
30
- label: 'Prodotto',
31
- labelPlural: 'Prodotti',
32
- displayNameAlias: 'name',
33
- allowPublicRead: true,
34
- branches: [
35
- { alias: 'name', label: 'Nome', type: 'text' },
36
- { alias: 'price', label: 'Prezzo (€)', type: 'number' },
37
- { alias: 'stock', label: 'Quantità disponibile', type: 'number' },
38
- { alias: 'active', label: 'In vendita', type: 'boolean' },
39
- { alias: 'coverImage', label: 'Immagine principale', type: 'file' },
40
- { alias: 'images', label: 'Galleria immagini', type: 'file', multiple: true, format: 'asset-list' },
41
- { alias: 'description', label: 'Descrizione', type: 'richtext' },
42
- { alias: 'metaTitle', label: 'Meta titolo (SEO)', type: 'text' },
43
- { alias: 'metaDescription', label: 'Meta descrizione (SEO)', type: 'text' },
44
- ],
45
- }
46
-
47
- /** Membro del team: profilo con foto e link social */
48
- export const TEAM_SEED: Seed = {
49
- slug: 'team',
50
- label: 'Membro',
51
- labelPlural: 'Team',
52
- displayNameAlias: 'name',
53
- allowPublicRead: true,
54
- branches: [
55
- { alias: 'name', label: 'Nome', type: 'text' },
56
- { alias: 'role', label: 'Ruolo', type: 'text' },
57
- { alias: 'bio', label: 'Bio breve', type: 'text' },
58
- { alias: 'photo', label: 'Foto', type: 'file' },
59
- { alias: 'linkedIn', label: 'URL LinkedIn', type: 'text' },
60
- { alias: 'active', label: 'Visibile', type: 'boolean' },
61
- { alias: 'metaTitle', label: 'Meta titolo (SEO)', type: 'text' },
62
- { alias: 'metaDescription', label: 'Meta descrizione (SEO)', type: 'text' },
63
- ],
64
- }
65
-
66
- /** Testimonianza: recensione cliente con valutazione e foto */
67
- export const TESTIMONIANZA_SEED: Seed = {
68
- slug: 'testimonianze',
69
- label: 'Testimonianza',
70
- labelPlural: 'Testimonianze',
71
- displayNameAlias: 'author',
72
- allowPublicRead: true,
73
- branches: [
74
- { alias: 'author', label: 'Autore', type: 'text' },
75
- { alias: 'company', label: 'Azienda', type: 'text' },
76
- { alias: 'quote', label: 'Citazione', type: 'text' },
77
- { alias: 'rating', label: 'Valutazione (1-5)', type: 'number' },
78
- { alias: 'date', label: 'Data', type: 'date' },
79
- { alias: 'photo', label: 'Foto autore', type: 'file' },
80
- { alias: 'active', label: 'Pubblica', type: 'boolean' },
81
- { alias: 'metaTitle', label: 'Meta titolo (SEO)', type: 'text' },
82
- { alias: 'metaDescription', label: 'Meta descrizione (SEO)', type: 'text' },
83
- ],
84
- }
85
-
86
- /** Pagina: pagina statica con richtext, immagine hero e SEO */
87
- export const PAGINA_SEED: Seed = {
88
- slug: 'pagine',
89
- label: 'Pagina',
90
- labelPlural: 'Pagine',
91
- displayNameAlias: 'title',
92
- allowPublicRead: true,
93
- allowDrafts: true,
94
- branches: [
95
- { alias: 'title', label: 'Titolo', type: 'text' },
96
- { alias: 'coverImage', label: 'Immagine hero', type: 'file' },
97
- { alias: 'body', label: 'Contenuto', type: 'richtext' },
98
- { alias: 'metaTitle', label: 'Meta titolo (SEO)', type: 'text' },
99
- { alias: 'metaDescription', label: 'Meta descrizione (SEO)', type: 'text' },
100
- ],
101
- }
102
-
103
- /**
104
- * Cliente: dimostra tutte le assi di Branch.policies.
105
- *
106
- * email → visibility:masked, search:false, public:false
107
- * passwordHash → privacy:hash, visibility:hidden, search/filter/sort/public: false
108
- * phone → visibility:masked, filter:false, public:false
109
- * internalNote → visibility:hidden, search:false, public:false
110
- * registeredAt → public:false (data interna, non esposta)
111
- */
112
- export const CLIENTE_SEED: Seed = {
113
- slug: 'clienti',
114
- label: 'Cliente',
115
- labelPlural: 'Clienti',
116
- displayNameAlias: 'name',
117
- allowPublicRead: true,
118
- branches: [
119
- { alias: 'name', label: 'Nome', type: 'text' },
120
- {
121
- alias: 'email',
122
- label: 'Email',
123
- type: 'text',
124
- policies: { visibility: 'masked', search: false, public: false },
125
- },
126
- {
127
- alias: 'passwordHash',
128
- label: 'Password (hash)',
129
- type: 'text',
130
- policies: { privacy: 'hash', visibility: 'hidden', search: false, filter: false, sort: false, public: false },
131
- },
132
- {
133
- alias: 'tier',
134
- label: 'Piano',
135
- type: 'text',
136
- options: ['free', 'starter', 'pro', 'enterprise'],
137
- },
138
- {
139
- alias: 'phone',
140
- label: 'Telefono',
141
- type: 'text',
142
- policies: { visibility: 'masked', filter: false, public: false },
143
- },
144
- {
145
- alias: 'internalNote',
146
- label: 'Note interne',
147
- type: 'text',
148
- policies: { visibility: 'hidden', search: false, public: false },
149
- },
150
- {
151
- alias: 'registeredAt',
152
- label: 'Data iscrizione',
153
- type: 'date',
154
- policies: { public: false },
155
- },
156
- { alias: 'active', label: 'Attivo', type: 'boolean' },
157
- ],
158
- }
159
-
160
- /**
161
- * TODO: Rimuovere al termine dei test del testsite
162
- * Messaggio di contatto: per form contatti esterni
163
- */
164
- export const MESSAGGI_SEED: Seed = {
165
- slug: 'messaggi',
166
- label: 'Messaggio',
167
- labelPlural: 'Messaggi',
168
- displayNameAlias: 'name',
169
- allowPublicPost: true,
170
- allowPublicEdit: true,
171
- branches: [
172
- { alias: 'name', label: 'Nome mittente', type: 'text', requiredOnCreate: true },
173
- { alias: 'email', label: 'Email mittente', type: 'text', requiredOnCreate: true },
174
- { alias: 'subject', label: 'Oggetto', type: 'text', requiredOnCreate: true },
175
- { alias: 'message', label: 'Messaggio', type: 'richtext', requiredOnCreate: true },
176
- { alias: 'read', label: 'Letto', type: 'boolean' },
177
- ],
178
- }
179
-
180
- /** Registro: slug -> Seed */
181
- export const SEED_REGISTRY: Record<string, Seed> = {
182
- articoli: ARTICOLO_SEED,
183
- prodotti: PRODOTTO_SEED,
184
- team: TEAM_SEED,
185
- testimonianze: TESTIMONIANZA_SEED,
186
- pagine: PAGINA_SEED,
187
- clienti: CLIENTE_SEED,
188
- messaggi: MESSAGGI_SEED,
189
- }
190
-
191
- /** Ritorna il Seed per lo slug dato, o null se non esiste. */
192
- export function getSeed(slug: string): Seed | null {
193
- return SEED_REGISTRY[slug] ?? null
194
- }
package/src/slug-utils.ts DELETED
@@ -1,33 +0,0 @@
1
- /**
2
- * Utility per la gestione degli slug.
3
- * Condivisa tra Dashboard e API pubbliche.
4
- */
5
-
6
- /**
7
- * Converte una stringa in uno slug URL-safe.
8
- * Limita la lunghezza a 15 caratteri come richiesto.
9
- */
10
- export function slugify(value: string): string {
11
- return value
12
- .normalize('NFKD') // Normalizza caratteri accentati
13
- .replace(/[^\w\s-]/g, '') // Rimuove tutto ciò che non è alfanumerico (accetta underscore temporaneamente)
14
- .trim()
15
- .toLowerCase()
16
- .replace(/[\s_-]+/g, '-') // Sostituisce spazi, underscore e trattini multipli con singolo trattino
17
- .replace(/^-+|-+$/g, '') // Rimuove trattini all'inizio o alla fine
18
- .slice(0, 15) // Limita a 15 caratteri
19
- }
20
-
21
- /**
22
- * Genera uno slug da un input (es. titolo o nome) o un fallback UUID.
23
- */
24
- export function generateEntrySlug(input: { slug?: string; title?: unknown; name?: unknown }): string {
25
- const candidate =
26
- (typeof input.slug === 'string' && input.slug) ||
27
- (typeof input.title === 'string' && input.title) ||
28
- (typeof input.name === 'string' && input.name) ||
29
- Math.random().toString(36).slice(2, 10) // Fallback compatto se crypto non disponibile (o slice 8)
30
-
31
- const normalized = slugify(candidate)
32
- return normalized || Math.random().toString(36).slice(2, 10)
33
- }
package/src/types.ts DELETED
@@ -1,121 +0,0 @@
1
- export type BranchType = 'text' | 'number' | 'boolean' | 'json' | 'date' | 'richtext' | 'file'
2
-
3
- /** Branch: definizione di una proprietà. alias = nome colonna SQL. */
4
- export interface Branch {
5
- /** Alias human-readable, usato nel payload API e come nome colonna SQL nella tabella dedicata */
6
- alias: string
7
- /** Etichetta per la UI */
8
- label: string
9
- /** Tipo del valore */
10
- type: BranchType
11
- /**
12
- * Variante semantica opzionale del campo per UI/validazione.
13
- * `asset-list` su `file` multiplo abilita la gestione galleria.
14
- */
15
- format?: 'plain' | 'markdown' | 'html' | 'date' | 'datetime' | 'asset-list'
16
- /**
17
- * Cardinalità opzionale per campi media:
18
- * - false/undefined: singolo asset (string URL)
19
- * - true: lista asset (string[] URL)
20
- */
21
- multiple?: boolean
22
- /**
23
- * Vocabolario predefinito per campi tag/select/multiselect.
24
- * Lista statica definita nel Seed (non salvata nel DB).
25
- */
26
- options?: string[]
27
- /** Campo obbligatorio in creazione — genera NOT NULL in generateCreateTable */
28
- requiredOnCreate?: boolean
29
- /** Campo obbligatorio in update */
30
- requiredOnUpdate?: boolean
31
- /**
32
- * Policy di accesso e trattamento del campo.
33
- * Tutti i valori sono opzionali — `resolvePolicies(branch)` fornisce i default.
34
- */
35
- policies?: {
36
- /** Come il valore viene memorizzato. Default: 'plain' */
37
- privacy?: 'plain' | 'hash' | 'encrypt'
38
- /** Come il valore viene restituito nelle risposte API. Default: 'full' */
39
- visibility?: 'full' | 'masked' | 'hidden'
40
- /** Il campo è incluso nelle query di ricerca full-text. Default: true */
41
- search?: boolean
42
- /** Il campo è disponibile come colonna di filtro nella dashboard. Default: true */
43
- filter?: boolean
44
- /** Il campo è disponibile come colonna di ordinamento nella dashboard. Default: true */
45
- sort?: boolean
46
- /** Il campo è incluso nelle risposte della Public API. Default: true */
47
- public?: boolean
48
- }
49
- }
50
-
51
- /** Seed: definizione dello schema di un tipo di contenuto */
52
- export interface Seed {
53
- /** Slug identificativo — anche nome tabella: `content_{slug}` */
54
- slug: string
55
- /** Etichetta singolare per la UI */
56
- label: string
57
- /** Etichetta plurale per la UI. Se assente si usa `label`. */
58
- labelPlural?: string
59
- /** Abilita lettura dalla Public API (`GET /api/v1/public/:seed`). Default: false */
60
- allowPublicRead?: boolean
61
- /** Abilita creazione dalla Public API (`POST /api/v1/public/:seed/add`). Default: false */
62
- allowPublicPost?: boolean
63
- /** Abilita modifica dalla Public API (`PUT /api/v1/public/:seed/edit/:id`). Default: false */
64
- allowPublicEdit?: boolean
65
- /**
66
- * Abilita la feature "bozza in attesa" per questo seed.
67
- * Quando true, genera tabella `content_{slug}_drafts` e abilita gli endpoint `/draft`.
68
- * Default: false
69
- */
70
- allowDrafts?: boolean
71
- /**
72
- * Alias del branch usato come nome leggibile dell'entry (es. "title", "name", "author").
73
- * Obbligatorio — le UI lo usano per display senza euristica.
74
- */
75
- displayNameAlias: string
76
- /** Lista dei campi (Branch) */
77
- branches: Branch[]
78
- }
79
-
80
- // ---- Query types (usati da buildSelectQuery nel Botanical Engine) ----
81
-
82
- export type FilterOperator =
83
- | 'eq'
84
- | 'gt'
85
- | 'gte'
86
- | 'lt'
87
- | 'lte'
88
- | 'contains'
89
- | 'is_empty'
90
- | 'is_not_empty'
91
-
92
- export type FilterType = 'text' | 'number' | 'date' | 'boolean' | 'tags' | 'select' | 'system'
93
-
94
- export interface FilterCondition {
95
- op: FilterOperator
96
- value: string | number | boolean | null
97
- }
98
-
99
- export interface FilterGroup {
100
- /** Nome colonna: system column (id/slug/status/created_at/updated_at) o branch alias */
101
- column: string
102
- type: FilterType
103
- conditions: FilterCondition[]
104
- }
105
-
106
- export interface SelectOptions {
107
- filters?: FilterGroup[]
108
- orderBy?: { column: string; dir: 'ASC' | 'DESC' }
109
- pagination?: { limit: number; offset: number }
110
- /** Filtra per status. null = nessun filtro status. */
111
- status?: string | null
112
- /** Full-text search — usa FTS5 se il seed ha branch richtext indicizzabili */
113
- search?: string
114
- /** Proiezione colonne. Vuoto = SELECT * */
115
- fields?: string[]
116
- }
117
-
118
- export interface ParameterizedQuery {
119
- sql: string
120
- bindings: (string | number | boolean | null)[]
121
- }