fimo 0.3.6 → 0.4.0-experimental.2
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/hooks-template.eta +8 -4
- package/assets/skills/fimo/references/content.md +67 -14
- package/assets/skills/fimo-cli/references/content.md +21 -1
- package/dist/cli/bundle.json +2 -2
- package/dist/cli/index.js +253 -159
- package/dist/runtime/primitives/lib/query.d.ts +1 -0
- package/dist/runtime/primitives/lib/query.d.ts.map +1 -1
- package/dist/runtime/primitives/public-surface.test.js +2 -0
- package/dist/scripts/validate-schemas.d.ts +1 -1
- package/dist/scripts/validate-schemas.js +72 -3
- package/dist/scripts/validate-schemas.test.js +82 -0
- package/package.json +1 -1
- package/release.json +2 -2
- package/templates/react-router/package.json +1 -1
- package/templates/react-router/src/routes.ts +4 -5
|
@@ -22,7 +22,6 @@ import fimoConfig from "virtual:fimo-config";
|
|
|
22
22
|
*/
|
|
23
23
|
export interface <%= model.pascal %>FimoMeta {
|
|
24
24
|
contentType: string;
|
|
25
|
-
translationProgress: { state: "queued" | "translating"; fields?: string[] } | null;
|
|
26
25
|
}
|
|
27
26
|
|
|
28
27
|
export interface <%= model.pascal %> {
|
|
@@ -64,6 +63,8 @@ function getTypeForField(field) {
|
|
|
64
63
|
return 'Record<string, any>';
|
|
65
64
|
case 'media':
|
|
66
65
|
return field.multiple ? 'FimoMedia[]' : 'FimoMedia';
|
|
66
|
+
case 'relation':
|
|
67
|
+
return field.many ? 'Array<string | Record<string, any>>' : 'string | Record<string, any> | null';
|
|
67
68
|
default:
|
|
68
69
|
return 'any';
|
|
69
70
|
}
|
|
@@ -102,7 +103,6 @@ function wrapWithSource(entry: Record<string, any>): <%= model.pascal %> {
|
|
|
102
103
|
updatedAt: entry.updatedAt,
|
|
103
104
|
__fimo: {
|
|
104
105
|
contentType: typeof entryFimo.contentType === "string" ? entryFimo.contentType : "<%= model.uid %>",
|
|
105
|
-
translationProgress: entryFimo.translationProgress && typeof entryFimo.translationProgress === "object" ? entryFimo.translationProgress as <%= model.pascal %>FimoMeta["translationProgress"] : null,
|
|
106
106
|
},
|
|
107
107
|
<%
|
|
108
108
|
if (model.fields) {
|
|
@@ -216,7 +216,7 @@ export async function getByField(fieldName: string, value: string, options: { lo
|
|
|
216
216
|
}
|
|
217
217
|
|
|
218
218
|
function toQueryString<TFields extends <%= model.pascal %>Fields | undefined = undefined>(params: <%= model.pascal %>Query<TFields> = {} as <%= model.pascal %>Query<TFields>): string {
|
|
219
|
-
const { where, sort, fields, ...flat } = params;
|
|
219
|
+
const { where, sort, fields, populate, ...flat } = params;
|
|
220
220
|
const query = new URLSearchParams();
|
|
221
221
|
|
|
222
222
|
for (const [key, value] of Object.entries(flat)) {
|
|
@@ -229,7 +229,11 @@ function toQueryString<TFields extends <%= model.pascal %>Fields | undefined = u
|
|
|
229
229
|
}
|
|
230
230
|
|
|
231
231
|
if (fields != null) {
|
|
232
|
-
query.set("fields",
|
|
232
|
+
query.set("fields", typeof fields === "string" ? fields : fields.join(","));
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (populate != null) {
|
|
236
|
+
query.set("populate", typeof populate === "string" ? populate : populate.join(","));
|
|
233
237
|
}
|
|
234
238
|
|
|
235
239
|
if (where && Object.keys(where).length > 0) {
|
|
@@ -16,7 +16,9 @@ This file covers the **package surface**: the JSON shape you write, the generate
|
|
|
16
16
|
"publishDate": { "type": "string" },
|
|
17
17
|
"featured": { "type": "boolean" },
|
|
18
18
|
"coverImage": { "type": "media" },
|
|
19
|
-
"metadata": { "type": "json" }
|
|
19
|
+
"metadata": { "type": "json" },
|
|
20
|
+
"author": { "type": "relation", "target": "Author" },
|
|
21
|
+
"contributors": { "type": "relation", "target": "Author", "many": true }
|
|
20
22
|
}
|
|
21
23
|
}
|
|
22
24
|
```
|
|
@@ -32,7 +34,7 @@ reserved entry metadata and cannot be used as a content field.`):
|
|
|
32
34
|
`id`, `documentId`, `slug`, `locale`, `createdAt`, `updatedAt`, `__fimo`.
|
|
33
35
|
|
|
34
36
|
In particular **do not add a `slug` field** — every entry gets a `slug` for free. You still
|
|
35
|
-
pass `slug` inside `data` when
|
|
37
|
+
pass `slug` inside `data` when _creating an entry_ (see "Entry body shape" below); it just
|
|
36
38
|
must not appear in the schema's `fields`. If you need an additional URL-ish string, name it
|
|
37
39
|
something else (e.g. `handle`, `permalink`).
|
|
38
40
|
|
|
@@ -47,9 +49,47 @@ something else (e.g. `handle`, `permalink`).
|
|
|
47
49
|
| `date` | ISO date string, e.g. `"2026-06-03"` | Publish dates, timestamps |
|
|
48
50
|
| `media` | `{ "source": "local"\|"external", "url": "https://...", "alt": "..." }` | Images, video |
|
|
49
51
|
| `json` | any JSON | Structured metadata |
|
|
52
|
+
| `relation` | document ID string, `null`, or document ID array when `many: true` | References to another schema |
|
|
50
53
|
|
|
51
54
|
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.
|
|
52
55
|
|
|
56
|
+
## Relation fields
|
|
57
|
+
|
|
58
|
+
Use `relation` when one content entry references another content entry. Do not store entry IDs in a `json` field as a workaround.
|
|
59
|
+
|
|
60
|
+
```json
|
|
61
|
+
{
|
|
62
|
+
"uid": "BlogPost",
|
|
63
|
+
"name": "Blog Posts",
|
|
64
|
+
"fields": {
|
|
65
|
+
"title": { "type": "string" },
|
|
66
|
+
"author": { "type": "relation", "target": "Author" },
|
|
67
|
+
"contributors": { "type": "relation", "target": "Author", "many": true }
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Rules:
|
|
73
|
+
|
|
74
|
+
- `target` is required and must be the PascalCase schema `uid` of the referenced content type.
|
|
75
|
+
- Omit `many` for a to-one relation. Use `"many": true` for a to-many relation.
|
|
76
|
+
- Self-references are allowed, e.g. a `Category` schema can have `parent: { "type": "relation", "target": "Category" }`.
|
|
77
|
+
- Entry values store target `documentId` values, not row `id` values. Use `null` for an empty to-one relation and `[]` for an empty to-many relation.
|
|
78
|
+
- The API validates that referenced target documents exist and rejects duplicate values in to-many relation arrays.
|
|
79
|
+
|
|
80
|
+
Entry payload example:
|
|
81
|
+
|
|
82
|
+
```json
|
|
83
|
+
{
|
|
84
|
+
"data": {
|
|
85
|
+
"slug": "relation-example",
|
|
86
|
+
"title": "Relation example",
|
|
87
|
+
"author": "750e8400-e29b-41d4-a716-446655440000",
|
|
88
|
+
"contributors": ["750e8400-e29b-41d4-a716-446655440000"]
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
53
93
|
**Default to `richtext`** for fields named `body`, `content`, `description`, `summary`, `bio`, `intro`, `details`, `excerpt` — only use `string` when the field is explicitly short (e.g. a 160-char meta description).
|
|
54
94
|
|
|
55
95
|
## Tiptap JSONContent (richtext)
|
|
@@ -133,7 +173,6 @@ interface BlogPost {
|
|
|
133
173
|
updatedAt: string;
|
|
134
174
|
__fimo: {
|
|
135
175
|
contentType: string;
|
|
136
|
-
translationProgress: { state: 'queued' | 'translating'; fields?: string[] } | null;
|
|
137
176
|
};
|
|
138
177
|
title: FimoString;
|
|
139
178
|
body: FimoRichText;
|
|
@@ -144,7 +183,7 @@ interface BlogPost {
|
|
|
144
183
|
}
|
|
145
184
|
```
|
|
146
185
|
|
|
147
|
-
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
|
|
186
|
+
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`.
|
|
148
187
|
|
|
149
188
|
## Localized content
|
|
150
189
|
|
|
@@ -183,7 +222,7 @@ if (isLoading || !posts) return null;
|
|
|
183
222
|
posts.map((post) => /* ... */); // BlogPost[]
|
|
184
223
|
```
|
|
185
224
|
|
|
186
|
-
Query params supported: `limit` (1-100, default 20), `offset` (default 0), `search`, `sort`, `where`, and `
|
|
225
|
+
Query params supported: `limit` (1-100, default 20), `offset` (default 0), `search`, `sort`, `where`, `fields`, and `populate`. Use these canonical names in new code.
|
|
187
226
|
|
|
188
227
|
`where` filters on the server. Use scalar values for equality and operator objects for ranges, lists, text matches, or null checks. Keep boolean, string, and number values typed in the object:
|
|
189
228
|
|
|
@@ -215,21 +254,35 @@ const { data: summaries = [] } = BlogPost.useGet({
|
|
|
215
254
|
// summaries: Array<Pick<BlogPost, 'id' | 'slug' | 'title' | 'excerpt' | 'coverImage'>>
|
|
216
255
|
```
|
|
217
256
|
|
|
257
|
+
Use `populate` to embed relation targets in read results:
|
|
258
|
+
|
|
259
|
+
```tsx
|
|
260
|
+
const { data: posts = [] } = BlogPost.useGet({
|
|
261
|
+
fields: ['slug', 'title', 'author'] as const,
|
|
262
|
+
populate: ['author'] as const,
|
|
263
|
+
});
|
|
264
|
+
// Without populate: post.author is a target documentId string.
|
|
265
|
+
// With populate: post.author is the target Author entry object, or null.
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
Populate is depth-one. It only works for relation fields; trying to populate a scalar field returns 400. To-many relation order is preserved.
|
|
269
|
+
|
|
218
270
|
The generated hooks do **not** surface pagination meta (`total`, `hasMore`, `limit`, `offset`). If you need it, read it from a raw fetch to `/entries/<typeName>` (see the **Read path** section below), which returns the full `{ data, meta }` envelope.
|
|
219
271
|
|
|
220
272
|
## Fimo primitives (what the field values are)
|
|
221
273
|
|
|
222
274
|
The entry objects a hook returns are **not** plain JS values. The generated client wraps each _trackable_ field in a `fimo/ui` primitive that carries source metadata for inline editing. Which fields wrap, and which stay plain:
|
|
223
275
|
|
|
224
|
-
| Schema field type | Field value you get | Notes
|
|
225
|
-
| ----------------- | --------------------------- |
|
|
226
|
-
| `string` | `FimoString` | `extends String` — **not** a primitive `string`
|
|
227
|
-
| `richtext` | `FimoRichText` | Tiptap JSONContent + source metadata
|
|
228
|
-
| `boolean` | `FimoBoolean` | class; `.valueOf()` → `boolean`
|
|
229
|
-
| `media` | `FimoMedia` / `FimoMedia[]` | shape + rendering in `references/assets.md`
|
|
230
|
-
| `number` | `number` | plain — not wrapped
|
|
231
|
-
| `json` | `Record<string, any>` | plain — not wrapped
|
|
232
|
-
| `date` | `any` (raw ISO date string) | not wrapped (`FimoDate` is not emitted — see below)
|
|
276
|
+
| Schema field type | Field value you get | Notes |
|
|
277
|
+
| ----------------- | --------------------------- | -------------------------------------------------------------------------------------------- |
|
|
278
|
+
| `string` | `FimoString` | `extends String` — **not** a primitive `string` |
|
|
279
|
+
| `richtext` | `FimoRichText` | Tiptap JSONContent + source metadata |
|
|
280
|
+
| `boolean` | `FimoBoolean` | class; `.valueOf()` → `boolean` |
|
|
281
|
+
| `media` | `FimoMedia` / `FimoMedia[]` | shape + rendering in `references/assets.md` |
|
|
282
|
+
| `number` | `number` | plain — not wrapped |
|
|
283
|
+
| `json` | `Record<string, any>` | plain — not wrapped |
|
|
284
|
+
| `date` | `any` (raw ISO date string) | not wrapped (`FimoDate` is not emitted — see below) |
|
|
285
|
+
| `relation` | `string` / `string[]` | target `documentId` value(s); use `populate` on raw API when embedded target rows are needed |
|
|
233
286
|
|
|
234
287
|
Rules of thumb:
|
|
235
288
|
|
|
@@ -36,7 +36,7 @@ Entries are NOT JSON-file-driven. Create/update them via CLI.
|
|
|
36
36
|
```bash
|
|
37
37
|
fimo entries create <TypeName> --body '<json>'
|
|
38
38
|
fimo entries create <TypeName> --locale es --document-id <documentId> --body '<json>'
|
|
39
|
-
fimo entries list <TypeName> [--limit N] [--offset N] [--search Q] [--where JSON] [--sort F] [--fields FIELDS]
|
|
39
|
+
fimo entries list <TypeName> [--limit N] [--offset N] [--search Q] [--where JSON] [--sort F] [--fields FIELDS] [--populate FIELDS]
|
|
40
40
|
fimo entries list <TypeName> --locale es
|
|
41
41
|
fimo entries get <TypeName> <id>
|
|
42
42
|
fimo entries update <TypeName> <id> --locale es --body '<json>'
|
|
@@ -54,12 +54,32 @@ fimo entries list BlogPost --where '{"__fimo":{"contentType":{"$eq":"BlogPost"}}
|
|
|
54
54
|
fimo entries list BlogPost --search "launch"
|
|
55
55
|
fimo entries list BlogPost --limit 10 --offset 20 --sort title
|
|
56
56
|
fimo entries list BlogPost --fields slug,title,excerpt,coverImage --sort -publishedAt --limit 12
|
|
57
|
+
fimo entries list BlogPost --fields slug,title,author --populate author
|
|
57
58
|
```
|
|
58
59
|
|
|
59
60
|
Use the canonical read query flags: `--search`, `--sort`, `--where`, `--limit`, `--offset`, and `--fields`.
|
|
60
61
|
`--where` is a JSON object keyed by schema field name. Simple values mean equality, so booleans, strings, and numbers should stay typed inside the JSON (`{"featured":true}`, `{"category":"react"}`, `{"rank":{"$gte":3}}`). Public Fimo-owned metadata lives under `__fimo`; currently `__fimo.contentType` is filterable. Operator objects support `$eq`, `$ne`, `$gt`, `$gte`, `$lt`, `$lte`, `$in`, `$nin`, `$contains`, `$startsWith`, and `$null`, subject to the field type. Use `--sort field` for ascending and `--sort -field` for descending.
|
|
61
62
|
`--fields` is a comma-separated projection for list payloads. The API always keeps `id`; pass only the fields your view needs, such as `slug,title,excerpt,coverImage`.
|
|
62
63
|
|
|
64
|
+
`--populate` is a comma-separated list of relation fields to embed. Without `--populate`, relation fields return target `documentId` strings. With `--populate author,contributors`, those fields return the target entry objects for the requested locale. Only relation fields can be populated; asking to populate a scalar field returns 400.
|
|
65
|
+
|
|
66
|
+
For relation schema JSON and entry value shape, load `fimo/references/content.md` § Relation fields. The short version:
|
|
67
|
+
|
|
68
|
+
```json
|
|
69
|
+
{
|
|
70
|
+
"fields": {
|
|
71
|
+
"author": { "type": "relation", "target": "Author" },
|
|
72
|
+
"contributors": { "type": "relation", "target": "Author", "many": true }
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Entry bodies store target `documentId` values:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
fimo entries create BlogPost --body '{"data":{"slug":"hello","title":"Hello","author":"750e8400-e29b-41d4-a716-446655440000","contributors":[]}}'
|
|
81
|
+
```
|
|
82
|
+
|
|
63
83
|
## Localized content
|
|
64
84
|
|
|
65
85
|
Content entries are stored per locale. Omit `--locale` for default-locale entries. Pass `--locale <locale>` when creating, listing, reading, or updating a specific non-default locale.
|
package/dist/cli/bundle.json
CHANGED
package/dist/cli/index.js
CHANGED
|
@@ -41921,7 +41921,7 @@ var init_pagination = __esm({
|
|
|
41921
41921
|
});
|
|
41922
41922
|
|
|
41923
41923
|
// ../../packages/api-contracts/dist/esm/types/common.js
|
|
41924
|
-
var CommonSchemas, BaseEntitySchema, ProjectRoleKeySchema, ProjectRoleKeyWithSystemSchema, ProjectPrincipalTypeSchema, ProjectVisibilitySchema, ProjectStatusSchema, ProjectInviteStatusSchema, PublicationStatusSchema, OrganizationRoleSchema, AssetTypeSchema, TemplateCodeSchema, FieldTypeSchema, RESERVED_CONTENT_FIELD_NAMES;
|
|
41924
|
+
var CommonSchemas, BaseEntitySchema, ProjectRoleKeySchema, ProjectRoleKeyWithSystemSchema, ProjectPrincipalTypeSchema, ProjectVisibilitySchema, ProjectStatusSchema, ProjectInviteStatusSchema, PublicationStatusSchema, OrganizationRoleSchema, AssetTypeSchema, TemplateCodeSchema, FieldTypeSchema, RESERVED_CONTENT_FIELD_NAMES, CONTENT_TYPE_NAME_PATTERN, CONTENT_TYPE_NAME_PATTERN_DESCRIPTION, CONTENT_FIELD_NAME_PATTERN, CONTENT_FIELD_NAME_PATTERN_DESCRIPTION;
|
|
41925
41925
|
var init_common = __esm({
|
|
41926
41926
|
"../../packages/api-contracts/dist/esm/types/common.js"() {
|
|
41927
41927
|
"use strict";
|
|
@@ -42000,7 +42000,8 @@ var init_common = __esm({
|
|
|
42000
42000
|
"number",
|
|
42001
42001
|
"boolean",
|
|
42002
42002
|
"date",
|
|
42003
|
-
"json"
|
|
42003
|
+
"json",
|
|
42004
|
+
"relation"
|
|
42004
42005
|
]);
|
|
42005
42006
|
RESERVED_CONTENT_FIELD_NAMES = [
|
|
42006
42007
|
"id",
|
|
@@ -42011,6 +42012,10 @@ var init_common = __esm({
|
|
|
42011
42012
|
"updatedAt",
|
|
42012
42013
|
"__fimo"
|
|
42013
42014
|
];
|
|
42015
|
+
CONTENT_TYPE_NAME_PATTERN = /^[A-Za-z]\w*$/;
|
|
42016
|
+
CONTENT_TYPE_NAME_PATTERN_DESCRIPTION = "letters, numbers, and underscores, starting with a letter";
|
|
42017
|
+
CONTENT_FIELD_NAME_PATTERN = /^[A-Za-z_]\w*$/;
|
|
42018
|
+
CONTENT_FIELD_NAME_PATTERN_DESCRIPTION = "letters, numbers, and underscores, starting with a letter or underscore";
|
|
42014
42019
|
}
|
|
42015
42020
|
});
|
|
42016
42021
|
|
|
@@ -44457,10 +44462,7 @@ var init_translation_dto = __esm({
|
|
|
44457
44462
|
key: external_exports.string(),
|
|
44458
44463
|
value: external_exports.string().optional(),
|
|
44459
44464
|
defaultValue: external_exports.string(),
|
|
44460
|
-
locale: external_exports.string()
|
|
44461
|
-
sourceLocale: external_exports.string().nullable().optional(),
|
|
44462
|
-
translationStatus: external_exports.string().optional(),
|
|
44463
|
-
translationMeta: external_exports.record(external_exports.string(), external_exports.unknown()).nullable().optional()
|
|
44465
|
+
locale: external_exports.string()
|
|
44464
44466
|
});
|
|
44465
44467
|
TranslationDTO = createDTO(TranslationDTOSchema);
|
|
44466
44468
|
}
|
|
@@ -45831,6 +45833,16 @@ function planRemoteConfig(remoteUrl) {
|
|
|
45831
45833
|
const credentialKey = `credential.${u2.protocol}//${u2.host}`;
|
|
45832
45834
|
return { cleanUrl, credentialKey };
|
|
45833
45835
|
}
|
|
45836
|
+
function shellQuote(value) {
|
|
45837
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
45838
|
+
}
|
|
45839
|
+
function credentialHelperCommand(argv = process.argv) {
|
|
45840
|
+
const entry = argv[1];
|
|
45841
|
+
if (!entry) {
|
|
45842
|
+
return "!fimo git-credential";
|
|
45843
|
+
}
|
|
45844
|
+
return `!${shellQuote(process.execPath)} ${shellQuote(entry)} git-credential`;
|
|
45845
|
+
}
|
|
45834
45846
|
async function ensureFimoRemote(cwd, remoteUrl) {
|
|
45835
45847
|
const { cleanUrl, credentialKey } = planRemoteConfig(remoteUrl);
|
|
45836
45848
|
await runGit2(["remote", "set-url", "fimo", cleanUrl], cwd).catch(async () => {
|
|
@@ -45838,7 +45850,7 @@ async function ensureFimoRemote(cwd, remoteUrl) {
|
|
|
45838
45850
|
});
|
|
45839
45851
|
await runGit2(["config", "--unset-all", `${credentialKey}.helper`], cwd).catch(() => void 0);
|
|
45840
45852
|
await runGit2(["config", "--add", `${credentialKey}.helper`, ""], cwd);
|
|
45841
|
-
await runGit2(["config", "--add", `${credentialKey}.helper`,
|
|
45853
|
+
await runGit2(["config", "--add", `${credentialKey}.helper`, credentialHelperCommand()], cwd);
|
|
45842
45854
|
await runGit2(["config", `${credentialKey}.useHttpPath`, "true"], cwd);
|
|
45843
45855
|
await runGit2(["config", `${credentialKey}.username`, "t"], cwd);
|
|
45844
45856
|
}
|
|
@@ -45846,6 +45858,8 @@ var init_git_credential_setup = __esm({
|
|
|
45846
45858
|
"src/cli/utils/git-credential-setup.ts"() {
|
|
45847
45859
|
init_git();
|
|
45848
45860
|
__name(planRemoteConfig, "planRemoteConfig");
|
|
45861
|
+
__name(shellQuote, "shellQuote");
|
|
45862
|
+
__name(credentialHelperCommand, "credentialHelperCommand");
|
|
45849
45863
|
__name(ensureFimoRemote, "ensureFimoRemote");
|
|
45850
45864
|
}
|
|
45851
45865
|
});
|
|
@@ -101079,6 +101093,10 @@ __name(listTemplatesCommand, "listTemplatesCommand");
|
|
|
101079
101093
|
import fs15 from "node:fs";
|
|
101080
101094
|
import path19 from "node:path";
|
|
101081
101095
|
init_dist();
|
|
101096
|
+
init_api();
|
|
101097
|
+
init_auth();
|
|
101098
|
+
init_config2();
|
|
101099
|
+
init_project();
|
|
101082
101100
|
async function runValidateAgents(opts) {
|
|
101083
101101
|
const projectRoot = resolveProjectRoot(opts.cwd ?? process.cwd());
|
|
101084
101102
|
const agentsDir = path19.join(projectRoot, "agents");
|
|
@@ -101261,6 +101279,94 @@ function validateOneBundle(bundle, allBundles) {
|
|
|
101261
101279
|
return { name: bundle.name, ok, phases, warnings };
|
|
101262
101280
|
}
|
|
101263
101281
|
__name(validateOneBundle, "validateOneBundle");
|
|
101282
|
+
async function runAvailabilityChecks(cwd) {
|
|
101283
|
+
const projectRoot = resolveProjectRoot(cwd);
|
|
101284
|
+
let projectId;
|
|
101285
|
+
try {
|
|
101286
|
+
projectId = loadProjectSettings(projectRoot).projectId;
|
|
101287
|
+
} catch {
|
|
101288
|
+
projectId = process.env.FIMO_PROJECT_ID?.trim() ?? "";
|
|
101289
|
+
if (!projectId) {
|
|
101290
|
+
return { ok: true, skippedReason: "not-linked", issues: [] };
|
|
101291
|
+
}
|
|
101292
|
+
}
|
|
101293
|
+
if (!process.env.FIMO_API_TOKEN?.trim() && !readCredentials()) {
|
|
101294
|
+
return { ok: true, skippedReason: "not-signed-in", issues: [] };
|
|
101295
|
+
}
|
|
101296
|
+
const api = new FimoApiClient(loadConfig());
|
|
101297
|
+
const bundles = await listBundles(path19.join(projectRoot, "agents"));
|
|
101298
|
+
const issues = [];
|
|
101299
|
+
const integrationsByAgent = bundles.map((bundle) => ({
|
|
101300
|
+
name: bundle.name,
|
|
101301
|
+
integrations: Object.entries(agentIntegrationPolicy(bundle.config)).map(([integrationKey, entry]) => ({
|
|
101302
|
+
integrationKey,
|
|
101303
|
+
enabled: entry.enabled,
|
|
101304
|
+
tools: enabledAgentIntegrationTools(entry)
|
|
101305
|
+
})).filter((entry) => entry.enabled)
|
|
101306
|
+
})).filter((entry) => entry.integrations.length > 0);
|
|
101307
|
+
if (integrationsByAgent.length > 0) {
|
|
101308
|
+
const statuses = await api.listProjectIntegrations(projectId);
|
|
101309
|
+
const statusByKey = new Map(statuses.map((status) => [status.integrationKey, status]));
|
|
101310
|
+
const infoByKey = /* @__PURE__ */ new Map();
|
|
101311
|
+
for (const agent of integrationsByAgent) {
|
|
101312
|
+
for (const integration of agent.integrations) {
|
|
101313
|
+
const file2 = `agents/${agent.name}/config.yaml`;
|
|
101314
|
+
const status = statusByKey.get(integration.integrationKey);
|
|
101315
|
+
if (!status) {
|
|
101316
|
+
issues.push({
|
|
101317
|
+
pointer: file2,
|
|
101318
|
+
message: `${agent.name} allows unsupported integration ${integration.integrationKey}.`
|
|
101319
|
+
});
|
|
101320
|
+
continue;
|
|
101321
|
+
}
|
|
101322
|
+
if (integration.tools.length === 0) {
|
|
101323
|
+
issues.push({
|
|
101324
|
+
pointer: file2,
|
|
101325
|
+
message: `${agent.name} enables ${integration.integrationKey}, but no tools are allowed. Set at least one integrations.${integration.integrationKey}.tools.<tool>: true entry.`
|
|
101326
|
+
});
|
|
101327
|
+
}
|
|
101328
|
+
if (!status.enabled) {
|
|
101329
|
+
issues.push({
|
|
101330
|
+
pointer: file2,
|
|
101331
|
+
message: `${agent.name} allows ${integration.integrationKey}, but the project integration is not enabled. Run: fimo integrations enable ${integration.integrationKey}`
|
|
101332
|
+
});
|
|
101333
|
+
}
|
|
101334
|
+
const info = infoByKey.get(integration.integrationKey) ?? await api.getProjectIntegrationInfo(projectId, integration.integrationKey);
|
|
101335
|
+
infoByKey.set(integration.integrationKey, info);
|
|
101336
|
+
const knownTools = new Set(info.tools.map((tool) => tool.key));
|
|
101337
|
+
for (const tool of integration.tools) {
|
|
101338
|
+
if (!knownTools.has(tool)) {
|
|
101339
|
+
issues.push({
|
|
101340
|
+
pointer: file2,
|
|
101341
|
+
message: `${agent.name} allows unknown ${integration.integrationKey}.${tool} tool. Supported tools: ${[...knownTools].sort().join(", ")}`
|
|
101342
|
+
});
|
|
101343
|
+
}
|
|
101344
|
+
}
|
|
101345
|
+
}
|
|
101346
|
+
}
|
|
101347
|
+
}
|
|
101348
|
+
const secretsByAgent = bundles.map((bundle) => ({
|
|
101349
|
+
name: bundle.name,
|
|
101350
|
+
keys: (bundle.config.secrets ?? []).filter((secret) => secret.required !== false).map((secret) => secret.key)
|
|
101351
|
+
})).filter((entry) => entry.keys.length > 0);
|
|
101352
|
+
if (secretsByAgent.length > 0) {
|
|
101353
|
+
const defined = new Set(
|
|
101354
|
+
(await api.listProjectSecretEntries(projectId, { kind: "secret" })).map((secret) => secret.key)
|
|
101355
|
+
);
|
|
101356
|
+
for (const entry of secretsByAgent) {
|
|
101357
|
+
for (const key of entry.keys) {
|
|
101358
|
+
if (!defined.has(key)) {
|
|
101359
|
+
issues.push({
|
|
101360
|
+
pointer: `agents/${entry.name}/config.yaml`,
|
|
101361
|
+
message: `${entry.name} requires project secret ${key}, but it is not set. Run: fimo secrets set ${key} --stdin`
|
|
101362
|
+
});
|
|
101363
|
+
}
|
|
101364
|
+
}
|
|
101365
|
+
}
|
|
101366
|
+
}
|
|
101367
|
+
return { ok: issues.length === 0, skippedReason: null, issues };
|
|
101368
|
+
}
|
|
101369
|
+
__name(runAvailabilityChecks, "runAvailabilityChecks");
|
|
101264
101370
|
async function validateAgentsCommand(opts) {
|
|
101265
101371
|
let report;
|
|
101266
101372
|
try {
|
|
@@ -101276,6 +101382,10 @@ async function validateAgentsCommand(opts) {
|
|
|
101276
101382
|
ui.info({ command: "agents validate", message: "No bundles or fimo.config.json found." });
|
|
101277
101383
|
return report;
|
|
101278
101384
|
}
|
|
101385
|
+
report.availability = await runAvailabilityChecks(opts.cwd ?? process.cwd());
|
|
101386
|
+
if (!report.availability.ok) {
|
|
101387
|
+
report.ok = false;
|
|
101388
|
+
}
|
|
101279
101389
|
ui.result({
|
|
101280
101390
|
command: "agents validate",
|
|
101281
101391
|
data: report,
|
|
@@ -101320,6 +101430,19 @@ function renderReportHuman(report) {
|
|
|
101320
101430
|
}
|
|
101321
101431
|
}
|
|
101322
101432
|
}
|
|
101433
|
+
if (report.availability) {
|
|
101434
|
+
if (report.availability.skippedReason) {
|
|
101435
|
+
const why = report.availability.skippedReason === "not-signed-in" ? "not signed in \u2014 run `fimo login`" : "no linked project";
|
|
101436
|
+
lines.push(` \u21B7 integrations & secrets \u2014 skipped (${why})`);
|
|
101437
|
+
} else if (report.availability.ok) {
|
|
101438
|
+
lines.push(" \u2713 integrations & secrets available");
|
|
101439
|
+
} else {
|
|
101440
|
+
lines.push(" \u2717 integrations & secrets");
|
|
101441
|
+
for (const issue2 of report.availability.issues) {
|
|
101442
|
+
lines.push(` \u2717 ${issue2.message}`);
|
|
101443
|
+
}
|
|
101444
|
+
}
|
|
101445
|
+
}
|
|
101323
101446
|
return lines.join("\n");
|
|
101324
101447
|
}
|
|
101325
101448
|
__name(renderReportHuman, "renderReportHuman");
|
|
@@ -102283,9 +102406,9 @@ init_interactive();
|
|
|
102283
102406
|
init_project_context();
|
|
102284
102407
|
|
|
102285
102408
|
// src/cli/commands/validate.ts
|
|
102409
|
+
init_dist();
|
|
102286
102410
|
import { readFile as readFile3 } from "node:fs/promises";
|
|
102287
102411
|
import path29 from "node:path";
|
|
102288
|
-
init_dist();
|
|
102289
102412
|
|
|
102290
102413
|
// src/runtime/shared/fimo-config.server.js
|
|
102291
102414
|
import { readFileSync as readFileSync2, renameSync, writeFileSync as writeFileSync3 } from "fs";
|
|
@@ -102736,14 +102859,31 @@ function validateSchemas(options = {}) {
|
|
|
102736
102859
|
}
|
|
102737
102860
|
const files = readdirSync4(schemasDir).filter((f) => f.endsWith(".json")).sort();
|
|
102738
102861
|
const errors2 = [];
|
|
102862
|
+
const schemaUids = /* @__PURE__ */ new Set();
|
|
102863
|
+
for (const fileName of files) {
|
|
102864
|
+
const schema = readSchemaObject(join9(schemasDir, fileName));
|
|
102865
|
+
if (schema && typeof schema.uid === "string") {
|
|
102866
|
+
schemaUids.add(schema.uid);
|
|
102867
|
+
}
|
|
102868
|
+
}
|
|
102739
102869
|
for (const fileName of files) {
|
|
102740
102870
|
const file2 = join9(SCHEMAS_REL2, fileName);
|
|
102741
|
-
errors2.push(...validateSchemaFile(join9(schemasDir, fileName), file2));
|
|
102871
|
+
errors2.push(...validateSchemaFile(join9(schemasDir, fileName), file2, schemaUids));
|
|
102742
102872
|
}
|
|
102743
102873
|
return { errors: errors2, schemasChecked: files.length };
|
|
102744
102874
|
}
|
|
102745
102875
|
__name(validateSchemas, "validateSchemas");
|
|
102746
|
-
function
|
|
102876
|
+
function readSchemaObject(absolutePath) {
|
|
102877
|
+
let schema;
|
|
102878
|
+
try {
|
|
102879
|
+
schema = JSON.parse(readFileSync7(absolutePath, "utf-8"));
|
|
102880
|
+
} catch {
|
|
102881
|
+
return null;
|
|
102882
|
+
}
|
|
102883
|
+
return typeof schema === "object" && schema !== null && !Array.isArray(schema) ? schema : null;
|
|
102884
|
+
}
|
|
102885
|
+
__name(readSchemaObject, "readSchemaObject");
|
|
102886
|
+
function validateSchemaFile(absolutePath, file2, schemaUids = /* @__PURE__ */ new Set()) {
|
|
102747
102887
|
let schema;
|
|
102748
102888
|
try {
|
|
102749
102889
|
schema = JSON.parse(readFileSync7(absolutePath, "utf-8"));
|
|
@@ -102774,6 +102914,12 @@ function validateSchemaFile(absolutePath, file2) {
|
|
|
102774
102914
|
message: `"uid" is "${uid}" but the file is ${expectedUid}.json \u2014 they must match.`,
|
|
102775
102915
|
file: file2
|
|
102776
102916
|
});
|
|
102917
|
+
} else if (!CONTENT_TYPE_NAME_PATTERN.test(uid)) {
|
|
102918
|
+
errors2.push({
|
|
102919
|
+
rule: "schemas/type-name",
|
|
102920
|
+
message: `"uid" must use ${CONTENT_TYPE_NAME_PATTERN_DESCRIPTION}.`,
|
|
102921
|
+
file: file2
|
|
102922
|
+
});
|
|
102777
102923
|
}
|
|
102778
102924
|
if (typeof name !== "string" || name.length === 0) {
|
|
102779
102925
|
errors2.push({
|
|
@@ -102791,6 +102937,14 @@ function validateSchemaFile(absolutePath, file2) {
|
|
|
102791
102937
|
return errors2;
|
|
102792
102938
|
}
|
|
102793
102939
|
for (const [fieldName, definition] of Object.entries(fields)) {
|
|
102940
|
+
if (!CONTENT_FIELD_NAME_PATTERN.test(fieldName)) {
|
|
102941
|
+
errors2.push({
|
|
102942
|
+
rule: "schemas/field-name",
|
|
102943
|
+
message: `Field "${fieldName}" must use ${CONTENT_FIELD_NAME_PATTERN_DESCRIPTION}.`,
|
|
102944
|
+
file: file2
|
|
102945
|
+
});
|
|
102946
|
+
continue;
|
|
102947
|
+
}
|
|
102794
102948
|
if (RESERVED_FIELD_NAMES.has(fieldName)) {
|
|
102795
102949
|
errors2.push({
|
|
102796
102950
|
rule: "schemas/reserved-field",
|
|
@@ -102822,6 +102976,39 @@ function validateSchemaFile(absolutePath, file2) {
|
|
|
102822
102976
|
file: file2
|
|
102823
102977
|
});
|
|
102824
102978
|
}
|
|
102979
|
+
if (type === "relation") {
|
|
102980
|
+
const { target, many } = definition;
|
|
102981
|
+
if (typeof target !== "string" || !CONTENT_TYPE_NAME_PATTERN.test(target)) {
|
|
102982
|
+
errors2.push({
|
|
102983
|
+
rule: "schemas/relation-target",
|
|
102984
|
+
message: `Relation field "${fieldName}" must declare a valid "target" content type.`,
|
|
102985
|
+
file: file2
|
|
102986
|
+
});
|
|
102987
|
+
} else if (schemaUids.size > 0 && !schemaUids.has(target)) {
|
|
102988
|
+
errors2.push({
|
|
102989
|
+
rule: "schemas/relation-target",
|
|
102990
|
+
message: `Relation field "${fieldName}" targets unknown schema "${target}".`,
|
|
102991
|
+
file: file2
|
|
102992
|
+
});
|
|
102993
|
+
}
|
|
102994
|
+
if (many !== void 0 && typeof many !== "boolean") {
|
|
102995
|
+
errors2.push({
|
|
102996
|
+
rule: "schemas/relation-many",
|
|
102997
|
+
message: `Relation field "${fieldName}" must use a boolean "many" value.`,
|
|
102998
|
+
file: file2
|
|
102999
|
+
});
|
|
103000
|
+
}
|
|
103001
|
+
} else {
|
|
103002
|
+
for (const relationOnlyKey of ["target", "many", "localized"]) {
|
|
103003
|
+
if (relationOnlyKey in definition) {
|
|
103004
|
+
errors2.push({
|
|
103005
|
+
rule: "schemas/relation-shape",
|
|
103006
|
+
message: `Field "${fieldName}": "${relationOnlyKey}" is only supported on relation fields.`,
|
|
103007
|
+
file: file2
|
|
103008
|
+
});
|
|
103009
|
+
}
|
|
103010
|
+
}
|
|
103011
|
+
}
|
|
102825
103012
|
}
|
|
102826
103013
|
return errors2;
|
|
102827
103014
|
}
|
|
@@ -103590,131 +103777,12 @@ async function runAgentsCheck(opts) {
|
|
|
103590
103777
|
}
|
|
103591
103778
|
}
|
|
103592
103779
|
}
|
|
103593
|
-
errors2.push(...await runAgentSecretAvailabilityCheck({ cwd }));
|
|
103594
|
-
errors2.push(...await runAgentIntegrationAvailabilityCheck({ cwd }));
|
|
103595
103780
|
return {
|
|
103596
103781
|
errors: errors2,
|
|
103597
103782
|
stats: { agentsChecked: report.bundles.length }
|
|
103598
103783
|
};
|
|
103599
103784
|
}
|
|
103600
103785
|
__name(runAgentsCheck, "runAgentsCheck");
|
|
103601
|
-
async function runAgentIntegrationAvailabilityCheck(opts) {
|
|
103602
|
-
const projectRoot = resolveProjectCwd(opts.cwd);
|
|
103603
|
-
let projectId;
|
|
103604
|
-
try {
|
|
103605
|
-
projectId = loadProjectSettings(projectRoot).projectId;
|
|
103606
|
-
} catch {
|
|
103607
|
-
return [];
|
|
103608
|
-
}
|
|
103609
|
-
const bundles = await listBundles(path29.join(projectRoot, "agents"));
|
|
103610
|
-
const requiredByAgent = bundles.map((bundle) => {
|
|
103611
|
-
const policy = agentIntegrationPolicy(bundle.config);
|
|
103612
|
-
return {
|
|
103613
|
-
name: bundle.name,
|
|
103614
|
-
integrations: Object.entries(policy).map(([integrationKey, entry]) => ({
|
|
103615
|
-
integrationKey,
|
|
103616
|
-
enabled: entry.enabled,
|
|
103617
|
-
tools: enabledAgentIntegrationTools(entry)
|
|
103618
|
-
})).filter((entry) => entry.enabled)
|
|
103619
|
-
};
|
|
103620
|
-
}).filter((entry) => entry.integrations.length > 0);
|
|
103621
|
-
if (requiredByAgent.length === 0) {
|
|
103622
|
-
return [];
|
|
103623
|
-
}
|
|
103624
|
-
const api = new FimoApiClient(loadConfig());
|
|
103625
|
-
const statuses = await api.listProjectIntegrations(projectId);
|
|
103626
|
-
const statusByKey = new Map(statuses.map((status) => [status.integrationKey, status]));
|
|
103627
|
-
const infoByKey = /* @__PURE__ */ new Map();
|
|
103628
|
-
const errors2 = [];
|
|
103629
|
-
for (const agent of requiredByAgent) {
|
|
103630
|
-
for (const integration of agent.integrations) {
|
|
103631
|
-
const status = statusByKey.get(integration.integrationKey);
|
|
103632
|
-
if (!status) {
|
|
103633
|
-
errors2.push(
|
|
103634
|
-
authorError({
|
|
103635
|
-
rule: "agents/integrations",
|
|
103636
|
-
message: `${agent.name} allows unsupported integration ${integration.integrationKey}.`,
|
|
103637
|
-
file: `agents/${agent.name}/config.yaml`
|
|
103638
|
-
})
|
|
103639
|
-
);
|
|
103640
|
-
continue;
|
|
103641
|
-
}
|
|
103642
|
-
if (integration.tools.length === 0) {
|
|
103643
|
-
errors2.push(
|
|
103644
|
-
authorError({
|
|
103645
|
-
rule: "agents/integrations",
|
|
103646
|
-
message: `${agent.name} enables ${integration.integrationKey}, but no tools are allowed. Set at least one integrations.${integration.integrationKey}.tools.<tool>: true entry.`,
|
|
103647
|
-
file: `agents/${agent.name}/config.yaml`
|
|
103648
|
-
})
|
|
103649
|
-
);
|
|
103650
|
-
}
|
|
103651
|
-
if (!status.enabled) {
|
|
103652
|
-
errors2.push(
|
|
103653
|
-
authorError({
|
|
103654
|
-
rule: "agents/integrations",
|
|
103655
|
-
message: `${agent.name} allows ${integration.integrationKey}, but the project integration is not enabled. Run: fimo integrations enable ${integration.integrationKey}`,
|
|
103656
|
-
file: `agents/${agent.name}/config.yaml`
|
|
103657
|
-
})
|
|
103658
|
-
);
|
|
103659
|
-
}
|
|
103660
|
-
const info = infoByKey.get(integration.integrationKey) ?? await api.getProjectIntegrationInfo(projectId, integration.integrationKey);
|
|
103661
|
-
infoByKey.set(integration.integrationKey, info);
|
|
103662
|
-
const knownTools = new Set(info.tools.map((tool) => tool.key));
|
|
103663
|
-
for (const tool of integration.tools) {
|
|
103664
|
-
if (!knownTools.has(tool)) {
|
|
103665
|
-
errors2.push(
|
|
103666
|
-
authorError({
|
|
103667
|
-
rule: "agents/integrations",
|
|
103668
|
-
message: `${agent.name} allows unknown ${integration.integrationKey}.${tool} tool. Supported tools: ${[
|
|
103669
|
-
...knownTools
|
|
103670
|
-
].sort().join(", ")}`,
|
|
103671
|
-
file: `agents/${agent.name}/config.yaml`
|
|
103672
|
-
})
|
|
103673
|
-
);
|
|
103674
|
-
}
|
|
103675
|
-
}
|
|
103676
|
-
}
|
|
103677
|
-
}
|
|
103678
|
-
return errors2;
|
|
103679
|
-
}
|
|
103680
|
-
__name(runAgentIntegrationAvailabilityCheck, "runAgentIntegrationAvailabilityCheck");
|
|
103681
|
-
async function runAgentSecretAvailabilityCheck(opts) {
|
|
103682
|
-
const projectRoot = resolveProjectCwd(opts.cwd);
|
|
103683
|
-
let projectId;
|
|
103684
|
-
try {
|
|
103685
|
-
projectId = loadProjectSettings(projectRoot).projectId;
|
|
103686
|
-
} catch {
|
|
103687
|
-
return [];
|
|
103688
|
-
}
|
|
103689
|
-
const bundles = await listBundles(path29.join(projectRoot, "agents"));
|
|
103690
|
-
const requiredByAgent = bundles.map((bundle) => ({
|
|
103691
|
-
name: bundle.name,
|
|
103692
|
-
keys: (bundle.config.secrets ?? []).filter((secret) => secret.required !== false).map((secret) => secret.key)
|
|
103693
|
-
})).filter((entry) => entry.keys.length > 0);
|
|
103694
|
-
if (requiredByAgent.length === 0) {
|
|
103695
|
-
return [];
|
|
103696
|
-
}
|
|
103697
|
-
const api = new FimoApiClient(loadConfig());
|
|
103698
|
-
const defined = new Set(
|
|
103699
|
-
(await api.listProjectSecretEntries(projectId, { kind: "secret" })).map((secret) => secret.key)
|
|
103700
|
-
);
|
|
103701
|
-
const errors2 = [];
|
|
103702
|
-
for (const entry of requiredByAgent) {
|
|
103703
|
-
for (const key of entry.keys) {
|
|
103704
|
-
if (!defined.has(key)) {
|
|
103705
|
-
errors2.push(
|
|
103706
|
-
authorError({
|
|
103707
|
-
rule: "agents/secrets",
|
|
103708
|
-
message: `${entry.name} requires project secret ${key}, but it is not set. Run: fimo secrets set ${key} --stdin`,
|
|
103709
|
-
file: `agents/${entry.name}/config.yaml`
|
|
103710
|
-
})
|
|
103711
|
-
);
|
|
103712
|
-
}
|
|
103713
|
-
}
|
|
103714
|
-
}
|
|
103715
|
-
return errors2;
|
|
103716
|
-
}
|
|
103717
|
-
__name(runAgentSecretAvailabilityCheck, "runAgentSecretAvailabilityCheck");
|
|
103718
103786
|
function runRouteMetadataCheck(opts) {
|
|
103719
103787
|
return validateRouteMetadata({ cwd: opts.cwd });
|
|
103720
103788
|
}
|
|
@@ -103778,8 +103846,24 @@ async function runLabelsPhase(opts) {
|
|
|
103778
103846
|
}
|
|
103779
103847
|
const api = new FimoApiClient(loadConfig()).withEnv(ctx.env);
|
|
103780
103848
|
const locale = getConfigServer(cwd).i18n?.defaultLocale ?? "en";
|
|
103849
|
+
const pinnedBranch = (await readProjectEnv(cwd)).VITE_FIMO_BRANCH;
|
|
103781
103850
|
try {
|
|
103782
|
-
|
|
103851
|
+
let labels2;
|
|
103852
|
+
if (pinnedBranch) {
|
|
103853
|
+
const branchLabels = await loadPublicTenantLabels(cwd, locale);
|
|
103854
|
+
if (branchLabels == null) {
|
|
103855
|
+
return {
|
|
103856
|
+
name: "Labels",
|
|
103857
|
+
ok: false,
|
|
103858
|
+
errors: [
|
|
103859
|
+
infraErrorFromException("labels-check", new Error(`Could not read labels from branch "${pinnedBranch}"`))
|
|
103860
|
+
]
|
|
103861
|
+
};
|
|
103862
|
+
}
|
|
103863
|
+
labels2 = branchLabels;
|
|
103864
|
+
} else {
|
|
103865
|
+
labels2 = labelsToRecord(await api.listLabels(ctx.projectId, locale));
|
|
103866
|
+
}
|
|
103783
103867
|
const existingKeys = new Set(Object.keys(labels2));
|
|
103784
103868
|
const missing = labelKeys.filter((label) => !existingKeys.has(label.key));
|
|
103785
103869
|
return {
|
|
@@ -105989,6 +106073,9 @@ async function entriesListCommand(typeName, options, cmd) {
|
|
|
105989
106073
|
if (options.fields) {
|
|
105990
106074
|
params.fields = options.fields;
|
|
105991
106075
|
}
|
|
106076
|
+
if (options.populate) {
|
|
106077
|
+
params.populate = options.populate;
|
|
106078
|
+
}
|
|
105992
106079
|
if (options.locale) {
|
|
105993
106080
|
params.locale = options.locale;
|
|
105994
106081
|
}
|
|
@@ -107857,14 +107944,18 @@ __name(labelsSetCommand, "labelsSetCommand");
|
|
|
107857
107944
|
async function labelsSetManyCommand(options, cmd) {
|
|
107858
107945
|
const { ctx, api } = await bootstrapProjectCommand(options, cmd, { refuseReadOnly: true });
|
|
107859
107946
|
await requireOperation("i18n.update", { projectId: ctx.projectId, env: ctx.env });
|
|
107860
|
-
const
|
|
107861
|
-
const
|
|
107862
|
-
|
|
107947
|
+
const fallbackLocale = options.locale ?? defaultLocaleForProject(options.cwd);
|
|
107948
|
+
const records = readLabelRecords(options).map(({ key, value, locale }) => ({
|
|
107949
|
+
key,
|
|
107950
|
+
value,
|
|
107951
|
+
locale: locale ?? fallbackLocale
|
|
107952
|
+
}));
|
|
107953
|
+
const locales = [...new Set(records.map((record2) => record2.locale))];
|
|
107863
107954
|
const result = await api.setLabels(ctx.projectId, records);
|
|
107864
107955
|
ui.result({
|
|
107865
107956
|
command: "labels set-many",
|
|
107866
|
-
data: { ...result, locale, labels: records.length },
|
|
107867
|
-
render: /* @__PURE__ */ __name(() => `Set ${records.length} label${records.length === 1 ? "" : "s"} (${
|
|
107957
|
+
data: { ...result, locale: fallbackLocale, locales, labels: records.length },
|
|
107958
|
+
render: /* @__PURE__ */ __name(() => `Set ${records.length} label${records.length === 1 ? "" : "s"} (${locales.join(", ")})`, "render"),
|
|
107868
107959
|
warnings: dbLabelsRuntimeWarnings(options.cwd)
|
|
107869
107960
|
});
|
|
107870
107961
|
}
|
|
@@ -107882,7 +107973,7 @@ async function labelsDeleteCommand(key, options, cmd) {
|
|
|
107882
107973
|
});
|
|
107883
107974
|
}
|
|
107884
107975
|
__name(labelsDeleteCommand, "labelsDeleteCommand");
|
|
107885
|
-
function
|
|
107976
|
+
function readLabelRecords(options) {
|
|
107886
107977
|
if (options.file && options.data) {
|
|
107887
107978
|
throw new Error("Pass either --file or --data, not both.");
|
|
107888
107979
|
}
|
|
@@ -107892,36 +107983,39 @@ function readLabelMap(options) {
|
|
|
107892
107983
|
const raw = options.data ?? readLabelFile(options);
|
|
107893
107984
|
const parsed = JSON.parse(raw);
|
|
107894
107985
|
if (Array.isArray(parsed)) {
|
|
107895
|
-
return
|
|
107896
|
-
|
|
107897
|
-
|
|
107898
|
-
|
|
107899
|
-
|
|
107900
|
-
|
|
107901
|
-
|
|
107902
|
-
|
|
107903
|
-
|
|
107904
|
-
|
|
107905
|
-
|
|
107906
|
-
|
|
107986
|
+
return parsed.map((item) => {
|
|
107987
|
+
if (!item || typeof item !== "object") {
|
|
107988
|
+
throw new Error("Label arrays must contain objects with key and value fields.");
|
|
107989
|
+
}
|
|
107990
|
+
const record2 = item;
|
|
107991
|
+
if (typeof record2.key !== "string") {
|
|
107992
|
+
throw new Error("Each label record must include a string key.");
|
|
107993
|
+
}
|
|
107994
|
+
const value = record2.value ?? record2.defaultValue;
|
|
107995
|
+
if (typeof value !== "string") {
|
|
107996
|
+
throw new Error(`Label ${record2.key} must include a string value.`);
|
|
107997
|
+
}
|
|
107998
|
+
let locale;
|
|
107999
|
+
if (record2.locale !== void 0) {
|
|
108000
|
+
if (typeof record2.locale !== "string") {
|
|
108001
|
+
throw new Error(`Label ${record2.key} has a non-string locale.`);
|
|
107907
108002
|
}
|
|
107908
|
-
|
|
107909
|
-
}
|
|
107910
|
-
|
|
108003
|
+
locale = record2.locale;
|
|
108004
|
+
}
|
|
108005
|
+
return { key: record2.key, value, locale };
|
|
108006
|
+
});
|
|
107911
108007
|
}
|
|
107912
108008
|
if (!parsed || typeof parsed !== "object") {
|
|
107913
108009
|
throw new Error("Labels JSON must be an object map or an array of { key, value } records.");
|
|
107914
108010
|
}
|
|
107915
|
-
|
|
107916
|
-
for (const [key, value] of Object.entries(parsed)) {
|
|
108011
|
+
return Object.entries(parsed).map(([key, value]) => {
|
|
107917
108012
|
if (typeof value !== "string") {
|
|
107918
108013
|
throw new Error(`Label ${key} must be a string.`);
|
|
107919
108014
|
}
|
|
107920
|
-
|
|
107921
|
-
}
|
|
107922
|
-
return labels2;
|
|
108015
|
+
return { key, value };
|
|
108016
|
+
});
|
|
107923
108017
|
}
|
|
107924
|
-
__name(
|
|
108018
|
+
__name(readLabelRecords, "readLabelRecords");
|
|
107925
108019
|
function readLabelFile(options) {
|
|
107926
108020
|
const projectRoot = resolveProjectCwd(options.cwd);
|
|
107927
108021
|
const filePath = path38.resolve(projectRoot, options.file);
|
|
@@ -109974,7 +110068,7 @@ schemas2.command("get").description("Get a schema by type name").argument("<type
|
|
|
109974
110068
|
schemas2.command("push").description("Push schema JSON files to the API (create or update)").argument("[typeName]", "Schema name to push (omit to push all)").option("-C, --cwd <dir>", "Project directory").action(schemaPushCommand);
|
|
109975
110069
|
schemas2.command("delete").description("Delete a schema by type name").argument("<typeName>", "Content type name").option("-C, --cwd <dir>", "Project directory").action(schemaDeleteCommand);
|
|
109976
110070
|
var entries = program.command("entries").description("Manage content entries");
|
|
109977
|
-
entries.command("list").description("List entries of a content type").argument("<typeName>", "Content type name").option("-C, --cwd <dir>", "Project directory").option("--limit <n>", "Max entries to return").option("--offset <n>", "Offset for pagination").option("--search <query>", "Search query").option("--where <json>", `JSON filter object, e.g. '{"featured":true}'`).option("--sort <field>", "Sort field, prefix with - for descending").option("--fields <fields>", "Comma-separated fields to return, e.g. slug,title,excerpt").option("--locale <locale>", "Locale to list (default en)").option("--sort-by <field>", "Sort field").option("--sort-direction <dir>", "Sort direction (asc|desc)").action(entriesListCommand);
|
|
110071
|
+
entries.command("list").description("List entries of a content type").argument("<typeName>", "Content type name").option("-C, --cwd <dir>", "Project directory").option("--limit <n>", "Max entries to return").option("--offset <n>", "Offset for pagination").option("--search <query>", "Search query").option("--where <json>", `JSON filter object, e.g. '{"featured":true}'`).option("--sort <field>", "Sort field, prefix with - for descending").option("--fields <fields>", "Comma-separated fields to return, e.g. slug,title,excerpt").option("--populate <fields>", "Comma-separated relation fields to populate, e.g. author,contributors").option("--locale <locale>", "Locale to list (default en)").option("--sort-by <field>", "Sort field").option("--sort-direction <dir>", "Sort direction (asc|desc)").action(entriesListCommand);
|
|
109978
110072
|
entries.command("get").description("Get a single entry").argument("<typeName>", "Content type name").argument("<id>", "Entry ID").option("-C, --cwd <dir>", "Project directory").option("--locale <locale>", "Locale to read (default en)").action(entriesGetCommand);
|
|
109979
110073
|
entries.command("create").description("Create a new entry").argument("<typeName>", "Content type name").option("-C, --cwd <dir>", "Project directory").requiredOption("--body <json>", "Entry JSON body (slug + data)").option("--locale <locale>", "Entry locale (default en)").option("--document-id <id>", "Document ID shared by localized variants").action(entriesCreateCommand);
|
|
109980
110074
|
entries.command("update").description("Update an existing entry").argument("<typeName>", "Content type name").argument("<id>", "Entry ID").option("-C, --cwd <dir>", "Project directory").requiredOption("--body <json>", "Updated entry JSON body").option("--locale <locale>", "Entry locale").option("--document-id <id>", "Document ID shared by localized variants").action(entriesUpdateCommand);
|
|
@@ -110051,7 +110145,7 @@ rules.command("sync").description(
|
|
|
110051
110145
|
var labels = program.command("labels").description("Manage site labels stored in Fimo");
|
|
110052
110146
|
labels.command("list").description("List labels stored in the DB").option("-C, --cwd <dir>", "Project directory").option("--locale <locale>", "Filter by locale (e.g. en)").action(labelsListCommand);
|
|
110053
110147
|
labels.command("set <key> [value]").description("Set one label value").option("-C, --cwd <dir>", "Project directory").option("--locale <locale>", "Target locale (default en)").option("--value <value>", "Label value").option("--stdin", "Read the label value from stdin").action(labelsSetCommand);
|
|
110054
|
-
labels.command("set-many").description("Set many label values from JSON").option("-C, --cwd <dir>", "Project directory").option("--locale <locale>", "Target locale (default en)").option("--file <path>", "JSON object map or array of { key, value } records").option("--data <json>", "Inline JSON object map or array of { key, value } records").action(labelsSetManyCommand);
|
|
110148
|
+
labels.command("set-many").description("Set many label values from JSON").option("-C, --cwd <dir>", "Project directory").option("--locale <locale>", "Target locale (default en)").option("--file <path>", "JSON object map or array of { key, value, locale? } records").option("--data <json>", "Inline JSON object map or array of { key, value, locale? } records").action(labelsSetManyCommand);
|
|
110055
110149
|
labels.command("delete <key>").alias("unset").description("Delete one label value").option("-C, --cwd <dir>", "Project directory").option("--locale <locale>", "Target locale (default en)").action(labelsDeleteCommand);
|
|
110056
110150
|
var analytics = program.command("analytics").description("Read site analytics (pageviews, top breakdowns, annotations)");
|
|
110057
110151
|
var rangeOptions = /* @__PURE__ */ __name((cmd) => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"query.d.ts","sourceRoot":"","sources":["../../../../src/runtime/primitives/lib/query.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"query.d.ts","sourceRoot":"","sources":["../../../../src/runtime/primitives/lib/query.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,QAAQ,EAAE,SAAS,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAElG,MAAM,MAAM,SAAS,CAAC,CAAC,IAAI;IACzB,GAAG,CAAC,EAAE,CAAC,CAAC;IACR,GAAG,CAAC,EAAE,CAAC,CAAC;IACR,GAAG,CAAC,EAAE,CAAC,EAAE,CAAC;IACV,IAAI,CAAC,EAAE,CAAC,EAAE,CAAC;IACX,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,SAAS,GAAG,SAAS,CAAC,MAAM,CAAC,GAAG;IAC1C,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,MAAM,MAAM,SAAS,GAAG,SAAS,CAAC,MAAM,CAAC,GAAG;IAC1C,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF,MAAM,MAAM,OAAO,GAAG,SAAS,CAAC;AAEhC,MAAM,MAAM,OAAO,GAAG,SAAS,CAAC,OAAO,CAAC,CAAC;AAEzC,MAAM,MAAM,WAAW,GAAG;IACxB,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,SAAS,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,UAAU,GAAG,MAAM,CAAC,GACxD,MAAM,GAAG,SAAS,GAClB,CAAC,CAAC,CAAC,SAAS,CAAC,WAAW,GAAG,OAAO,CAAC,GACjC,OAAO,GAAG,OAAO,GACjB,CAAC,CAAC,CAAC,SAAS,CAAC,QAAQ,GAAG,IAAI,CAAC,GAC3B,MAAM,GAAG,OAAO,GAChB,CAAC,CAAC,CAAC,SAAS,CAAC,MAAM,CAAC,GAClB,MAAM,GAAG,SAAS,GAClB,CAAC,CAAC,CAAC,SAAS,CAAC,SAAS,GAAG,SAAS,EAAE,CAAC,GACnC,WAAW,GACX,CAAC,CAAC,CAAC,SAAS,CAAC,YAAY,CAAC,GACxB,MAAM,GAAG,SAAS,GAClB,WAAW,CAAC;AAE1B,KAAK,aAAa,GAAG;IACnB,EAAE,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACxB,UAAU,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAChC,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1B,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5B,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;IAC7B,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;IAC7B,MAAM,CAAC,EAAE;QACP,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;KAClC,CAAC;CACH,CAAC;AAEF,MAAM,MAAM,KAAK,CAAC,CAAC,IAAI,aAAa,GAAG;KACpC,CAAC,IAAI,OAAO,CAAC,MAAM,CAAC,EAAE,MAAM,aAAa,CAAC,CAAC,CAAC,EAAE,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;CAC/D,CAAC;AAEF,KAAK,iBAAiB,GAAG,aAAa,CAAC;AACvC,KAAK,SAAS,GAAG,QAAQ,GAAG,UAAU,iBAAiB,EAAE,CAAC;AAE1D,MAAM,MAAM,SAAS,CAAC,CAAC,IACnB,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,EAAE,QAAQ,CAAC,GAC3C,WAAW,GACX,WAAW,GACX,OAAO,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC;AAEjC,MAAM,MAAM,IAAI,CAAC,CAAC,IAAI,SAAS,CAAC,CAAC,CAAC,GAAG,IAAI,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC;AAExD,MAAM,MAAM,KAAK,CAAC,CAAC,IAAI,OAAO,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,GAAG,WAAW,GAAG,WAAW,GAAG,SAAS,CAAC;AAExF,MAAM,MAAM,MAAM,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,CAAC,GAAG,SAAS,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC;AAEvD,KAAK,aAAa,CAAC,MAAM,IAAI,MAAM,SAAS,UAAU,MAAM,EAAE,GAAG,QAAQ,GAAG,MAAM,CAAC;AAEnF,KAAK,cAAc,CAAC,OAAO,IAAI,OAAO,SAAS,SAAS,CAAC,MAAM,CAAC,CAAC,EAAE,GAAG,aAAa,CAAC,CAAC,CAAC,GAAG,aAAa,CAAC,OAAO,CAAC,CAAC;AAEhH,MAAM,MAAM,SAAS,CAAC,CAAC,SAAS;IAAE,EAAE,EAAE,MAAM,CAAA;CAAE,EAAE,OAAO,SAAS,MAAM,CAAC,CAAC,CAAC,GAAG,SAAS,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,SAAS,CAAC,GAClH,CAAC,GACD,IAAI,CAAC,CAAC,EAAE,OAAO,CAAC,IAAI,GAAG,cAAc,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC;AAE9D,MAAM,MAAM,KAAK,CAAC,CAAC,SAAS;IAAE,EAAE,EAAE,MAAM,CAAA;CAAE,EAAE,OAAO,SAAS,MAAM,CAAC,CAAC,CAAC,GAAG,SAAS,GAAG,MAAM,CAAC,CAAC,CAAC,GAAG,SAAS,IAAI;IAC3G,KAAK,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC;IAC3B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC;CACtB,CAAC"}
|
|
@@ -6,9 +6,11 @@ describe('fimo/ui public type surface', () => {
|
|
|
6
6
|
const query = {
|
|
7
7
|
where: { name: 'BASIC' },
|
|
8
8
|
sort: '-price',
|
|
9
|
+
populate: 'name',
|
|
9
10
|
limit: 10,
|
|
10
11
|
};
|
|
11
12
|
expectTypeOf(query.fields).toEqualTypeOf();
|
|
13
|
+
expectTypeOf(query.populate).toEqualTypeOf();
|
|
12
14
|
expectTypeOf().toEqualTypeOf();
|
|
13
15
|
});
|
|
14
16
|
});
|
|
@@ -10,4 +10,4 @@ export interface ValidateSchemasResult {
|
|
|
10
10
|
export declare function validateSchemas(options?: {
|
|
11
11
|
cwd?: string;
|
|
12
12
|
}): ValidateSchemasResult;
|
|
13
|
-
export declare function validateSchemaFile(absolutePath: string, file: string): SchemaFileError[];
|
|
13
|
+
export declare function validateSchemaFile(absolutePath: string, file: string, schemaUids?: Set<string>): SchemaFileError[];
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
2
2
|
import { basename, join, resolve } from 'node:path';
|
|
3
|
-
import { FieldTypeSchema, RESERVED_CONTENT_FIELD_NAMES } from '@fimo/api-contracts';
|
|
3
|
+
import { CONTENT_FIELD_NAME_PATTERN, CONTENT_FIELD_NAME_PATTERN_DESCRIPTION, CONTENT_TYPE_NAME_PATTERN, CONTENT_TYPE_NAME_PATTERN_DESCRIPTION, FieldTypeSchema, RESERVED_CONTENT_FIELD_NAMES, } from '@fimo/api-contracts';
|
|
4
4
|
/**
|
|
5
5
|
* Validate `src/schemas/*.json` locally against the same rules the API's
|
|
6
6
|
* schema validator enforces (field types, reserved field names, required
|
|
@@ -20,13 +20,32 @@ export function validateSchemas(options = {}) {
|
|
|
20
20
|
.filter((f) => f.endsWith('.json'))
|
|
21
21
|
.sort();
|
|
22
22
|
const errors = [];
|
|
23
|
+
const schemaUids = new Set();
|
|
24
|
+
for (const fileName of files) {
|
|
25
|
+
const schema = readSchemaObject(join(schemasDir, fileName));
|
|
26
|
+
if (schema && typeof schema.uid === 'string') {
|
|
27
|
+
schemaUids.add(schema.uid);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
23
30
|
for (const fileName of files) {
|
|
24
31
|
const file = join(SCHEMAS_REL, fileName);
|
|
25
|
-
errors.push(...validateSchemaFile(join(schemasDir, fileName), file));
|
|
32
|
+
errors.push(...validateSchemaFile(join(schemasDir, fileName), file, schemaUids));
|
|
26
33
|
}
|
|
27
34
|
return { errors, schemasChecked: files.length };
|
|
28
35
|
}
|
|
29
|
-
|
|
36
|
+
function readSchemaObject(absolutePath) {
|
|
37
|
+
let schema;
|
|
38
|
+
try {
|
|
39
|
+
schema = JSON.parse(readFileSync(absolutePath, 'utf-8'));
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
return typeof schema === 'object' && schema !== null && !Array.isArray(schema)
|
|
45
|
+
? schema
|
|
46
|
+
: null;
|
|
47
|
+
}
|
|
48
|
+
export function validateSchemaFile(absolutePath, file, schemaUids = new Set()) {
|
|
30
49
|
let schema;
|
|
31
50
|
try {
|
|
32
51
|
schema = JSON.parse(readFileSync(absolutePath, 'utf-8'));
|
|
@@ -60,6 +79,13 @@ export function validateSchemaFile(absolutePath, file) {
|
|
|
60
79
|
file,
|
|
61
80
|
});
|
|
62
81
|
}
|
|
82
|
+
else if (!CONTENT_TYPE_NAME_PATTERN.test(uid)) {
|
|
83
|
+
errors.push({
|
|
84
|
+
rule: 'schemas/type-name',
|
|
85
|
+
message: `"uid" must use ${CONTENT_TYPE_NAME_PATTERN_DESCRIPTION}.`,
|
|
86
|
+
file,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
63
89
|
if (typeof name !== 'string' || name.length === 0) {
|
|
64
90
|
errors.push({
|
|
65
91
|
rule: 'schemas/name',
|
|
@@ -76,6 +102,14 @@ export function validateSchemaFile(absolutePath, file) {
|
|
|
76
102
|
return errors;
|
|
77
103
|
}
|
|
78
104
|
for (const [fieldName, definition] of Object.entries(fields)) {
|
|
105
|
+
if (!CONTENT_FIELD_NAME_PATTERN.test(fieldName)) {
|
|
106
|
+
errors.push({
|
|
107
|
+
rule: 'schemas/field-name',
|
|
108
|
+
message: `Field "${fieldName}" must use ${CONTENT_FIELD_NAME_PATTERN_DESCRIPTION}.`,
|
|
109
|
+
file,
|
|
110
|
+
});
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
79
113
|
if (RESERVED_FIELD_NAMES.has(fieldName)) {
|
|
80
114
|
errors.push({
|
|
81
115
|
rule: 'schemas/reserved-field',
|
|
@@ -108,6 +142,41 @@ export function validateSchemaFile(absolutePath, file) {
|
|
|
108
142
|
file,
|
|
109
143
|
});
|
|
110
144
|
}
|
|
145
|
+
if (type === 'relation') {
|
|
146
|
+
const { target, many } = definition;
|
|
147
|
+
if (typeof target !== 'string' || !CONTENT_TYPE_NAME_PATTERN.test(target)) {
|
|
148
|
+
errors.push({
|
|
149
|
+
rule: 'schemas/relation-target',
|
|
150
|
+
message: `Relation field "${fieldName}" must declare a valid "target" content type.`,
|
|
151
|
+
file,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
else if (schemaUids.size > 0 && !schemaUids.has(target)) {
|
|
155
|
+
errors.push({
|
|
156
|
+
rule: 'schemas/relation-target',
|
|
157
|
+
message: `Relation field "${fieldName}" targets unknown schema "${target}".`,
|
|
158
|
+
file,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
if (many !== undefined && typeof many !== 'boolean') {
|
|
162
|
+
errors.push({
|
|
163
|
+
rule: 'schemas/relation-many',
|
|
164
|
+
message: `Relation field "${fieldName}" must use a boolean "many" value.`,
|
|
165
|
+
file,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
for (const relationOnlyKey of ['target', 'many', 'localized']) {
|
|
171
|
+
if (relationOnlyKey in definition) {
|
|
172
|
+
errors.push({
|
|
173
|
+
rule: 'schemas/relation-shape',
|
|
174
|
+
message: `Field "${fieldName}": "${relationOnlyKey}" is only supported on relation fields.`,
|
|
175
|
+
file,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
111
180
|
}
|
|
112
181
|
return errors;
|
|
113
182
|
}
|
|
@@ -21,6 +21,13 @@ describe('validateSchemas', () => {
|
|
|
21
21
|
}
|
|
22
22
|
it('accepts a valid schema', () => {
|
|
23
23
|
const cwd = writeProject({
|
|
24
|
+
'Author.json': {
|
|
25
|
+
uid: 'Author',
|
|
26
|
+
name: 'Authors',
|
|
27
|
+
fields: {
|
|
28
|
+
name: { type: 'string' },
|
|
29
|
+
},
|
|
30
|
+
},
|
|
24
31
|
'BlogPost.json': {
|
|
25
32
|
uid: 'BlogPost',
|
|
26
33
|
name: 'Blog Posts',
|
|
@@ -29,6 +36,20 @@ describe('validateSchemas', () => {
|
|
|
29
36
|
body: { type: 'richtext' },
|
|
30
37
|
coverImage: { type: 'media' },
|
|
31
38
|
featured: { type: 'boolean' },
|
|
39
|
+
author: { type: 'relation', target: 'Author' },
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
expect(validateSchemas({ cwd })).toEqual({ errors: [], schemasChecked: 2 });
|
|
44
|
+
});
|
|
45
|
+
it('accepts self-referential relation fields', () => {
|
|
46
|
+
const cwd = writeProject({
|
|
47
|
+
'Item.json': {
|
|
48
|
+
uid: 'Item',
|
|
49
|
+
name: 'Items',
|
|
50
|
+
fields: {
|
|
51
|
+
title: { type: 'string' },
|
|
52
|
+
parent: { type: 'relation', target: 'Item' },
|
|
32
53
|
},
|
|
33
54
|
},
|
|
34
55
|
});
|
|
@@ -84,6 +105,67 @@ describe('validateSchemas', () => {
|
|
|
84
105
|
expect(errors[0].message).toContain('"decimal"');
|
|
85
106
|
expect(errors[0].message).toContain('string, richtext, media');
|
|
86
107
|
});
|
|
108
|
+
it('rejects relation fields without a valid target', () => {
|
|
109
|
+
const cwd = writeProject({
|
|
110
|
+
'Post.json': {
|
|
111
|
+
uid: 'Post',
|
|
112
|
+
name: 'Posts',
|
|
113
|
+
fields: { author: { type: 'relation' } },
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
const { errors } = validateSchemas({ cwd });
|
|
117
|
+
expect(errors).toHaveLength(1);
|
|
118
|
+
expect(errors[0]).toMatchObject({ rule: 'schemas/relation-target' });
|
|
119
|
+
});
|
|
120
|
+
it('rejects relation fields targeting a missing local schema', () => {
|
|
121
|
+
const cwd = writeProject({
|
|
122
|
+
'Post.json': {
|
|
123
|
+
uid: 'Post',
|
|
124
|
+
name: 'Posts',
|
|
125
|
+
fields: { author: { type: 'relation', target: 'Author' } },
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
const { errors } = validateSchemas({ cwd });
|
|
129
|
+
expect(errors).toHaveLength(1);
|
|
130
|
+
expect(errors[0]).toMatchObject({ rule: 'schemas/relation-target' });
|
|
131
|
+
expect(errors[0].message).toContain('unknown schema "Author"');
|
|
132
|
+
});
|
|
133
|
+
it('rejects relation-only properties on scalar fields', () => {
|
|
134
|
+
const cwd = writeProject({
|
|
135
|
+
'Post.json': {
|
|
136
|
+
uid: 'Post',
|
|
137
|
+
name: 'Posts',
|
|
138
|
+
fields: { author: { type: 'string', target: 'Author' } },
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
const { errors } = validateSchemas({ cwd });
|
|
142
|
+
expect(errors).toHaveLength(1);
|
|
143
|
+
expect(errors[0]).toMatchObject({ rule: 'schemas/relation-shape' });
|
|
144
|
+
});
|
|
145
|
+
it('rejects schema uid names that cannot be used as content type identifiers', () => {
|
|
146
|
+
const cwd = writeProject({
|
|
147
|
+
'blog-post.json': {
|
|
148
|
+
uid: 'blog-post',
|
|
149
|
+
name: 'Blog Posts',
|
|
150
|
+
fields: { title: { type: 'string' } },
|
|
151
|
+
},
|
|
152
|
+
});
|
|
153
|
+
const { errors } = validateSchemas({ cwd });
|
|
154
|
+
expect(errors).toHaveLength(1);
|
|
155
|
+
expect(errors[0]).toMatchObject({ rule: 'schemas/type-name' });
|
|
156
|
+
});
|
|
157
|
+
it('rejects field names that cannot be used in generated source paths', () => {
|
|
158
|
+
const cwd = writeProject({
|
|
159
|
+
'Page.json': {
|
|
160
|
+
uid: 'Page',
|
|
161
|
+
name: 'Pages',
|
|
162
|
+
fields: { 'hero-title': { type: 'string' } },
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
const { errors } = validateSchemas({ cwd });
|
|
166
|
+
expect(errors).toHaveLength(1);
|
|
167
|
+
expect(errors[0]).toMatchObject({ rule: 'schemas/field-name' });
|
|
168
|
+
});
|
|
87
169
|
it('rejects field-level i18n settings', () => {
|
|
88
170
|
const cwd = writeProject({
|
|
89
171
|
'Page.json': {
|
package/package.json
CHANGED
package/release.json
CHANGED
|
@@ -1,16 +1,15 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { index, layout, route } from '@react-router/dev/routes';
|
|
1
|
+
import { index } from '@react-router/dev/routes';
|
|
3
2
|
import { fimoRoutes } from 'fimo/react-router/routes';
|
|
4
3
|
|
|
5
4
|
// Examples of React Router route patterns:
|
|
6
5
|
// index('./pages/Index.tsx'),
|
|
6
|
+
// import { route } from '@react-router/dev/routes';
|
|
7
7
|
// route('about', './pages/About.tsx'),
|
|
8
8
|
// route('blog/:slug', './pages/BlogPost.tsx'),
|
|
9
|
+
// import { layout } from '@react-router/dev/routes';
|
|
9
10
|
// layout('./pages/layouts/Dashboard.tsx', [
|
|
10
11
|
// index('./pages/dashboard/Overview.tsx'),
|
|
11
12
|
// route('settings', './pages/dashboard/Settings.tsx'),
|
|
12
13
|
// ]),
|
|
13
14
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
export default fimoRoutes(routes) satisfies RouteConfig;
|
|
15
|
+
export default fimoRoutes([index('./pages/Index.tsx', { id: 'home' })]);
|