@beechcms/core 0.4.0-preview.1
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/dist/define-seed.d.ts +3 -0
- package/dist/define-seed.d.ts.map +1 -0
- package/dist/define-seed.js +3 -0
- package/dist/engine.d.ts +68 -0
- package/dist/engine.d.ts.map +1 -0
- package/dist/engine.js +401 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +18 -0
- package/dist/policies.d.ts +10 -0
- package/dist/policies.d.ts.map +1 -0
- package/dist/policies.js +28 -0
- package/dist/richtext-render.d.ts +11 -0
- package/dist/richtext-render.d.ts.map +1 -0
- package/dist/richtext-render.js +85 -0
- package/dist/richtext.d.ts +11 -0
- package/dist/richtext.d.ts.map +1 -0
- package/dist/richtext.js +11 -0
- package/dist/seeds.d.ts +36 -0
- package/dist/seeds.d.ts.map +1 -0
- package/dist/seeds.js +179 -0
- package/dist/slug-utils.d.ts +18 -0
- package/dist/slug-utils.d.ts.map +1 -0
- package/dist/slug-utils.js +29 -0
- package/dist/types.d.ts +110 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/dist/validation.d.ts +44 -0
- package/dist/validation.d.ts.map +1 -0
- package/dist/validation.js +571 -0
- package/package.json +35 -0
- package/src/define-seed.ts +5 -0
- package/src/engine.ts +465 -0
- package/src/index.ts +19 -0
- package/src/policies.test.ts +127 -0
- package/src/policies.ts +32 -0
- package/src/richtext-render.ts +87 -0
- package/src/richtext.ts +16 -0
- package/src/seeds.ts +194 -0
- package/src/slug-utils.ts +33 -0
- package/src/types.ts +121 -0
- package/src/validation.ts +667 -0
- package/tsconfig.json +14 -0
- package/tsconfig.tsbuildinfo +1 -0
|
@@ -0,0 +1,87 @@
|
|
|
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
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
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
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
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
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
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
|
+
}
|