fimo 0.3.6 → 0.4.0-experimental.2

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