fimo 0.9.0 → 0.10.0-staging.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.
@@ -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 = getInputTypeForField(field);
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
- The available columns are `id`, `document_id`, `content_type`, `locale`, `slug`, `data`, `created_at`, and `updated_at`. Schema fields live inside the JSONB `data` column. 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.
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
 
@@ -150,7 +187,7 @@ Entry bodies use reference objects with target `id` values:
150
187
  fimo entries create BlogPost --body '{"data":{"slug":"hello","title":"Hello","author":{"id":"750e8400-e29b-41d4-a716-446655440000"},"contributors":[]}}'
151
188
  ```
152
189
 
153
- Do not add `localized` or field-level `i18n` to reference fields. The reference value belongs to the entry variant containing it.
190
+ `localized` is not a supported spelling anywhere. A reference field may carry `"i18n": false` to point every locale of the document at the same target — see Localized and shared content below.
154
191
 
155
192
  ## Localized and shared content
156
193
 
@@ -158,7 +195,42 @@ Schemas are localized by default. Omit `--locale` to use the project default. Pa
158
195
 
159
196
  When creating a localized variant of an existing entry, pass the source entry's `documentId` with `--document-id`. Keep projects single-locale unless the user asks for multilingual or localized output; when multiple locales are configured, create and update every locale explicitly.
160
197
 
161
- For content shared across locales, define `"i18n": { "enabled": false }` at the schema root and create each entry once. Do not pass `--document-id` to manufacture locale variants. Raw API and CLI responses omit `locale`; generated collection models add the effective active/default locale without creating per-locale variants. No other `i18n` options and no field-level localization settings are supported.
198
+ For content shared across locales, define `"i18n": { "enabled": false }` at the schema root and create each entry once. Do not pass `--document-id` to manufacture locale variants. Raw API and CLI responses omit `locale`; generated collection models add the effective active/default locale without creating per-locale variants. No other schema-root `i18n` options are supported.
199
+
200
+ ### Shared fields inside a localized type
201
+
202
+ A localized type can still hold fields that are the same in every language. Mark the field `"i18n": false`. Use it for anything that is not translated — a price, a SKU, a product photo, a reference to the same target everywhere. Without it those values are re-entered per locale and drift apart.
203
+
204
+ ```json
205
+ {
206
+ "uid": "Product",
207
+ "name": "Products",
208
+ "fields": {
209
+ "title": { "type": "string" },
210
+ "description": { "type": "richtext" },
211
+ "sku": { "type": "string", "i18n": false, "unique": true },
212
+ "price": { "type": "number", "i18n": false },
213
+ "photo": { "type": "media", "i18n": false },
214
+ "brand": { "type": "reference", "target": "Brand", "i18n": false }
215
+ }
216
+ }
217
+ ```
218
+
219
+ Writing a shared field through any locale updates every locale of that document:
220
+
221
+ ```bash
222
+ fimo entries update Product <id> --locale es --body '{"data":{"title":"Silla","price":120}}'
223
+ ```
224
+
225
+ That sets the Spanish title and moves the price to `120` for every language. An update that does not mention a shared field leaves it untouched, so partial writes are safe.
226
+
227
+ Rules:
228
+
229
+ - `i18n` belongs on top-level scalar and reference fields only. Fields inside `blocks` follow the locale of the entry containing them and are rejected.
230
+ - On a non-localized type the flag is inert — there is one row already.
231
+ - `"i18n": true` is the default and never needs writing.
232
+
233
+ Turning an existing field shared is destructive when the languages currently hold different values. The push collapses each document to its default-locale value and discards the others; there is no prompt and no undo. Most documents already agree, so most collapses change nothing — but tell the user what will happen before adding `"i18n": false` to a field that already holds content, and check whether the languages differ if you are unsure.
162
234
 
163
235
  Use `"isSingleton": true` for a type with exactly one logical document. Generated singleton clients expose `get`/`update` and their React Query hooks, not collection create/remove APIs.
164
236
 
@@ -81,3 +81,5 @@ Labels and content are separate translated surfaces:
81
81
  - content: schema entries, managed with `fimo entries`
82
82
 
83
83
  Omit `--locale` for default-locale writes. Pass `--locale <locale>` for each non-default localized entry you need to create or update. Shared schemas (`i18n.enabled: false`) have one locale-free entry instead.
84
+
85
+ Inside a localized schema, a single field can still be shared across every language with `"i18n": false` — use it for values that are not translated, such as a price or a SKU, so they are written once instead of per locale. See `content.md` § Shared fields inside a localized type.
@@ -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
 
@@ -27,7 +27,21 @@ This file covers the **package surface**: the JSON shape you write, the generate
27
27
 
28
28
  **Required**: `uid` (PascalCase, matches filename), `name`, `fields`.
29
29
 
30
- `i18n` is an optional type-level policy. Omit it or use `{ "enabled": true }` for localized content. Use `{ "enabled": false }` for one shared entry that appears in every locale, such as an Author, Category, or Settings type. No other `i18n` keys and no field-level localization settings are supported.
30
+ `i18n` is an optional type-level policy. Omit it or use `{ "enabled": true }` for localized content. Use `{ "enabled": false }` for one shared entry that appears in every locale, such as an Author, Category, or Settings type. No other schema-root `i18n` keys are supported.
31
+
32
+ Inside a localized type, an individual top-level scalar or reference field may carry `"i18n": false`. That field then holds one value for the whole document: writing it through any locale updates all of them. Use it for values that are not translated, such as a price, a SKU, a product photo, or a reference that should point at the same target everywhere. Fields inside `blocks` follow the locale of the entry containing them and reject the flag.
33
+
34
+ ```json
35
+ {
36
+ "uid": "Product",
37
+ "name": "Products",
38
+ "fields": {
39
+ "title": { "type": "string" },
40
+ "sku": { "type": "string", "i18n": false },
41
+ "price": { "type": "number", "i18n": false }
42
+ }
43
+ }
44
+ ```
31
45
 
32
46
  ### Reserved field names — do NOT declare these in `fields`
33
47
 
@@ -61,6 +75,48 @@ something else (e.g. `handle`, `permalink`).
61
75
 
62
76
  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
77
 
78
+ ## Enumerated strings
79
+
80
+ 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`.
81
+
82
+ ```json
83
+ "layout": { "type": "string", "options": ["article", "video"], "default": "article" }
84
+ ```
85
+
86
+ - Values must be unique. Set a `default` that is one of them.
87
+ - `options` cannot be combined with `regex`, `minLength`, or `maxLength`, and is rejected on every field type other than `string`.
88
+ - 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.
89
+ - 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.
90
+
91
+ ## Conditional fields
92
+
93
+ 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.
94
+
95
+ ```json
96
+ {
97
+ "layout": { "type": "string", "options": ["article", "video"], "default": "article" },
98
+ "body": { "type": "richtext", "showIf": { "field": "layout", "equals": "article" } },
99
+ "videoUrl": { "type": "string", "required": true, "showIf": { "field": "layout", "equals": "video" } },
100
+ "transcript": {
101
+ "type": "text",
102
+ "showIfAny": [
103
+ { "field": "layout", "equals": "video" },
104
+ { "field": "hasAudio", "equals": true }
105
+ ]
106
+ }
107
+ }
108
+ ```
109
+
110
+ - A clause is `{ "field": "<sibling>", "equals": <value> }` or `{ "field": "<sibling>", "in": [<values>] }`. Use `in` for several values of one field.
111
+ - `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.
112
+ - **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.
113
+ - **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.
114
+ - Because a conditional field can be absent, it is always optional in the generated types even when `required` is set.
115
+ - Conditions resolve against siblings in the same object. A field inside a block variant may only reference fields in that same block item.
116
+ - 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.
117
+ - A trigger cannot itself be conditional — conditions are one level deep, so a chain like A shows B shows C is rejected.
118
+ - `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.
119
+
64
120
  ## Blocks
65
121
 
66
122
  `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.
@@ -139,7 +195,7 @@ Rules:
139
195
  - Self-references are allowed, e.g. a `Category` schema can have `parent: { "type": "reference", "target": "Category" }`.
140
196
  - Entry values use the target's stable `id`, never the variant `entryId`: `{ "id": "..." }`.
141
197
  - Do not include `contentType` for normal fixed-target references; the schema's `target` already defines it.
142
- - Do not add `localized` or field-level `i18n` to a reference. The reference value belongs to the entry variant that stores it.
198
+ - `localized` is not a supported spelling. A reference may carry `"i18n": false` so every locale of the document points at the same target; without it the reference belongs to the entry variant that stores it.
143
199
  - Use `null` for an empty to-one reference and `[]` for an empty to-many reference.
144
200
  - The API validates that referenced target documents exist and rejects duplicate values in to-many reference arrays.
145
201
 
@@ -272,7 +328,7 @@ interface BlogPost {
272
328
  };
273
329
  title: FimoString;
274
330
  summary: FimoString;
275
- body: FimoRichText;
331
+ body?: FimoRichText | null;
276
332
  coverImage: FimoMedia;
277
333
  publishDate: FimoString;
278
334
  featured: FimoBoolean;
@@ -282,6 +338,8 @@ interface BlogPost {
282
338
 
283
339
  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
340
 
341
+ 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.
342
+
285
343
  ## Localized and shared content
286
344
 
287
345
  Localized content has one variant per locale. Generated hooks read the active Fimo locale automatically.
@@ -296,7 +354,7 @@ For shared content, set `i18n: { "enabled": false }` on the schema and create ea
296
354
  - ignore locale on owner reads and writes;
297
355
  - still use the active/requested locale when a populated reference targets a localized schema.
298
356
 
299
- Do not duplicate shared entries across locales or add field-level localization settings.
357
+ Do not duplicate shared entries across locales. This section is about a whole type being shared; a single shared field inside a localized type is the separate `"i18n": false` field flag described above.
300
358
 
301
359
  A populated reference whose target has no variant in the requested locale resolves to null. Reads accept an opt-in `localeFallbacks: true` that resolves those to the best available variant instead (project default first), marked by its `locale` field — never implement fallbacks by hand; public reads stay strict unless the option is passed.
302
360
 
@@ -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.
@@ -53,6 +64,8 @@ Your edits save automatically as you change each field (the save is debounced),
53
64
 
54
65
  If a field is invalid, the editor keeps the change unsaved and pins a status bar below the header. Choose **Review issues** to jump to the first invalid field, then **Next issue** to move through the remaining fields. Collapsed blocks open automatically. Once every issue is fixed, the editor confirms that all fields are valid and resumes saving.
55
66
 
67
+ A field labelled with a padlocked globe is shared across languages: it holds one value for the whole entry, so editing it in any language changes it in all of them. Fields without that marker are per language and only affect the language you are editing. Hover the icon for the same explanation in context.
68
+
56
69
  ## Editing Text
57
70
 
58
71
  - Short text such as titles, names, and slugs uses one compact line.
@@ -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 Media. Webhooks belong to the project, with up to 20 configurations. They listen to main by default or can include every environment.
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
 
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "bundled": true,
3
3
  "bundler": "esbuild",
4
- "bundledAt": "2026-07-24T11:14:53.290Z",
4
+ "bundledAt": "2026-07-27T07:55:40.361Z",
5
5
  "cliVersion": "0.9.0",
6
6
  "external": [
7
7
  "oxc-parser",