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.
@@ -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", Array.isArray(fields) ? fields.join(",") : 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 *creating an entry* (see "Entry body shape" below); it just
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 `fields`. Use these canonical names in new code.
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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "bundled": true,
3
3
  "bundler": "esbuild",
4
- "bundledAt": "2026-07-07T18:48:49.464Z",
4
+ "bundledAt": "2026-07-08T14:52:17.138Z",
5
5
  "cliVersion": "0.3.5",
6
6
  "external": [
7
7
  "oxc-parser",
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`, "!fimo git-credential"], cwd);
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 validateSchemaFile(absolutePath, file2) {
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
- const labels2 = await loadPublicTenantLabels(cwd, locale) ?? labelsToRecord(await api.listLabels(ctx.projectId, locale));
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 locale = options.locale ?? defaultLocaleForProject(options.cwd);
107861
- const labels2 = readLabelMap(options);
107862
- const records = Object.entries(labels2).map(([key, value]) => ({ key, value, locale }));
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"} (${locale})`, "render"),
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 readLabelMap(options) {
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 Object.fromEntries(
107896
- parsed.map((item) => {
107897
- if (!item || typeof item !== "object") {
107898
- throw new Error("Label arrays must contain objects with key and value fields.");
107899
- }
107900
- const record2 = item;
107901
- if (typeof record2.key !== "string") {
107902
- throw new Error("Each label record must include a string key.");
107903
- }
107904
- const value = record2.value ?? record2.defaultValue;
107905
- if (typeof value !== "string") {
107906
- throw new Error(`Label ${record2.key} must include a string value.`);
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
- return [record2.key, value];
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
- const labels2 = {};
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
- labels2[key] = value;
107921
- }
107922
- return labels2;
108018
+ return { key, value };
108019
+ });
107923
108020
  }
107924
- __name(readLabelMap, "readLabelMap");
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) => {
@@ -61,6 +61,7 @@ export type Query<T extends {
61
61
  limit?: number;
62
62
  offset?: number;
63
63
  fields?: TFields;
64
+ populate?: Fields<T>;
64
65
  };
65
66
  export {};
66
67
  //# sourceMappingURL=query.d.ts.map
@@ -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
- export function validateSchemaFile(absolutePath, file) {
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,6 +1,6 @@
1
1
  {
2
2
  "name": "fimo",
3
- "version": "0.3.6-staging.1",
3
+ "version": "0.4.0-experimental.1",
4
4
  "description": "Fimo CLI - create, deploy, and manage Fimo projects",
5
5
  "bin": {
6
6
  "fimo": "dist/cli/index.js"
@@ -21,7 +21,7 @@
21
21
  "cmdk": "^1.1.1",
22
22
  "date-fns": "^4.1.0",
23
23
  "embla-carousel-react": "^8.6.0",
24
- "fimo": "0.3.6-staging.1",
24
+ "fimo": "0.4.0-experimental.1",
25
25
  "input-otp": "^1.4.2",
26
26
  "isbot": "^5",
27
27
  "lucide-react": "^0.577.0",
@@ -1,16 +1,15 @@
1
- import type { RouteConfig } from '@react-router/dev/routes';
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
- const routes = [index('./pages/Index.tsx', { id: 'home' })] satisfies RouteConfig;
15
-
16
- export default fimoRoutes(routes) satisfies RouteConfig;
15
+ export default fimoRoutes([index('./pages/Index.tsx', { id: 'home' })]);