fimo 0.9.0 → 0.10.0-experimental.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/assets/content-templates/types-template.eta +30 -3
- package/assets/skills/fimo-cli/references/content.md +39 -2
- package/assets/skills/fimo-cli/references/webhooks.md +4 -0
- package/assets/skills/fimo-code/references/content.md +45 -1
- package/assets/skills/fimo-studio/references/cms.md +11 -0
- package/assets/skills/fimo-studio/references/webhooks.md +1 -1
- package/dist/cli/bundle.json +1 -1
- package/dist/cli/index.js +14 -14
- package/dist/runtime/primitives/components/RichText.d.ts +2 -2
- package/dist/runtime/primitives/components/RichText.js +5 -3
- package/dist/runtime/primitives/lib/query.d.ts +1 -1
- package/dist/scripts/validate-schemas.js +10 -1
- package/package.json +1 -1
- package/release.json +2 -2
- package/templates/react-router/package.json +1 -1
|
@@ -48,9 +48,9 @@ export interface <%= model.pascal %> extends WithReferences<<%= model.pascal %>R
|
|
|
48
48
|
// Generate interface properties based on fields
|
|
49
49
|
if (model.fields) {
|
|
50
50
|
Object.entries(model.fields).forEach(([key, field]) => {
|
|
51
|
-
const type = getTypeForField(field);
|
|
51
|
+
const type = isNullableRichTextField(field) ? `${getTypeForField(field)} | null` : getTypeForField(field);
|
|
52
52
|
-%>
|
|
53
|
-
<%= key %>: <%= type %>;
|
|
53
|
+
<%= key %><%= isOptionalOutputField(field) ? '?' : '' %>: <%= type %>;
|
|
54
54
|
<%
|
|
55
55
|
});
|
|
56
56
|
}
|
|
@@ -68,7 +68,9 @@ export interface <%= model.pascal %>Input {
|
|
|
68
68
|
<%
|
|
69
69
|
if (model.fields) {
|
|
70
70
|
Object.entries(model.fields).forEach(([key, field]) => {
|
|
71
|
-
const type =
|
|
71
|
+
const type = isNullableRichTextField(field)
|
|
72
|
+
? `${getInputTypeForField(field)} | null`
|
|
73
|
+
: getInputTypeForField(field);
|
|
72
74
|
-%>
|
|
73
75
|
<%= key %><%= isRequiredField(field) ? '' : '?' %>: <%= type %>;
|
|
74
76
|
<%
|
|
@@ -85,6 +87,7 @@ if (model.fields) {
|
|
|
85
87
|
function getTypeForField(field) {
|
|
86
88
|
switch(field.type) {
|
|
87
89
|
case 'string':
|
|
90
|
+
return getStringLiteralUnion(field) || 'FimoString';
|
|
88
91
|
case 'text':
|
|
89
92
|
return 'FimoString';
|
|
90
93
|
case 'richtext':
|
|
@@ -113,6 +116,7 @@ function getTypeForField(field) {
|
|
|
113
116
|
function getInputTypeForField(field) {
|
|
114
117
|
switch(field.type) {
|
|
115
118
|
case 'string':
|
|
119
|
+
return getStringLiteralUnion(field) || 'string';
|
|
116
120
|
case 'text':
|
|
117
121
|
return 'string';
|
|
118
122
|
case 'richtext':
|
|
@@ -143,6 +147,7 @@ function getInputTypeForField(field) {
|
|
|
143
147
|
function getNestedFieldType(field, input) {
|
|
144
148
|
switch(field.type) {
|
|
145
149
|
case 'string':
|
|
150
|
+
return getStringLiteralUnion(field) || 'string';
|
|
146
151
|
case 'text':
|
|
147
152
|
return 'string';
|
|
148
153
|
case 'richtext':
|
|
@@ -195,9 +200,31 @@ function getBlocksType(field, input) {
|
|
|
195
200
|
return `${item}[]`;
|
|
196
201
|
}
|
|
197
202
|
|
|
203
|
+
function getStringLiteralUnion(field) {
|
|
204
|
+
if (!Array.isArray(field.options) || field.options.length === 0) {
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
return field.options.map((option) => JSON.stringify(String(option))).join(' | ');
|
|
208
|
+
}
|
|
209
|
+
|
|
198
210
|
function isRequiredField(field) {
|
|
211
|
+
if (isConditionalField(field)) {
|
|
212
|
+
return false;
|
|
213
|
+
}
|
|
199
214
|
return Boolean(field.required || (field.type === 'blocks' && Number(field.min || 0) > 0));
|
|
200
215
|
}
|
|
216
|
+
|
|
217
|
+
function isNullableRichTextField(field) {
|
|
218
|
+
return field.type === 'richtext' && !isRequiredField(field);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function isOptionalOutputField(field) {
|
|
222
|
+
return isConditionalField(field) || isNullableRichTextField(field);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function isConditionalField(field) {
|
|
226
|
+
return field.showIf !== undefined || field.showIfAny !== undefined;
|
|
227
|
+
}
|
|
201
228
|
-%>
|
|
202
229
|
|
|
203
230
|
export interface <%= model.pascal %>References {
|
|
@@ -94,9 +94,46 @@ fimo entries query --file query.sql
|
|
|
94
94
|
cat query.sql | fimo entries query --stdin
|
|
95
95
|
```
|
|
96
96
|
|
|
97
|
-
SQL exposes the raw internal representation. `document_id` is the stable ID used by `fimo entries`, content tools, references, and the API. `id` is the internal ID of one exact locale variant. When query results will feed another Fimo command or tool, select `document_id AS id`; use `locale` to select a variant.
|
|
97
|
+
SQL exposes the raw internal representation. `document_id` is the stable ID used by `fimo entries`, content tools, references, and the API. `id` is the internal ID of one exact locale variant. Localized variants share one `document_id` and have one row per locale; non-localized entries have a null `locale`. When query results will feed another Fimo command or tool, select `document_id AS id`; use `locale` to select a variant.
|
|
98
|
+
|
|
99
|
+
The supported content columns are `id`, `document_id`, `content_type`, `locale`, `slug`, `data`, `created_at`, and `updated_at`. Schema fields live inside the JSONB `data` column. Fimo may add internal columns, so select the fields you need instead of `SELECT *` and do not depend on other columns. If a safe read is blocked because a SQL function is unavailable, tell the user which function is missing and use the built-in `supportReport` tool when the platform gap materially blocks the task. Do not bypass the restriction with a write.
|
|
100
|
+
|
|
101
|
+
For example, find English blog posts that do not yet have a Spanish variant:
|
|
102
|
+
|
|
103
|
+
```sql
|
|
104
|
+
SELECT source.document_id AS id, source.data
|
|
105
|
+
FROM entries source
|
|
106
|
+
WHERE source.content_type = 'BlogPost'
|
|
107
|
+
AND source.locale = 'en'
|
|
108
|
+
AND NOT EXISTS (
|
|
109
|
+
SELECT 1
|
|
110
|
+
FROM entries target
|
|
111
|
+
WHERE target.content_type = source.content_type
|
|
112
|
+
AND target.document_id = source.document_id
|
|
113
|
+
AND target.locale = 'es'
|
|
114
|
+
)
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Find Spanish variants whose English source was edited later:
|
|
118
|
+
|
|
119
|
+
```sql
|
|
120
|
+
SELECT
|
|
121
|
+
target.document_id AS id,
|
|
122
|
+
target.content_type,
|
|
123
|
+
target.slug,
|
|
124
|
+
source.updated_at AS source_updated_at,
|
|
125
|
+
target.updated_at AS target_updated_at
|
|
126
|
+
FROM entries source
|
|
127
|
+
JOIN entries target
|
|
128
|
+
ON target.content_type = source.content_type
|
|
129
|
+
AND target.document_id = source.document_id
|
|
130
|
+
WHERE source.locale = 'en'
|
|
131
|
+
AND target.locale = 'es'
|
|
132
|
+
AND source.updated_at > target.updated_at
|
|
133
|
+
ORDER BY source.updated_at DESC
|
|
134
|
+
```
|
|
98
135
|
|
|
99
|
-
|
|
136
|
+
Treat these results as possibly stale, not proof of a bad translation. The query only proves that the source row was edited later; an agent should review the content and link each target with its stable `id`.
|
|
100
137
|
|
|
101
138
|
### Bulk mutations
|
|
102
139
|
|
|
@@ -24,6 +24,10 @@ Run this before writing a custom payload. The list shows supported event types,
|
|
|
24
24
|
- Analytics: `analytics.report_ready`
|
|
25
25
|
- Media: uploaded/updated/deleted/generated/processing_failed
|
|
26
26
|
- Entries: `entry.changed`
|
|
27
|
+
- Domains: `domain.created`, `domain.deleted`
|
|
28
|
+
- Labels: `label.changed`
|
|
29
|
+
- Locales: `locale.created`, `locale.default_changed`, `locale.deleted`
|
|
30
|
+
- Environments: `environment.created`, `environment.deleted`, `environment.merged`, `environment.restored`
|
|
27
31
|
|
|
28
32
|
Do not guess variable paths. The common envelope is:
|
|
29
33
|
|
|
@@ -61,6 +61,48 @@ something else (e.g. `handle`, `permalink`).
|
|
|
61
61
|
|
|
62
62
|
A `date` field currently generates as `any` in the TS client, so for typed access (and the `<Date>` component) prefer a `string` field holding an ISO date — which is what the `publishDate` examples below do.
|
|
63
63
|
|
|
64
|
+
## Enumerated strings
|
|
65
|
+
|
|
66
|
+
A `string` field may declare `options` to close its value set. The CMS renders a select, the API validates the value as an enum, and the generated client types the field as a literal union instead of `string`.
|
|
67
|
+
|
|
68
|
+
```json
|
|
69
|
+
"layout": { "type": "string", "options": ["article", "video"], "default": "article" }
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
- Values must be unique. Set a `default` that is one of them.
|
|
73
|
+
- `options` cannot be combined with `regex`, `minLength`, or `maxLength`, and is rejected on every field type other than `string`.
|
|
74
|
+
- Writes are validated against the set: a value outside it is rejected with a 400. An optional field also accepts `""` and `null` so the editor can clear the choice; a `required` one only accepts a declared value.
|
|
75
|
+
- Removing an option that entries still hold fails the push, the same way an unsafe type change does. Migrate those entries to a remaining value first.
|
|
76
|
+
|
|
77
|
+
## Conditional fields
|
|
78
|
+
|
|
79
|
+
A field may declare `showIf` (every clause must match) or `showIfAny` (one clause is enough) to apply only in some entries. A clause names a sibling field and a value.
|
|
80
|
+
|
|
81
|
+
```json
|
|
82
|
+
{
|
|
83
|
+
"layout": { "type": "string", "options": ["article", "video"], "default": "article" },
|
|
84
|
+
"body": { "type": "richtext", "showIf": { "field": "layout", "equals": "article" } },
|
|
85
|
+
"videoUrl": { "type": "string", "required": true, "showIf": { "field": "layout", "equals": "video" } },
|
|
86
|
+
"transcript": {
|
|
87
|
+
"type": "text",
|
|
88
|
+
"showIfAny": [
|
|
89
|
+
{ "field": "layout", "equals": "video" },
|
|
90
|
+
{ "field": "hasAudio", "equals": true }
|
|
91
|
+
]
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
- A clause is `{ "field": "<sibling>", "equals": <value> }` or `{ "field": "<sibling>", "in": [<values>] }`. Use `in` for several values of one field.
|
|
97
|
+
- `showIf` accepts one clause or an array of clauses, and every clause must match. `showIfAny` takes an array and one match is enough. A field declares one key or the other, never both.
|
|
98
|
+
- **A hidden field is not rendered and not validated, so `required` does not apply to it.** An entry whose condition never held can be saved without ever filling the field.
|
|
99
|
+
- **Hiding a field changes nothing about storage or reads.** The value stays in the entry and the API keeps returning it, so an editor who flips the trigger back gets their work back. Narrow on the trigger in site code rather than trusting the value.
|
|
100
|
+
- Because a conditional field can be absent, it is always optional in the generated types even when `required` is set.
|
|
101
|
+
- Conditions resolve against siblings in the same object. A field inside a block variant may only reference fields in that same block item.
|
|
102
|
+
- A clause may reference a `boolean`, `number`, or `string` field. A `string` trigger must declare `options`, which is what lets `fimo validate` reject a clause value that could never match.
|
|
103
|
+
- A trigger cannot itself be conditional — conditions are one level deep, so a chain like A shows B shows C is rejected.
|
|
104
|
+
- `fimo validate` and `fimo schemas push` reject an unknown trigger name, a trigger of the wrong type, a value outside the trigger's `options`, and a trigger that is itself conditional.
|
|
105
|
+
|
|
64
106
|
## Blocks
|
|
65
107
|
|
|
66
108
|
`blocks` is the only nested structured-content type and always stores an array. Use `schema.fields` for one homogeneous block shape. Set `max: 1` when the field is a single optional group. Use `schema.oneOf` when editors choose a variant for each item.
|
|
@@ -272,7 +314,7 @@ interface BlogPost {
|
|
|
272
314
|
};
|
|
273
315
|
title: FimoString;
|
|
274
316
|
summary: FimoString;
|
|
275
|
-
body
|
|
317
|
+
body?: FimoRichText | null;
|
|
276
318
|
coverImage: FimoMedia;
|
|
277
319
|
publishDate: FimoString;
|
|
278
320
|
featured: FimoBoolean;
|
|
@@ -282,6 +324,8 @@ interface BlogPost {
|
|
|
282
324
|
|
|
283
325
|
Content fields live at the top level. Fimo-owned metadata that is not part of the user schema lives under `__fimo`, so a schema can safely define fields named `contentType`.
|
|
284
326
|
|
|
327
|
+
Optional richtext fields are nullable in generated read and write types. `<RichText>` accepts the generated value directly and renders nothing when it is null or undefined. A non-conditional required richtext field stays non-null; conditional fields remain optional because their condition may not apply.
|
|
328
|
+
|
|
285
329
|
## Localized and shared content
|
|
286
330
|
|
|
287
331
|
Localized content has one variant per locale. Generated hooks read the active Fimo locale automatically.
|
|
@@ -36,6 +36,17 @@ When you open a Collection, the entry list provides several tools:
|
|
|
36
36
|
- **Columns** dropdown (the eye button) next to the search bar to show/hide columns and reorder them by dragging each column row by its grip handle.
|
|
37
37
|
- **Pagination** at the bottom when there are many entries.
|
|
38
38
|
|
|
39
|
+
## Completing Missing Translations
|
|
40
|
+
|
|
41
|
+
Localized Collections show only entries for the locale selected beside the Collection name. When that locale is missing translations that exist in the project's default locale:
|
|
42
|
+
|
|
43
|
+
1. Click the amber **N missing** warning beside the locale selector.
|
|
44
|
+
2. Review the source and target languages in the popover.
|
|
45
|
+
3. Choose **Create missing translations**.
|
|
46
|
+
4. Review or edit the prompt placed in AI chat, then send it when ready.
|
|
47
|
+
|
|
48
|
+
Fimo does not send the prompt automatically. After the AI finishes, translated entries appear in the selected locale and the warning updates.
|
|
49
|
+
|
|
39
50
|
## Creating a New Entry
|
|
40
51
|
|
|
41
52
|
1. Navigate to the Collection you want to add content to.
|
|
@@ -15,7 +15,7 @@ Only project members with Full Access can see Webhooks in Settings or manage its
|
|
|
15
15
|
5. Optionally narrow form, agent, media, or entry events with the filter shown for that event.
|
|
16
16
|
6. Review the request and select **Save**.
|
|
17
17
|
|
|
18
|
-
The event groups are Entries, Forms, Publish, Agents, Analytics, and
|
|
18
|
+
The event groups are Entries, Forms, Publish, Agents, Analytics, Media, Domains, Labels, Locales, and Environments. Webhooks belong to the project, with up to 20 configurations. They listen to main by default or can include every environment.
|
|
19
19
|
|
|
20
20
|
Entry webhooks show **Combine rapid changes**. The default waits 5 seconds after direct editing stops and sends one delivery with the combined changes. Immediate, 30-second, and 1-minute windows are also available. Bulk edits and environment merges already produce batches and send immediately.
|
|
21
21
|
|