fimo 0.3.6-staging.1 → 0.4.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/hooks-template.eta +8 -2
- package/assets/skills/fimo/references/content.md +66 -12
- package/assets/skills/fimo-cli/references/content.md +21 -1
- package/dist/cli/bundle.json +1 -1
- package/dist/cli/index.js +252 -155
- 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/templates/react-router/package.json +1 -1
- package/templates/react-router/src/routes.ts +4 -5
|
@@ -64,6 +64,8 @@ function getTypeForField(field) {
|
|
|
64
64
|
return 'Record<string, any>';
|
|
65
65
|
case 'media':
|
|
66
66
|
return field.multiple ? 'FimoMedia[]' : 'FimoMedia';
|
|
67
|
+
case 'relation':
|
|
68
|
+
return field.many ? 'Array<string | Record<string, any>>' : 'string | Record<string, any> | null';
|
|
67
69
|
default:
|
|
68
70
|
return 'any';
|
|
69
71
|
}
|
|
@@ -216,7 +218,7 @@ export async function getByField(fieldName: string, value: string, options: { lo
|
|
|
216
218
|
}
|
|
217
219
|
|
|
218
220
|
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;
|
|
221
|
+
const { where, sort, fields, populate, ...flat } = params;
|
|
220
222
|
const query = new URLSearchParams();
|
|
221
223
|
|
|
222
224
|
for (const [key, value] of Object.entries(flat)) {
|
|
@@ -229,7 +231,11 @@ function toQueryString<TFields extends <%= model.pascal %>Fields | undefined = u
|
|
|
229
231
|
}
|
|
230
232
|
|
|
231
233
|
if (fields != null) {
|
|
232
|
-
query.set("fields",
|
|
234
|
+
query.set("fields", typeof fields === "string" ? fields : fields.join(","));
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (populate != null) {
|
|
238
|
+
query.set("populate", typeof populate === "string" ? populate : populate.join(","));
|
|
233
239
|
}
|
|
234
240
|
|
|
235
241
|
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)
|
|
@@ -183,7 +223,7 @@ if (isLoading || !posts) return null;
|
|
|
183
223
|
posts.map((post) => /* ... */); // BlogPost[]
|
|
184
224
|
```
|
|
185
225
|
|
|
186
|
-
Query params supported: `limit` (1-100, default 20), `offset` (default 0), `search`, `sort`, `where`, and `
|
|
226
|
+
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
227
|
|
|
188
228
|
`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
229
|
|
|
@@ -215,21 +255,35 @@ const { data: summaries = [] } = BlogPost.useGet({
|
|
|
215
255
|
// summaries: Array<Pick<BlogPost, 'id' | 'slug' | 'title' | 'excerpt' | 'coverImage'>>
|
|
216
256
|
```
|
|
217
257
|
|
|
258
|
+
Use `populate` to embed relation targets in read results:
|
|
259
|
+
|
|
260
|
+
```tsx
|
|
261
|
+
const { data: posts = [] } = BlogPost.useGet({
|
|
262
|
+
fields: ['slug', 'title', 'author'] as const,
|
|
263
|
+
populate: ['author'] as const,
|
|
264
|
+
});
|
|
265
|
+
// Without populate: post.author is a target documentId string.
|
|
266
|
+
// With populate: post.author is the target Author entry object, or null.
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
Populate is depth-one. It only works for relation fields; trying to populate a scalar field returns 400. To-many relation order is preserved.
|
|
270
|
+
|
|
218
271
|
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
272
|
|
|
220
273
|
## Fimo primitives (what the field values are)
|
|
221
274
|
|
|
222
275
|
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
276
|
|
|
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)
|
|
277
|
+
| Schema field type | Field value you get | Notes |
|
|
278
|
+
| ----------------- | --------------------------- | -------------------------------------------------------------------------------------------- |
|
|
279
|
+
| `string` | `FimoString` | `extends String` — **not** a primitive `string` |
|
|
280
|
+
| `richtext` | `FimoRichText` | Tiptap JSONContent + source metadata |
|
|
281
|
+
| `boolean` | `FimoBoolean` | class; `.valueOf()` → `boolean` |
|
|
282
|
+
| `media` | `FimoMedia` / `FimoMedia[]` | shape + rendering in `references/assets.md` |
|
|
283
|
+
| `number` | `number` | plain — not wrapped |
|
|
284
|
+
| `json` | `Record<string, any>` | plain — not wrapped |
|
|
285
|
+
| `date` | `any` (raw ISO date string) | not wrapped (`FimoDate` is not emitted — see below) |
|
|
286
|
+
| `relation` | `string` / `string[]` | target `documentId` value(s); use `populate` on raw API when embedded target rows are needed |
|
|
233
287
|
|
|
234
288
|
Rules of thumb:
|
|
235
289
|
|
|
@@ -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
|
|
|
@@ -45831,6 +45836,16 @@ function planRemoteConfig(remoteUrl) {
|
|
|
45831
45836
|
const credentialKey = `credential.${u2.protocol}//${u2.host}`;
|
|
45832
45837
|
return { cleanUrl, credentialKey };
|
|
45833
45838
|
}
|
|
45839
|
+
function shellQuote(value) {
|
|
45840
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
45841
|
+
}
|
|
45842
|
+
function credentialHelperCommand(argv = process.argv) {
|
|
45843
|
+
const entry = argv[1];
|
|
45844
|
+
if (!entry) {
|
|
45845
|
+
return "!fimo git-credential";
|
|
45846
|
+
}
|
|
45847
|
+
return `!${shellQuote(process.execPath)} ${shellQuote(entry)} git-credential`;
|
|
45848
|
+
}
|
|
45834
45849
|
async function ensureFimoRemote(cwd, remoteUrl) {
|
|
45835
45850
|
const { cleanUrl, credentialKey } = planRemoteConfig(remoteUrl);
|
|
45836
45851
|
await runGit2(["remote", "set-url", "fimo", cleanUrl], cwd).catch(async () => {
|
|
@@ -45838,7 +45853,7 @@ async function ensureFimoRemote(cwd, remoteUrl) {
|
|
|
45838
45853
|
});
|
|
45839
45854
|
await runGit2(["config", "--unset-all", `${credentialKey}.helper`], cwd).catch(() => void 0);
|
|
45840
45855
|
await runGit2(["config", "--add", `${credentialKey}.helper`, ""], cwd);
|
|
45841
|
-
await runGit2(["config", "--add", `${credentialKey}.helper`,
|
|
45856
|
+
await runGit2(["config", "--add", `${credentialKey}.helper`, credentialHelperCommand()], cwd);
|
|
45842
45857
|
await runGit2(["config", `${credentialKey}.useHttpPath`, "true"], cwd);
|
|
45843
45858
|
await runGit2(["config", `${credentialKey}.username`, "t"], cwd);
|
|
45844
45859
|
}
|
|
@@ -45846,6 +45861,8 @@ var init_git_credential_setup = __esm({
|
|
|
45846
45861
|
"src/cli/utils/git-credential-setup.ts"() {
|
|
45847
45862
|
init_git();
|
|
45848
45863
|
__name(planRemoteConfig, "planRemoteConfig");
|
|
45864
|
+
__name(shellQuote, "shellQuote");
|
|
45865
|
+
__name(credentialHelperCommand, "credentialHelperCommand");
|
|
45849
45866
|
__name(ensureFimoRemote, "ensureFimoRemote");
|
|
45850
45867
|
}
|
|
45851
45868
|
});
|
|
@@ -101079,6 +101096,10 @@ __name(listTemplatesCommand, "listTemplatesCommand");
|
|
|
101079
101096
|
import fs15 from "node:fs";
|
|
101080
101097
|
import path19 from "node:path";
|
|
101081
101098
|
init_dist();
|
|
101099
|
+
init_api();
|
|
101100
|
+
init_auth();
|
|
101101
|
+
init_config2();
|
|
101102
|
+
init_project();
|
|
101082
101103
|
async function runValidateAgents(opts) {
|
|
101083
101104
|
const projectRoot = resolveProjectRoot(opts.cwd ?? process.cwd());
|
|
101084
101105
|
const agentsDir = path19.join(projectRoot, "agents");
|
|
@@ -101261,6 +101282,94 @@ function validateOneBundle(bundle, allBundles) {
|
|
|
101261
101282
|
return { name: bundle.name, ok, phases, warnings };
|
|
101262
101283
|
}
|
|
101263
101284
|
__name(validateOneBundle, "validateOneBundle");
|
|
101285
|
+
async function runAvailabilityChecks(cwd) {
|
|
101286
|
+
const projectRoot = resolveProjectRoot(cwd);
|
|
101287
|
+
let projectId;
|
|
101288
|
+
try {
|
|
101289
|
+
projectId = loadProjectSettings(projectRoot).projectId;
|
|
101290
|
+
} catch {
|
|
101291
|
+
projectId = process.env.FIMO_PROJECT_ID?.trim() ?? "";
|
|
101292
|
+
if (!projectId) {
|
|
101293
|
+
return { ok: true, skippedReason: "not-linked", issues: [] };
|
|
101294
|
+
}
|
|
101295
|
+
}
|
|
101296
|
+
if (!process.env.FIMO_API_TOKEN?.trim() && !readCredentials()) {
|
|
101297
|
+
return { ok: true, skippedReason: "not-signed-in", issues: [] };
|
|
101298
|
+
}
|
|
101299
|
+
const api = new FimoApiClient(loadConfig());
|
|
101300
|
+
const bundles = await listBundles(path19.join(projectRoot, "agents"));
|
|
101301
|
+
const issues = [];
|
|
101302
|
+
const integrationsByAgent = bundles.map((bundle) => ({
|
|
101303
|
+
name: bundle.name,
|
|
101304
|
+
integrations: Object.entries(agentIntegrationPolicy(bundle.config)).map(([integrationKey, entry]) => ({
|
|
101305
|
+
integrationKey,
|
|
101306
|
+
enabled: entry.enabled,
|
|
101307
|
+
tools: enabledAgentIntegrationTools(entry)
|
|
101308
|
+
})).filter((entry) => entry.enabled)
|
|
101309
|
+
})).filter((entry) => entry.integrations.length > 0);
|
|
101310
|
+
if (integrationsByAgent.length > 0) {
|
|
101311
|
+
const statuses = await api.listProjectIntegrations(projectId);
|
|
101312
|
+
const statusByKey = new Map(statuses.map((status) => [status.integrationKey, status]));
|
|
101313
|
+
const infoByKey = /* @__PURE__ */ new Map();
|
|
101314
|
+
for (const agent of integrationsByAgent) {
|
|
101315
|
+
for (const integration of agent.integrations) {
|
|
101316
|
+
const file2 = `agents/${agent.name}/config.yaml`;
|
|
101317
|
+
const status = statusByKey.get(integration.integrationKey);
|
|
101318
|
+
if (!status) {
|
|
101319
|
+
issues.push({
|
|
101320
|
+
pointer: file2,
|
|
101321
|
+
message: `${agent.name} allows unsupported integration ${integration.integrationKey}.`
|
|
101322
|
+
});
|
|
101323
|
+
continue;
|
|
101324
|
+
}
|
|
101325
|
+
if (integration.tools.length === 0) {
|
|
101326
|
+
issues.push({
|
|
101327
|
+
pointer: file2,
|
|
101328
|
+
message: `${agent.name} enables ${integration.integrationKey}, but no tools are allowed. Set at least one integrations.${integration.integrationKey}.tools.<tool>: true entry.`
|
|
101329
|
+
});
|
|
101330
|
+
}
|
|
101331
|
+
if (!status.enabled) {
|
|
101332
|
+
issues.push({
|
|
101333
|
+
pointer: file2,
|
|
101334
|
+
message: `${agent.name} allows ${integration.integrationKey}, but the project integration is not enabled. Run: fimo integrations enable ${integration.integrationKey}`
|
|
101335
|
+
});
|
|
101336
|
+
}
|
|
101337
|
+
const info = infoByKey.get(integration.integrationKey) ?? await api.getProjectIntegrationInfo(projectId, integration.integrationKey);
|
|
101338
|
+
infoByKey.set(integration.integrationKey, info);
|
|
101339
|
+
const knownTools = new Set(info.tools.map((tool) => tool.key));
|
|
101340
|
+
for (const tool of integration.tools) {
|
|
101341
|
+
if (!knownTools.has(tool)) {
|
|
101342
|
+
issues.push({
|
|
101343
|
+
pointer: file2,
|
|
101344
|
+
message: `${agent.name} allows unknown ${integration.integrationKey}.${tool} tool. Supported tools: ${[...knownTools].sort().join(", ")}`
|
|
101345
|
+
});
|
|
101346
|
+
}
|
|
101347
|
+
}
|
|
101348
|
+
}
|
|
101349
|
+
}
|
|
101350
|
+
}
|
|
101351
|
+
const secretsByAgent = bundles.map((bundle) => ({
|
|
101352
|
+
name: bundle.name,
|
|
101353
|
+
keys: (bundle.config.secrets ?? []).filter((secret) => secret.required !== false).map((secret) => secret.key)
|
|
101354
|
+
})).filter((entry) => entry.keys.length > 0);
|
|
101355
|
+
if (secretsByAgent.length > 0) {
|
|
101356
|
+
const defined = new Set(
|
|
101357
|
+
(await api.listProjectSecretEntries(projectId, { kind: "secret" })).map((secret) => secret.key)
|
|
101358
|
+
);
|
|
101359
|
+
for (const entry of secretsByAgent) {
|
|
101360
|
+
for (const key of entry.keys) {
|
|
101361
|
+
if (!defined.has(key)) {
|
|
101362
|
+
issues.push({
|
|
101363
|
+
pointer: `agents/${entry.name}/config.yaml`,
|
|
101364
|
+
message: `${entry.name} requires project secret ${key}, but it is not set. Run: fimo secrets set ${key} --stdin`
|
|
101365
|
+
});
|
|
101366
|
+
}
|
|
101367
|
+
}
|
|
101368
|
+
}
|
|
101369
|
+
}
|
|
101370
|
+
return { ok: issues.length === 0, skippedReason: null, issues };
|
|
101371
|
+
}
|
|
101372
|
+
__name(runAvailabilityChecks, "runAvailabilityChecks");
|
|
101264
101373
|
async function validateAgentsCommand(opts) {
|
|
101265
101374
|
let report;
|
|
101266
101375
|
try {
|
|
@@ -101276,6 +101385,10 @@ async function validateAgentsCommand(opts) {
|
|
|
101276
101385
|
ui.info({ command: "agents validate", message: "No bundles or fimo.config.json found." });
|
|
101277
101386
|
return report;
|
|
101278
101387
|
}
|
|
101388
|
+
report.availability = await runAvailabilityChecks(opts.cwd ?? process.cwd());
|
|
101389
|
+
if (!report.availability.ok) {
|
|
101390
|
+
report.ok = false;
|
|
101391
|
+
}
|
|
101279
101392
|
ui.result({
|
|
101280
101393
|
command: "agents validate",
|
|
101281
101394
|
data: report,
|
|
@@ -101320,6 +101433,19 @@ function renderReportHuman(report) {
|
|
|
101320
101433
|
}
|
|
101321
101434
|
}
|
|
101322
101435
|
}
|
|
101436
|
+
if (report.availability) {
|
|
101437
|
+
if (report.availability.skippedReason) {
|
|
101438
|
+
const why = report.availability.skippedReason === "not-signed-in" ? "not signed in \u2014 run `fimo login`" : "no linked project";
|
|
101439
|
+
lines.push(` \u21B7 integrations & secrets \u2014 skipped (${why})`);
|
|
101440
|
+
} else if (report.availability.ok) {
|
|
101441
|
+
lines.push(" \u2713 integrations & secrets available");
|
|
101442
|
+
} else {
|
|
101443
|
+
lines.push(" \u2717 integrations & secrets");
|
|
101444
|
+
for (const issue2 of report.availability.issues) {
|
|
101445
|
+
lines.push(` \u2717 ${issue2.message}`);
|
|
101446
|
+
}
|
|
101447
|
+
}
|
|
101448
|
+
}
|
|
101323
101449
|
return lines.join("\n");
|
|
101324
101450
|
}
|
|
101325
101451
|
__name(renderReportHuman, "renderReportHuman");
|
|
@@ -102283,9 +102409,9 @@ init_interactive();
|
|
|
102283
102409
|
init_project_context();
|
|
102284
102410
|
|
|
102285
102411
|
// src/cli/commands/validate.ts
|
|
102412
|
+
init_dist();
|
|
102286
102413
|
import { readFile as readFile3 } from "node:fs/promises";
|
|
102287
102414
|
import path29 from "node:path";
|
|
102288
|
-
init_dist();
|
|
102289
102415
|
|
|
102290
102416
|
// src/runtime/shared/fimo-config.server.js
|
|
102291
102417
|
import { readFileSync as readFileSync2, renameSync, writeFileSync as writeFileSync3 } from "fs";
|
|
@@ -102736,14 +102862,31 @@ function validateSchemas(options = {}) {
|
|
|
102736
102862
|
}
|
|
102737
102863
|
const files = readdirSync4(schemasDir).filter((f) => f.endsWith(".json")).sort();
|
|
102738
102864
|
const errors2 = [];
|
|
102865
|
+
const schemaUids = /* @__PURE__ */ new Set();
|
|
102866
|
+
for (const fileName of files) {
|
|
102867
|
+
const schema = readSchemaObject(join9(schemasDir, fileName));
|
|
102868
|
+
if (schema && typeof schema.uid === "string") {
|
|
102869
|
+
schemaUids.add(schema.uid);
|
|
102870
|
+
}
|
|
102871
|
+
}
|
|
102739
102872
|
for (const fileName of files) {
|
|
102740
102873
|
const file2 = join9(SCHEMAS_REL2, fileName);
|
|
102741
|
-
errors2.push(...validateSchemaFile(join9(schemasDir, fileName), file2));
|
|
102874
|
+
errors2.push(...validateSchemaFile(join9(schemasDir, fileName), file2, schemaUids));
|
|
102742
102875
|
}
|
|
102743
102876
|
return { errors: errors2, schemasChecked: files.length };
|
|
102744
102877
|
}
|
|
102745
102878
|
__name(validateSchemas, "validateSchemas");
|
|
102746
|
-
function
|
|
102879
|
+
function readSchemaObject(absolutePath) {
|
|
102880
|
+
let schema;
|
|
102881
|
+
try {
|
|
102882
|
+
schema = JSON.parse(readFileSync7(absolutePath, "utf-8"));
|
|
102883
|
+
} catch {
|
|
102884
|
+
return null;
|
|
102885
|
+
}
|
|
102886
|
+
return typeof schema === "object" && schema !== null && !Array.isArray(schema) ? schema : null;
|
|
102887
|
+
}
|
|
102888
|
+
__name(readSchemaObject, "readSchemaObject");
|
|
102889
|
+
function validateSchemaFile(absolutePath, file2, schemaUids = /* @__PURE__ */ new Set()) {
|
|
102747
102890
|
let schema;
|
|
102748
102891
|
try {
|
|
102749
102892
|
schema = JSON.parse(readFileSync7(absolutePath, "utf-8"));
|
|
@@ -102774,6 +102917,12 @@ function validateSchemaFile(absolutePath, file2) {
|
|
|
102774
102917
|
message: `"uid" is "${uid}" but the file is ${expectedUid}.json \u2014 they must match.`,
|
|
102775
102918
|
file: file2
|
|
102776
102919
|
});
|
|
102920
|
+
} else if (!CONTENT_TYPE_NAME_PATTERN.test(uid)) {
|
|
102921
|
+
errors2.push({
|
|
102922
|
+
rule: "schemas/type-name",
|
|
102923
|
+
message: `"uid" must use ${CONTENT_TYPE_NAME_PATTERN_DESCRIPTION}.`,
|
|
102924
|
+
file: file2
|
|
102925
|
+
});
|
|
102777
102926
|
}
|
|
102778
102927
|
if (typeof name !== "string" || name.length === 0) {
|
|
102779
102928
|
errors2.push({
|
|
@@ -102791,6 +102940,14 @@ function validateSchemaFile(absolutePath, file2) {
|
|
|
102791
102940
|
return errors2;
|
|
102792
102941
|
}
|
|
102793
102942
|
for (const [fieldName, definition] of Object.entries(fields)) {
|
|
102943
|
+
if (!CONTENT_FIELD_NAME_PATTERN.test(fieldName)) {
|
|
102944
|
+
errors2.push({
|
|
102945
|
+
rule: "schemas/field-name",
|
|
102946
|
+
message: `Field "${fieldName}" must use ${CONTENT_FIELD_NAME_PATTERN_DESCRIPTION}.`,
|
|
102947
|
+
file: file2
|
|
102948
|
+
});
|
|
102949
|
+
continue;
|
|
102950
|
+
}
|
|
102794
102951
|
if (RESERVED_FIELD_NAMES.has(fieldName)) {
|
|
102795
102952
|
errors2.push({
|
|
102796
102953
|
rule: "schemas/reserved-field",
|
|
@@ -102822,6 +102979,39 @@ function validateSchemaFile(absolutePath, file2) {
|
|
|
102822
102979
|
file: file2
|
|
102823
102980
|
});
|
|
102824
102981
|
}
|
|
102982
|
+
if (type === "relation") {
|
|
102983
|
+
const { target, many } = definition;
|
|
102984
|
+
if (typeof target !== "string" || !CONTENT_TYPE_NAME_PATTERN.test(target)) {
|
|
102985
|
+
errors2.push({
|
|
102986
|
+
rule: "schemas/relation-target",
|
|
102987
|
+
message: `Relation field "${fieldName}" must declare a valid "target" content type.`,
|
|
102988
|
+
file: file2
|
|
102989
|
+
});
|
|
102990
|
+
} else if (schemaUids.size > 0 && !schemaUids.has(target)) {
|
|
102991
|
+
errors2.push({
|
|
102992
|
+
rule: "schemas/relation-target",
|
|
102993
|
+
message: `Relation field "${fieldName}" targets unknown schema "${target}".`,
|
|
102994
|
+
file: file2
|
|
102995
|
+
});
|
|
102996
|
+
}
|
|
102997
|
+
if (many !== void 0 && typeof many !== "boolean") {
|
|
102998
|
+
errors2.push({
|
|
102999
|
+
rule: "schemas/relation-many",
|
|
103000
|
+
message: `Relation field "${fieldName}" must use a boolean "many" value.`,
|
|
103001
|
+
file: file2
|
|
103002
|
+
});
|
|
103003
|
+
}
|
|
103004
|
+
} else {
|
|
103005
|
+
for (const relationOnlyKey of ["target", "many", "localized"]) {
|
|
103006
|
+
if (relationOnlyKey in definition) {
|
|
103007
|
+
errors2.push({
|
|
103008
|
+
rule: "schemas/relation-shape",
|
|
103009
|
+
message: `Field "${fieldName}": "${relationOnlyKey}" is only supported on relation fields.`,
|
|
103010
|
+
file: file2
|
|
103011
|
+
});
|
|
103012
|
+
}
|
|
103013
|
+
}
|
|
103014
|
+
}
|
|
102825
103015
|
}
|
|
102826
103016
|
return errors2;
|
|
102827
103017
|
}
|
|
@@ -103590,131 +103780,12 @@ async function runAgentsCheck(opts) {
|
|
|
103590
103780
|
}
|
|
103591
103781
|
}
|
|
103592
103782
|
}
|
|
103593
|
-
errors2.push(...await runAgentSecretAvailabilityCheck({ cwd }));
|
|
103594
|
-
errors2.push(...await runAgentIntegrationAvailabilityCheck({ cwd }));
|
|
103595
103783
|
return {
|
|
103596
103784
|
errors: errors2,
|
|
103597
103785
|
stats: { agentsChecked: report.bundles.length }
|
|
103598
103786
|
};
|
|
103599
103787
|
}
|
|
103600
103788
|
__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
103789
|
function runRouteMetadataCheck(opts) {
|
|
103719
103790
|
return validateRouteMetadata({ cwd: opts.cwd });
|
|
103720
103791
|
}
|
|
@@ -103778,8 +103849,24 @@ async function runLabelsPhase(opts) {
|
|
|
103778
103849
|
}
|
|
103779
103850
|
const api = new FimoApiClient(loadConfig()).withEnv(ctx.env);
|
|
103780
103851
|
const locale = getConfigServer(cwd).i18n?.defaultLocale ?? "en";
|
|
103852
|
+
const pinnedBranch = (await readProjectEnv(cwd)).VITE_FIMO_BRANCH;
|
|
103781
103853
|
try {
|
|
103782
|
-
|
|
103854
|
+
let labels2;
|
|
103855
|
+
if (pinnedBranch) {
|
|
103856
|
+
const branchLabels = await loadPublicTenantLabels(cwd, locale);
|
|
103857
|
+
if (branchLabels == null) {
|
|
103858
|
+
return {
|
|
103859
|
+
name: "Labels",
|
|
103860
|
+
ok: false,
|
|
103861
|
+
errors: [
|
|
103862
|
+
infraErrorFromException("labels-check", new Error(`Could not read labels from branch "${pinnedBranch}"`))
|
|
103863
|
+
]
|
|
103864
|
+
};
|
|
103865
|
+
}
|
|
103866
|
+
labels2 = branchLabels;
|
|
103867
|
+
} else {
|
|
103868
|
+
labels2 = labelsToRecord(await api.listLabels(ctx.projectId, locale));
|
|
103869
|
+
}
|
|
103783
103870
|
const existingKeys = new Set(Object.keys(labels2));
|
|
103784
103871
|
const missing = labelKeys.filter((label) => !existingKeys.has(label.key));
|
|
103785
103872
|
return {
|
|
@@ -105989,6 +106076,9 @@ async function entriesListCommand(typeName, options, cmd) {
|
|
|
105989
106076
|
if (options.fields) {
|
|
105990
106077
|
params.fields = options.fields;
|
|
105991
106078
|
}
|
|
106079
|
+
if (options.populate) {
|
|
106080
|
+
params.populate = options.populate;
|
|
106081
|
+
}
|
|
105992
106082
|
if (options.locale) {
|
|
105993
106083
|
params.locale = options.locale;
|
|
105994
106084
|
}
|
|
@@ -107857,14 +107947,18 @@ __name(labelsSetCommand, "labelsSetCommand");
|
|
|
107857
107947
|
async function labelsSetManyCommand(options, cmd) {
|
|
107858
107948
|
const { ctx, api } = await bootstrapProjectCommand(options, cmd, { refuseReadOnly: true });
|
|
107859
107949
|
await requireOperation("i18n.update", { projectId: ctx.projectId, env: ctx.env });
|
|
107860
|
-
const
|
|
107861
|
-
const
|
|
107862
|
-
|
|
107950
|
+
const fallbackLocale = options.locale ?? defaultLocaleForProject(options.cwd);
|
|
107951
|
+
const records = readLabelRecords(options).map(({ key, value, locale }) => ({
|
|
107952
|
+
key,
|
|
107953
|
+
value,
|
|
107954
|
+
locale: locale ?? fallbackLocale
|
|
107955
|
+
}));
|
|
107956
|
+
const locales = [...new Set(records.map((record2) => record2.locale))];
|
|
107863
107957
|
const result = await api.setLabels(ctx.projectId, records);
|
|
107864
107958
|
ui.result({
|
|
107865
107959
|
command: "labels set-many",
|
|
107866
|
-
data: { ...result, locale, labels: records.length },
|
|
107867
|
-
render: /* @__PURE__ */ __name(() => `Set ${records.length} label${records.length === 1 ? "" : "s"} (${
|
|
107960
|
+
data: { ...result, locale: fallbackLocale, locales, labels: records.length },
|
|
107961
|
+
render: /* @__PURE__ */ __name(() => `Set ${records.length} label${records.length === 1 ? "" : "s"} (${locales.join(", ")})`, "render"),
|
|
107868
107962
|
warnings: dbLabelsRuntimeWarnings(options.cwd)
|
|
107869
107963
|
});
|
|
107870
107964
|
}
|
|
@@ -107882,7 +107976,7 @@ async function labelsDeleteCommand(key, options, cmd) {
|
|
|
107882
107976
|
});
|
|
107883
107977
|
}
|
|
107884
107978
|
__name(labelsDeleteCommand, "labelsDeleteCommand");
|
|
107885
|
-
function
|
|
107979
|
+
function readLabelRecords(options) {
|
|
107886
107980
|
if (options.file && options.data) {
|
|
107887
107981
|
throw new Error("Pass either --file or --data, not both.");
|
|
107888
107982
|
}
|
|
@@ -107892,36 +107986,39 @@ function readLabelMap(options) {
|
|
|
107892
107986
|
const raw = options.data ?? readLabelFile(options);
|
|
107893
107987
|
const parsed = JSON.parse(raw);
|
|
107894
107988
|
if (Array.isArray(parsed)) {
|
|
107895
|
-
return
|
|
107896
|
-
|
|
107897
|
-
|
|
107898
|
-
|
|
107899
|
-
|
|
107900
|
-
|
|
107901
|
-
|
|
107902
|
-
|
|
107903
|
-
|
|
107904
|
-
|
|
107905
|
-
|
|
107906
|
-
|
|
107989
|
+
return parsed.map((item) => {
|
|
107990
|
+
if (!item || typeof item !== "object") {
|
|
107991
|
+
throw new Error("Label arrays must contain objects with key and value fields.");
|
|
107992
|
+
}
|
|
107993
|
+
const record2 = item;
|
|
107994
|
+
if (typeof record2.key !== "string") {
|
|
107995
|
+
throw new Error("Each label record must include a string key.");
|
|
107996
|
+
}
|
|
107997
|
+
const value = record2.value ?? record2.defaultValue;
|
|
107998
|
+
if (typeof value !== "string") {
|
|
107999
|
+
throw new Error(`Label ${record2.key} must include a string value.`);
|
|
108000
|
+
}
|
|
108001
|
+
let locale;
|
|
108002
|
+
if (record2.locale !== void 0) {
|
|
108003
|
+
if (typeof record2.locale !== "string") {
|
|
108004
|
+
throw new Error(`Label ${record2.key} has a non-string locale.`);
|
|
107907
108005
|
}
|
|
107908
|
-
|
|
107909
|
-
}
|
|
107910
|
-
|
|
108006
|
+
locale = record2.locale;
|
|
108007
|
+
}
|
|
108008
|
+
return { key: record2.key, value, locale };
|
|
108009
|
+
});
|
|
107911
108010
|
}
|
|
107912
108011
|
if (!parsed || typeof parsed !== "object") {
|
|
107913
108012
|
throw new Error("Labels JSON must be an object map or an array of { key, value } records.");
|
|
107914
108013
|
}
|
|
107915
|
-
|
|
107916
|
-
for (const [key, value] of Object.entries(parsed)) {
|
|
108014
|
+
return Object.entries(parsed).map(([key, value]) => {
|
|
107917
108015
|
if (typeof value !== "string") {
|
|
107918
108016
|
throw new Error(`Label ${key} must be a string.`);
|
|
107919
108017
|
}
|
|
107920
|
-
|
|
107921
|
-
}
|
|
107922
|
-
return labels2;
|
|
108018
|
+
return { key, value };
|
|
108019
|
+
});
|
|
107923
108020
|
}
|
|
107924
|
-
__name(
|
|
108021
|
+
__name(readLabelRecords, "readLabelRecords");
|
|
107925
108022
|
function readLabelFile(options) {
|
|
107926
108023
|
const projectRoot = resolveProjectCwd(options.cwd);
|
|
107927
108024
|
const filePath = path38.resolve(projectRoot, options.file);
|
|
@@ -109974,7 +110071,7 @@ schemas2.command("get").description("Get a schema by type name").argument("<type
|
|
|
109974
110071
|
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
110072
|
schemas2.command("delete").description("Delete a schema by type name").argument("<typeName>", "Content type name").option("-C, --cwd <dir>", "Project directory").action(schemaDeleteCommand);
|
|
109976
110073
|
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);
|
|
110074
|
+
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
110075
|
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
110076
|
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
110077
|
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 +110148,7 @@ rules.command("sync").description(
|
|
|
110051
110148
|
var labels = program.command("labels").description("Manage site labels stored in Fimo");
|
|
110052
110149
|
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
110150
|
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);
|
|
110151
|
+
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
110152
|
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
110153
|
var analytics = program.command("analytics").description("Read site analytics (pageviews, top breakdowns, annotations)");
|
|
110057
110154
|
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":"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"}
|
|
@@ -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
|
@@ -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' })]);
|