@lupinum/ginko-content 0.1.5 → 0.2.0

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.
Files changed (230) hide show
  1. package/compatibility.json +1 -1
  2. package/dist/cli.mjs +2 -2
  3. package/dist/cms-contract/build.js +15 -20
  4. package/dist/cms-contract/types.d.ts +12 -7
  5. package/dist/core/content/document +1 -0
  6. package/dist/core/content/document.d.ts +16 -0
  7. package/dist/core/content/document.js +2 -0
  8. package/dist/core/content/graph.d.ts +3 -1
  9. package/dist/core/content/graph.js +21 -20
  10. package/dist/core/content/locale.d.ts +6 -1
  11. package/dist/core/content/locale.js +17 -6
  12. package/dist/core/content/path.d.ts +5 -5
  13. package/dist/core/content/path.js +8 -8
  14. package/dist/core/content/snapshot +1 -0
  15. package/dist/core/content/snapshot.d.ts +25 -0
  16. package/dist/core/content/snapshot.js +99 -0
  17. package/dist/core/provider-errors.d.ts +1 -1
  18. package/dist/core/provider-errors.js +1 -0
  19. package/dist/core/query/builder.js +3 -3
  20. package/dist/core/query/execute.js +75 -36
  21. package/dist/core/query/filter.js +2 -2
  22. package/dist/core/query/lower.js +79 -3
  23. package/dist/core/query/params.js +7 -7
  24. package/dist/core/query/plan.d.ts +18 -0
  25. package/dist/core/query/plan.js +7 -0
  26. package/dist/core/references/resolve.js +6 -9
  27. package/dist/features/agent/agent-markdown +1 -0
  28. package/dist/features/agent/agent-markdown.d.ts +116 -0
  29. package/dist/features/agent/agent-markdown.js +138 -0
  30. package/dist/features/agent/walker +1 -0
  31. package/dist/features/agent/walker.d.ts +10 -0
  32. package/dist/features/agent/walker.js +174 -0
  33. package/dist/features/collections/resolve.d.ts +2 -2
  34. package/dist/features/collections/resolve.js +19 -16
  35. package/dist/features/localization/links.js +1 -1
  36. package/dist/features/localization/results.d.ts +3 -3
  37. package/dist/features/localization/results.js +46 -41
  38. package/dist/features/localization/translated-slugs.js +5 -5
  39. package/dist/features/navigation/build.d.ts +2 -2
  40. package/dist/features/navigation/build.js +23 -26
  41. package/dist/features/navigation/canonical.d.ts +10 -11
  42. package/dist/features/navigation/canonical.js +26 -21
  43. package/dist/features/navigation/resolve.js +4 -4
  44. package/dist/features/navigation/tree.js +3 -3
  45. package/dist/{runtime → features}/query/backlinks.js +3 -5
  46. package/dist/{runtime → features}/query/documents.js +1 -1
  47. package/dist/{runtime → features}/query/localized-docs.d.ts +1 -1
  48. package/dist/{runtime → features}/query/localized-docs.js +3 -6
  49. package/dist/{runtime → features}/query/navigation.d.ts +1 -1
  50. package/dist/{runtime → features}/query/navigation.js +8 -8
  51. package/dist/{runtime → features}/query/populate.js +6 -14
  52. package/dist/{runtime → features}/query/unified.d.ts +4 -4
  53. package/dist/{runtime → features}/query/unified.js +4 -2
  54. package/dist/{runtime → features}/query/variants.js +4 -4
  55. package/dist/features/search/navigation.js +2 -2
  56. package/dist/features/search/sections.d.ts +1 -1
  57. package/dist/features/search/sections.js +1 -1
  58. package/dist/features/sitemap/query.d.ts +5 -5
  59. package/dist/features/sitemap/query.js +6 -6
  60. package/dist/integrations/nitro/ingest.js +9 -20
  61. package/dist/integrations/nitro/storage.d.ts +1 -0
  62. package/dist/integrations/nitro/storage.js +21 -34
  63. package/dist/module.d.mts +135 -803
  64. package/dist/module.json +1 -1
  65. package/dist/module.mjs +143 -51
  66. package/dist/parsers/csv.js +3 -3
  67. package/dist/parsers/index.js +3 -3
  68. package/dist/parsers/json.js +8 -6
  69. package/dist/parsers/markdown-plugins.js +9 -5
  70. package/dist/parsers/markdown.js +7 -6
  71. package/dist/parsers/path-meta.js +21 -19
  72. package/dist/parsers/reserved +1 -0
  73. package/dist/parsers/reserved.d.ts +15 -0
  74. package/dist/parsers/reserved.js +35 -0
  75. package/dist/parsers/yaml.js +6 -4
  76. package/dist/public/agent +1 -0
  77. package/dist/public/agent.d.ts +17 -0
  78. package/dist/public/agent.js +34 -0
  79. package/dist/public/client.d.ts +2 -3
  80. package/dist/public/client.js +1 -2
  81. package/dist/public/provider-query +1 -0
  82. package/dist/public/provider-query.d.ts +58 -0
  83. package/dist/public/provider-query.js +5 -0
  84. package/dist/public/provider.d.ts +8 -3
  85. package/dist/public/provider.js +2 -0
  86. package/dist/public/server.d.ts +7 -12
  87. package/dist/public/server.js +5 -36
  88. package/dist/runtime/app/components/ContentRenderer.d.vue.ts +3 -3
  89. package/dist/runtime/app/components/ContentRenderer.vue +2 -2
  90. package/dist/runtime/app/components/ContentRenderer.vue.d.ts +3 -3
  91. package/dist/runtime/app/components/ContentRendererInline.d.vue.ts +1 -1
  92. package/dist/runtime/app/components/ContentRendererInline.vue.d.ts +1 -1
  93. package/dist/runtime/app/components/Prose/ProseImg.d.vue.ts +4 -4
  94. package/dist/runtime/app/components/Prose/ProseImg.vue.d.ts +4 -4
  95. package/dist/runtime/app/components/Prose/ProsePre.d.vue.ts +1 -1
  96. package/dist/runtime/app/components/Prose/ProsePre.vue.d.ts +1 -1
  97. package/dist/runtime/app/components/internal/ContentRendererMarkdown.d.vue.ts +1 -1
  98. package/dist/runtime/app/components/internal/ContentRendererMarkdown.vue +4 -4
  99. package/dist/runtime/app/components/internal/ContentRendererMarkdown.vue.d.ts +1 -1
  100. package/dist/runtime/app/components/internal/MarkdownRenderer.d.ts +3 -3
  101. package/dist/runtime/app/composables/preview.d.ts +1 -1
  102. package/dist/runtime/app/composables/query-api.d.ts +5 -5
  103. package/dist/runtime/app/composables/query-api.js +5 -3
  104. package/dist/runtime/app/composables/route.d.ts +1 -1
  105. package/dist/runtime/app/composables/route.js +4 -4
  106. package/dist/runtime/app/composables/search.js +1 -1
  107. package/dist/runtime/app/composables/use-content-document.js +1 -1
  108. package/dist/runtime/app/composables/use-content-list.js +1 -1
  109. package/dist/runtime/app/composables/use-content-navigation.js +2 -2
  110. package/dist/runtime/app/composables/use-content-page.js +4 -4
  111. package/dist/runtime/app/pages/content.vue +2 -1
  112. package/dist/runtime/markdown/plugins.js +6 -8
  113. package/dist/runtime/server/agent-markdown.d.ts +5 -65
  114. package/dist/runtime/server/agent-markdown.js +40 -315
  115. package/dist/runtime/server/agent-site.js +9 -2
  116. package/dist/runtime/server/api/agent-raw.js +1 -1
  117. package/dist/runtime/server/api/cache.d.ts +1 -1
  118. package/dist/runtime/server/api/cache.js +43 -4
  119. package/dist/runtime/server/api/locales.js +3 -3
  120. package/dist/runtime/server/api/navigation.d.ts +1 -1
  121. package/dist/runtime/server/api/navigation.js +3 -1
  122. package/dist/runtime/server/api/query.js +2 -2
  123. package/dist/runtime/server/api/search-index.d.ts +1 -1
  124. package/dist/runtime/server/api/sitemap.d.ts +1 -1
  125. package/dist/runtime/server/cache-adapters.d.ts +18 -0
  126. package/dist/runtime/server/cache-adapters.js +12 -0
  127. package/dist/runtime/server/collection-helpers.d.ts +2 -2
  128. package/dist/runtime/server/collection-helpers.js +15 -11
  129. package/dist/runtime/server/index.d.ts +6 -4
  130. package/dist/runtime/server/index.js +10 -3
  131. package/dist/runtime/server/middleware/agent-link-headers.js +1 -1
  132. package/dist/runtime/server/middleware/agent-markdown.js +1 -1
  133. package/dist/runtime/server/navigation-query.d.ts +2 -2
  134. package/dist/runtime/server/navigation-query.js +52 -7
  135. package/dist/runtime/server/navigation.d.ts +1 -1
  136. package/dist/runtime/server/provider-document +1 -0
  137. package/dist/runtime/server/provider-document.d.ts +59 -0
  138. package/dist/runtime/server/provider-document.js +48 -0
  139. package/dist/runtime/server/provider-query-wire +1 -0
  140. package/dist/runtime/server/provider-query-wire.d.ts +32 -0
  141. package/dist/runtime/server/provider-query-wire.js +27 -0
  142. package/dist/runtime/server/provider-query.d.ts +18 -9
  143. package/dist/runtime/server/provider-query.js +62 -28
  144. package/dist/runtime/server/provider-result.d.ts +4 -4
  145. package/dist/runtime/server/providers/filesystem.js +3 -11
  146. package/dist/runtime/server/providers/index.d.ts +18 -1
  147. package/dist/runtime/server/providers/index.js +72 -20
  148. package/dist/runtime/server/query-api.d.ts +5 -5
  149. package/dist/runtime/server/query-api.js +9 -6
  150. package/dist/runtime/server/query-executor.d.ts +3 -3
  151. package/dist/runtime/server/query-executor.js +43 -59
  152. package/dist/runtime/server/search.d.ts +1 -1
  153. package/dist/runtime/server/search.js +4 -4
  154. package/dist/runtime/server/sitemap.js +5 -5
  155. package/dist/runtime/server/storage-access.d.ts +4 -1
  156. package/dist/runtime/server/storage-access.js +27 -2
  157. package/dist/runtime/transformers/component-resolver.d.ts +1 -1
  158. package/dist/storage/content.d.ts +2 -6
  159. package/dist/storage/content.js +19 -8
  160. package/dist/storage/contents.d.ts +9 -1
  161. package/dist/storage/contents.js +26 -19
  162. package/dist/storage/driver.d.ts +2 -2
  163. package/dist/storage/driver.js +2 -1
  164. package/dist/storage/graph.js +4 -0
  165. package/dist/storage/manifest.js +3 -1
  166. package/dist/storage/references.js +9 -5
  167. package/dist/storage/snapshot-runtime +1 -0
  168. package/dist/storage/snapshot-runtime.d.ts +11 -0
  169. package/dist/storage/snapshot-runtime.js +46 -0
  170. package/dist/storage/validation.d.ts +1 -1
  171. package/dist/storage/validation.js +79 -105
  172. package/dist/testing/provider-contract.d.ts +14 -4
  173. package/dist/testing/provider-contract.js +121 -68
  174. package/dist/testing/provider-fixture.d.ts +1 -1
  175. package/dist/testing/provider-fixture.js +124 -120
  176. package/dist/types/config.d.ts +17 -4
  177. package/dist/types/content.d.ts +104 -70
  178. package/dist/types/fields.js +1 -1
  179. package/dist/types/module.d.ts +1 -1
  180. package/dist/types/query-parts/public.d.ts +14 -5
  181. package/dist/types/query-parts/results.d.ts +6 -2
  182. package/dist/types/query-parts/transport.d.ts +19 -0
  183. package/dist/types/virtual.d.ts +6 -0
  184. package/dist/types.d.mts +2 -2
  185. package/dist/web-types.json +1 -1
  186. package/package.json +39 -9
  187. package/dist/runtime/app/plugins/markdown-components +0 -1
  188. package/dist/runtime/app/plugins/markdown-components.d.ts +0 -2
  189. package/dist/runtime/app/plugins/markdown-components.js +0 -19
  190. /package/dist/{runtime → features/agent}/agent-paths +0 -0
  191. /package/dist/{runtime → features/agent}/agent-paths.d.ts +0 -0
  192. /package/dist/{runtime → features/agent}/agent-paths.js +0 -0
  193. /package/dist/{runtime → features}/query/backlinks +0 -0
  194. /package/dist/{runtime → features}/query/backlinks.d.ts +0 -0
  195. /package/dist/{runtime → features}/query/context +0 -0
  196. /package/dist/{runtime → features}/query/context.d.ts +0 -0
  197. /package/dist/{runtime → features}/query/context.js +0 -0
  198. /package/dist/{runtime → features}/query/documents +0 -0
  199. /package/dist/{runtime → features}/query/documents.d.ts +0 -0
  200. /package/dist/{runtime → features}/query/errors +0 -0
  201. /package/dist/{runtime → features}/query/errors.d.ts +0 -0
  202. /package/dist/{runtime → features}/query/errors.js +0 -0
  203. /package/dist/{runtime → features}/query/handles +0 -0
  204. /package/dist/{runtime → features}/query/handles.d.ts +0 -0
  205. /package/dist/{runtime → features}/query/handles.js +0 -0
  206. /package/dist/{runtime → features}/query/locale-options +0 -0
  207. /package/dist/{runtime → features}/query/locale-options.d.ts +0 -0
  208. /package/dist/{runtime → features}/query/locale-options.js +0 -0
  209. /package/dist/{runtime → features}/query/localized-docs +0 -0
  210. /package/dist/{runtime → features}/query/navigation +0 -0
  211. /package/dist/{runtime → features}/query/pagination +0 -0
  212. /package/dist/{runtime → features}/query/pagination.d.ts +0 -0
  213. /package/dist/{runtime → features}/query/pagination.js +0 -0
  214. /package/dist/{runtime → features}/query/populate +0 -0
  215. /package/dist/{runtime → features}/query/populate.d.ts +0 -0
  216. /package/dist/{runtime → features}/query/public-limits +0 -0
  217. /package/dist/{runtime → features}/query/public-limits.d.ts +0 -0
  218. /package/dist/{runtime → features}/query/public-limits.js +0 -0
  219. /package/dist/{runtime → features}/query/resolve +0 -0
  220. /package/dist/{runtime → features}/query/resolve.d.ts +0 -0
  221. /package/dist/{runtime → features}/query/resolve.js +0 -0
  222. /package/dist/{runtime → features}/query/responses +0 -0
  223. /package/dist/{runtime → features}/query/responses.d.ts +0 -0
  224. /package/dist/{runtime → features}/query/responses.js +0 -0
  225. /package/dist/{runtime → features}/query/routes +0 -0
  226. /package/dist/{runtime → features}/query/routes.d.ts +0 -0
  227. /package/dist/{runtime → features}/query/routes.js +0 -0
  228. /package/dist/{runtime → features}/query/unified +0 -0
  229. /package/dist/{runtime → features}/query/variants +0 -0
  230. /package/dist/{runtime → features}/query/variants.d.ts +0 -0
@@ -47,29 +47,43 @@ const collectReferenceIssues = (schema, value, resolveReference, issues, path =
47
47
  }
48
48
  }
49
49
  };
50
+ const collectDerivedReferenceIssues = (references, value, resolveReference, issues) => {
51
+ if (!references) {
52
+ return;
53
+ }
54
+ for (const [collection, fields] of Object.entries(references)) {
55
+ for (const field of fields) {
56
+ const fieldValue = value[field];
57
+ const values = Array.isArray(fieldValue) ? fieldValue : [fieldValue];
58
+ for (const item of values) {
59
+ if (typeof item === "undefined" || item === null) {
60
+ continue;
61
+ }
62
+ if (typeof item !== "string") {
63
+ issues.push(`${field}: expected a string reference`);
64
+ continue;
65
+ }
66
+ if (!resolveReference(item, collection === "*" ? void 0 : collection)) {
67
+ issues.push(`${field}: unresolved reference "${item}"${collection !== "*" ? ` in collection "${collection}"` : ""}`);
68
+ }
69
+ }
70
+ }
71
+ }
72
+ };
50
73
  const internalDocumentFields = /* @__PURE__ */ new Set([
51
- "_id",
52
- "_file",
53
- "_path",
54
- "_type",
55
- "_collection",
56
- "_dir",
57
- "_locale",
58
- "_canonicalKey",
59
- "_partial",
60
- "_navigation",
61
- "_draft",
62
- "_stem",
63
- "_extension",
64
- "_resolvedLocale",
65
- "_requestedLocale",
66
- "_fallback",
67
- "_availableLocales",
68
- "_variantPaths",
69
- "_resolvedRefs",
70
- "_source",
71
- "body",
72
- "draft"
74
+ "id",
75
+ "file",
76
+ "path",
77
+ "type",
78
+ "collection",
79
+ "canonicalKey",
80
+ "partial",
81
+ "draft",
82
+ "dir",
83
+ "locale",
84
+ "navigationFile",
85
+ "resolved",
86
+ "body"
73
87
  ]);
74
88
  const toUserContentDocument = (document) => {
75
89
  const userDocument = {};
@@ -81,7 +95,7 @@ const toUserContentDocument = (document) => {
81
95
  return userDocument;
82
96
  };
83
97
  const validateNavigationDocument = (document) => {
84
- if (!document._navigation) {
98
+ if (!document.navigationFile) {
85
99
  return ok(void 0);
86
100
  }
87
101
  const invalidFields = [];
@@ -106,7 +120,7 @@ const validateNavigationDocument = (document) => {
106
120
  if (invalidFields.length) {
107
121
  return fail(createContentError(
108
122
  "INVALID_NAVIGATION_YAML",
109
- document._file || document._id,
123
+ document.file?.path || document.id,
110
124
  "malformed .navigation.yml",
111
125
  invalidFields.join("; ")
112
126
  ));
@@ -114,13 +128,10 @@ const validateNavigationDocument = (document) => {
114
128
  return ok(void 0);
115
129
  };
116
130
  export const getCanonicalContentId = (document, locales = []) => {
117
- if (typeof document.id === "string" && document.id.length) {
118
- return document.id;
119
- }
120
- if (typeof document._canonicalKey === "string" && document._canonicalKey.length) {
121
- return document._canonicalKey;
131
+ if (typeof document.canonicalKey === "string" && document.canonicalKey.length) {
132
+ return document.canonicalKey;
122
133
  }
123
- const file = (document._file || "").replace(/^\/+/, "");
134
+ const file = (document.file?.path || "").replace(/^\/+/, "");
124
135
  const parts = file.split("/").filter(Boolean);
125
136
  const normalized = parts[0] && locales.includes(parts[0]) ? parts.slice(1) : parts;
126
137
  return normalized.join("/").replace(/\.[^.]+$/, "");
@@ -130,10 +141,10 @@ export const validateCollectionDocument = (document, collections = {}) => {
130
141
  if (!navigationResult.ok) {
131
142
  return navigationResult;
132
143
  }
133
- if (!document._collection) {
144
+ if (!document.collection) {
134
145
  return ok(document);
135
146
  }
136
- const collection = getCollectionConfig(document._collection, collections);
147
+ const collection = getCollectionConfig(document.collection, collections);
137
148
  const schema = collection?.schema;
138
149
  if (!hasSafeParse(schema)) {
139
150
  return ok(document);
@@ -149,7 +160,7 @@ export const validateCollectionDocument = (document, collections = {}) => {
149
160
  const path = issue.path?.length ? issue.path.join(".") : "document";
150
161
  return `${path}: ${issue.message}`;
151
162
  }).join("; ");
152
- const message = `Invalid content in ${document._file || document._id}${details ? ` (${details})` : ""}`;
163
+ const message = `Invalid content in ${document.file?.path || document.id}${details ? ` (${details})` : ""}`;
153
164
  if (collection?.strict === false) {
154
165
  console.warn(message);
155
166
  return ok(document);
@@ -158,32 +169,30 @@ export const validateCollectionDocument = (document, collections = {}) => {
158
169
  "SCHEMA_VALIDATION_FAILED",
159
170
  message,
160
171
  {
161
- file: document._file || document._id,
162
- collection: document._collection,
172
+ file: document.file?.path || document.id,
173
+ collection: document.collection,
163
174
  ...details ? { details } : {}
164
175
  }
165
176
  ));
166
177
  };
167
178
  export const validateContentGraph = (contents, config) => {
168
179
  const locales = config.locales || [];
169
- const docs = contents.filter((content) => content && content._path);
170
- const routeEntries = docs.filter((content) => !content._partial && !content._navigation);
171
- const markdownEntries = routeEntries.filter((content) => content._type === "markdown");
180
+ const docs = contents.filter((content) => content && content.path);
181
+ const routeEntries = docs.filter((content) => !content.partial && !content.navigationFile);
182
+ const markdownEntries = routeEntries.filter((content) => content.type === "markdown");
172
183
  const idsByLocale = /* @__PURE__ */ new Map();
173
184
  const pathsByLocale = /* @__PURE__ */ new Map();
174
185
  const referenceTargets = buildReferenceTargets(routeEntries, locales);
175
186
  const targetCollections = /* @__PURE__ */ new Map();
176
- const variantsByCanonicalKey = /* @__PURE__ */ new Map();
177
187
  const markdownVariantsByCanonicalKey = /* @__PURE__ */ new Map();
178
- const explicitIdsByValue = /* @__PURE__ */ new Map();
179
188
  const refsByValue = /* @__PURE__ */ new Map();
180
189
  for (const document of routeEntries) {
181
- const canonicalId = document._canonicalKey || getCanonicalContentId(document, locales);
182
- if (!canonicalId || !document._collection) {
190
+ const canonicalId = document.canonicalKey || getCanonicalContentId(document, locales);
191
+ if (!canonicalId || !document.collection) {
183
192
  continue;
184
193
  }
185
194
  const collections = targetCollections.get(canonicalId) || /* @__PURE__ */ new Set();
186
- collections.add(document._collection);
195
+ collections.add(document.collection);
187
196
  targetCollections.set(canonicalId, collections);
188
197
  }
189
198
  for (const issue of collectTranslatedSlugValidationIssues(markdownEntries, {
@@ -199,85 +208,44 @@ export const validateContentGraph = (contents, config) => {
199
208
  }
200
209
  return fail(createContentError("TRANSLATED_SLUG_CONFLICT", issue.file, issue.reason, issue.details));
201
210
  }
202
- for (const document of routeEntries) {
203
- const variantKey = document._canonicalKey || getCanonicalContentId(document, locales);
204
- const siblings = variantsByCanonicalKey.get(variantKey) || [];
205
- siblings.push(document);
206
- variantsByCanonicalKey.set(variantKey, siblings);
207
- }
208
211
  for (const document of markdownEntries) {
209
- const variantKey = document._canonicalKey || getCanonicalContentId(document, locales);
212
+ const variantKey = document.canonicalKey || getCanonicalContentId(document, locales);
210
213
  const siblings = markdownVariantsByCanonicalKey.get(variantKey) || [];
211
214
  siblings.push(document);
212
215
  markdownVariantsByCanonicalKey.set(variantKey, siblings);
213
216
  }
214
- for (const [variantKey, variants] of variantsByCanonicalKey.entries()) {
215
- const explicitIds = Array.from(new Set(variants.map((document) => typeof document.id === "string" && document.id.length ? document.id : void 0).filter((value) => Boolean(value))));
216
- if (explicitIds.length > 1) {
217
- return fail(createContentError(
218
- "CONFLICTING_REFS",
219
- variants[0]?._file || variants[0]?._id || variantKey,
220
- `conflicting explicit ids across locale variants for canonical key "${variantKey}"`,
221
- explicitIds.join(", ")
222
- ));
223
- }
224
- if (explicitIds.length === 1 && variants.some((document) => document.id !== explicitIds[0])) {
225
- return fail(createContentError(
226
- "CONFLICTING_REFS",
227
- variants[0]?._file || variants[0]?._id || variantKey,
228
- `explicit id "${explicitIds[0]}" must be declared consistently across locale variants for canonical key "${variantKey}"`
229
- ));
230
- }
231
- }
232
- for (const document of routeEntries) {
233
- const explicitId = typeof document.id === "string" && document.id.length ? document.id : void 0;
234
- if (!explicitId) {
235
- continue;
236
- }
237
- const variantKey = document._canonicalKey || getCanonicalContentId(document, locales);
238
- const previous = explicitIdsByValue.get(explicitId);
239
- if (previous && previous.variantKey !== variantKey) {
240
- return fail(createContentError(
241
- "CONFLICTING_REFS",
242
- document._file || document._id,
243
- `duplicate explicit id "${explicitId}"`,
244
- `conflicts with ${previous.document._file || previous.document._id}`
245
- ));
246
- }
247
- explicitIdsByValue.set(explicitId, { document, variantKey });
248
- }
249
217
  for (const [variantKey, variants] of markdownVariantsByCanonicalKey.entries()) {
250
218
  const explicitRefs = Array.from(new Set(variants.map((document) => typeof document.ref === "string" && document.ref.length ? document.ref : void 0).filter((value) => Boolean(value))));
251
219
  if (explicitRefs.length > 1) {
252
220
  return fail(createContentError(
253
221
  "CONFLICTING_REFS",
254
- variants[0]?._file || variants[0]?._id || variantKey,
222
+ variants[0]?.file?.path || variants[0]?.id || variantKey,
255
223
  `conflicting refs across locale variants for canonical key "${variantKey}"`,
256
224
  explicitRefs.join(", ")
257
225
  ));
258
226
  }
259
227
  }
260
228
  for (const document of routeEntries) {
261
- const canonicalId = document._canonicalKey || getCanonicalContentId(document, locales);
262
- const localeKey = `${canonicalId}:${document._locale || ""}`;
229
+ const canonicalId = document.canonicalKey || getCanonicalContentId(document, locales);
230
+ const localeKey = `${canonicalId}:${document.locale || ""}`;
263
231
  if (idsByLocale.has(localeKey)) {
264
232
  const previous = idsByLocale.get(localeKey);
265
233
  return fail(createContentError(
266
234
  "DUPLICATE_CANONICAL_ID",
267
- document._file || document._id,
268
- `duplicate canonical id "${canonicalId}" for locale "${document._locale || "default"}"`,
269
- `conflicts with ${previous._file || previous._id}`
235
+ document.file?.path || document.id,
236
+ `duplicate canonical id "${canonicalId}" for locale "${document.locale || "default"}"`,
237
+ `conflicts with ${previous.file?.path || previous.id}`
270
238
  ));
271
239
  }
272
240
  idsByLocale.set(localeKey, document);
273
- const pathKey = `${document._locale || ""}:${document._path || ""}`;
241
+ const pathKey = `${document.locale || ""}:${document.path || ""}`;
274
242
  if (pathsByLocale.has(pathKey)) {
275
243
  const previous = pathsByLocale.get(pathKey);
276
244
  return fail(createContentError(
277
245
  "DUPLICATE_LOCALIZED_PATH",
278
- document._file || document._id,
279
- `duplicate localized path "${document._path}" for locale "${document._locale || "default"}"`,
280
- `conflicts with ${previous._file || previous._id}`
246
+ document.file?.path || document.id,
247
+ `duplicate localized path "${document.path}" for locale "${document.locale || "default"}"`,
248
+ `conflicts with ${previous.file?.path || previous.id}`
281
249
  ));
282
250
  }
283
251
  pathsByLocale.set(pathKey, document);
@@ -286,7 +254,7 @@ export const validateContentGraph = (contents, config) => {
286
254
  if (typeof document.ref !== "undefined" && (typeof document.ref !== "string" || !document.ref.length)) {
287
255
  return fail(createContentError(
288
256
  "INVALID_REF_VALUE",
289
- document._file || document._id,
257
+ document.file?.path || document.id,
290
258
  "ref must be a non-empty string"
291
259
  ));
292
260
  }
@@ -295,39 +263,45 @@ export const validateContentGraph = (contents, config) => {
295
263
  continue;
296
264
  }
297
265
  const previous = refsByValue.get(ref);
298
- if (previous && (previous._canonicalKey || getCanonicalContentId(previous, locales)) !== (document._canonicalKey || getCanonicalContentId(document, locales))) {
266
+ if (previous && (previous.canonicalKey || getCanonicalContentId(previous, locales)) !== (document.canonicalKey || getCanonicalContentId(document, locales))) {
299
267
  return fail(createContentError(
300
268
  "CONFLICTING_REFS",
301
- document._file || document._id,
269
+ document.file?.path || document.id,
302
270
  `duplicate ref "${ref}"`,
303
- `conflicts with ${previous._file || previous._id}`
271
+ `conflicts with ${previous.file?.path || previous.id}`
304
272
  ));
305
273
  }
306
274
  refsByValue.set(ref, document);
307
275
  }
308
276
  for (const document of docs) {
309
- if (!document._collection) {
277
+ if (!document.collection) {
310
278
  continue;
311
279
  }
312
- const collection = getCollectionConfig(document._collection, config.collections);
280
+ const collection = getCollectionConfig(document.collection, config.collections);
281
+ const references = collection?.references;
313
282
  const schema = collection?.schema;
314
- if (!schema) {
283
+ if (!schema && !references) {
315
284
  continue;
316
285
  }
317
286
  const issues = [];
318
- collectReferenceIssues(schema, document, (value, collection2) => {
287
+ const resolveReference = (value, collection2) => {
319
288
  const canonicalId = referenceTargets.get(normalizeReferenceValue(value));
320
289
  if (!canonicalId) {
321
290
  return false;
322
291
  }
323
292
  return collection2 ? targetCollections.get(canonicalId)?.has(collection2) === true : true;
324
- }, issues);
293
+ };
294
+ if (schema) {
295
+ collectReferenceIssues(schema, document, resolveReference, issues);
296
+ } else {
297
+ collectDerivedReferenceIssues(references, document, resolveReference, issues);
298
+ }
325
299
  if (!issues.length) {
326
300
  continue;
327
301
  }
328
302
  const error = createContentError(
329
303
  "SCHEMA_VALIDATION_FAILED",
330
- document._file || document._id,
304
+ document.file?.path || document.id,
331
305
  "unresolved content references",
332
306
  issues.join("; ")
333
307
  );
@@ -1,11 +1,21 @@
1
- import { type ContentProvider } from '../public/provider';
2
- export interface SaasProviderFixtureContractSuiteOptions {
1
+ import { type ContentProvider, type ContentProviderCapabilities } from '../public/provider';
2
+ export interface ProviderContractSuiteOptions {
3
3
  name: string;
4
4
  expectedProviderName: string;
5
5
  loadProvider: () => Promise<ContentProvider>;
6
6
  createEvent: () => any;
7
+ /**
8
+ * The capabilities the provider under test declares. Each capability block
9
+ * runs its positive assertions only when the matching flag is `true`.
10
+ * Operation capabilities (`routeBackedCollections`, `navigation`,
11
+ * `surroundings`, `searchSections`, `sitemap`) and the query sub-capabilities
12
+ * (`query.limit`/`skip`/`count`) additionally assert the typed provider error
13
+ * when declared `false`; descriptive flags (`dataCollections`,
14
+ * `localizedRoutes`, `translatedSlugs`) only gate their positive block.
15
+ */
16
+ expectedCapabilities: ContentProviderCapabilities;
7
17
  collectNavPaths?: (items: Array<{
8
- _path?: string;
18
+ unprefixedPath?: string;
9
19
  children?: any[];
10
20
  }>) => string[];
11
21
  }
@@ -18,7 +28,7 @@ export interface AuthorDependencyContractOptions {
18
28
  key: string;
19
29
  }>;
20
30
  }
21
- export declare const runSaasProviderFixtureContractSuite: ({ name, expectedProviderName, loadProvider, createEvent, collectNavPaths }: SaasProviderFixtureContractSuiteOptions) => void;
31
+ export declare const runProviderContractSuite: ({ name, expectedProviderName, loadProvider, createEvent, expectedCapabilities, collectNavPaths }: ProviderContractSuiteOptions) => void;
22
32
  export declare const createAuthorDependencyContractProvider: () => {
23
33
  fixture: import("./provider-fixture").ProviderFixture;
24
34
  provider: ContentProvider & {
@@ -1,42 +1,53 @@
1
1
  import { expect, test } from "vitest";
2
- import { contentProviderResultMarker } from "../public/provider";
2
+ import { contentProviderResultMarker, toContentProviderNavigationQuery, toContentProviderQuery } from "../public/provider";
3
3
  import { createAuthorDependencyProviderFixture, createFixtureContentProvider, createProviderFixtureEvent, getProviderFixtureCacheHint } from "./provider-fixture.js";
4
4
  const defaultCollectNavPaths = (items) => items.flatMap((item) => [
5
- item._path,
5
+ item.unprefixedPath,
6
6
  ...defaultCollectNavPaths(item.children || [])
7
7
  ].filter(Boolean));
8
8
  const unwrapProviderResult = (value) => value && typeof value === "object" && value[contentProviderResultMarker] ? value.data : value;
9
- export const runSaasProviderFixtureContractSuite = ({
9
+ const expectUnsupportedOperation = (run, operation) => expect(Promise.resolve().then(run)).rejects.toMatchObject({
10
+ statusCode: 400,
11
+ statusMessage: "unsupported_provider_operation",
12
+ data: { code: "unsupported_provider_operation", operation }
13
+ });
14
+ const expectUnsupportedQueryShape = (run, field) => expect(Promise.resolve().then(run)).rejects.toMatchObject({
15
+ statusCode: 400,
16
+ statusMessage: "unsupported_query_shape",
17
+ data: { code: "unsupported_query_shape", field }
18
+ });
19
+ export const runProviderContractSuite = ({
10
20
  name,
11
21
  expectedProviderName,
12
22
  loadProvider,
13
23
  createEvent,
24
+ expectedCapabilities,
14
25
  collectNavPaths = defaultCollectNavPaths
15
26
  }) => {
27
+ const caps = expectedCapabilities;
16
28
  test(`${name} exposes explicit capabilities`, async () => {
17
29
  const provider = await loadProvider();
18
30
  expect(provider.name).toBe(expectedProviderName);
19
31
  expect(provider.capabilities).toMatchObject({
20
- routeBackedCollections: true,
21
- dataCollections: true,
22
- localizedRoutes: true,
23
- translatedSlugs: true,
24
- navigation: true,
25
- surroundings: true,
26
- searchSections: true,
27
- sitemap: true,
32
+ ...caps,
28
33
  query: {
29
- limit: true,
30
- skip: true,
31
- count: true
34
+ limit: caps.query.limit,
35
+ skip: caps.query.skip,
36
+ count: caps.query.count
32
37
  }
33
38
  });
34
- expect(provider.capabilities.query.operators).toContain("$eq");
35
- expect(provider.capabilities.query.operators).toContain("$contains");
39
+ expect(provider.capabilities.query.operators).toEqual(
40
+ expect.arrayContaining(caps.query.operators)
41
+ );
36
42
  });
37
43
  test(`${name} returns complete localized page and route metadata`, async () => {
38
44
  const provider = await loadProvider();
39
45
  const event = createEvent();
46
+ if (!caps.routeBackedCollections) {
47
+ await expectUnsupportedOperation(() => provider.page?.(event, "docs", "/de/dokumentation/einstieg"), "route-backed pages");
48
+ await expectUnsupportedOperation(() => provider.routeMeta?.(event, "docs", "/de/dokumentation/einstieg", { locale: "de" }), "route metadata");
49
+ return;
50
+ }
40
51
  const page = unwrapProviderResult(await provider.page?.(event, "docs", "/de/dokumentation/einstieg"));
41
52
  expect(page).toMatchObject({
42
53
  title: "Einstieg",
@@ -61,43 +72,62 @@ export const runSaasProviderFixtureContractSuite = ({
61
72
  locale: "de"
62
73
  })
63
74
  });
64
- });
65
- test(`${name} supports localized page misses without fallback routes`, async () => {
66
- const provider = await loadProvider();
67
75
  const miss = unwrapProviderResult(await provider.page?.(createEvent(), "docs", "/de/dokumentation/not-found"));
68
76
  expect(miss).toBeNull();
69
77
  });
70
78
  test(`${name} supports list queries with locale, ordering, limit, count, and projection`, async () => {
71
79
  const provider = await loadProvider();
72
- const response = unwrapProviderResult(await provider.query(createEvent(), {
73
- collection: "posts",
74
- resolveLocale: { locale: "de", fallback: false },
75
- sort: [{ date: -1 }],
76
- limit: 1,
77
- only: ["title", "_path", "_locale"]
78
- }));
79
- expect(response.result).toEqual([
80
- {
81
- title: "Mehrsprachiges Onboarding",
82
- _path: "/magazin/mehrsprachiges-onboarding",
83
- _locale: "de"
84
- }
85
- ]);
86
- expect(typeof response.total).toBe("number");
87
- const countResponse = unwrapProviderResult(await provider.query(createEvent(), {
88
- collection: "posts",
89
- resolveLocale: { locale: "de", fallback: false },
90
- count: true
91
- }));
92
- expect(countResponse).toEqual({ result: 1 });
80
+ if (!caps.query.limit) {
81
+ await expectUnsupportedQueryShape(() => provider.query(createEvent(), toContentProviderQuery({
82
+ collection: "posts",
83
+ resolveLocale: { locale: "de", fallback: false },
84
+ limit: 1
85
+ })), "limit");
86
+ } else {
87
+ const response = unwrapProviderResult(await provider.query(createEvent(), toContentProviderQuery({
88
+ collection: "posts",
89
+ resolveLocale: { locale: "de", fallback: false },
90
+ sort: [{ date: -1 }],
91
+ limit: 1,
92
+ only: ["title", "path", "locale"]
93
+ })));
94
+ expect(response.result).toEqual([
95
+ {
96
+ title: "Mehrsprachiges Onboarding",
97
+ path: "/magazin/mehrsprachiges-onboarding",
98
+ locale: "de"
99
+ }
100
+ ]);
101
+ expect(typeof response.total).toBe("number");
102
+ }
103
+ if (!caps.query.count) {
104
+ await expectUnsupportedQueryShape(() => provider.query(createEvent(), toContentProviderQuery({
105
+ collection: "posts",
106
+ resolveLocale: { locale: "de", fallback: false },
107
+ count: true
108
+ })), "count");
109
+ } else {
110
+ const countResponse = unwrapProviderResult(await provider.query(createEvent(), toContentProviderQuery({
111
+ collection: "posts",
112
+ resolveLocale: { locale: "de", fallback: false },
113
+ count: true
114
+ })));
115
+ expect(countResponse).toEqual({ result: 1 });
116
+ }
93
117
  });
94
- test(`${name} produces navigation, surroundings, search sections, and sitemap entries`, async () => {
118
+ test(`${name} produces navigation entries`, async () => {
95
119
  const provider = await loadProvider();
96
120
  const event = createEvent();
121
+ if (!caps.navigation) {
122
+ await expectUnsupportedOperation(() => provider.navigation?.(event, "docs", { locale: "de" }), "navigation");
123
+ const wire = toContentProviderNavigationQuery({ where: { locale: "de" } });
124
+ await expectUnsupportedOperation(() => provider.navigationQuery?.(event, wire.query, wire.options), "navigation");
125
+ return;
126
+ }
97
127
  const nav = unwrapProviderResult(await provider.navigation?.(event, "docs", { locale: "de" }));
98
128
  expect(collectNavPaths(nav || [])).toContain("/dokumentation/einstieg");
99
129
  expect(nav?.[0]).toEqual(expect.objectContaining({
100
- _locale: "de",
130
+ locale: "de",
101
131
  ref: expect.any(String),
102
132
  stableId: expect.any(String)
103
133
  }));
@@ -112,27 +142,54 @@ export const runSaasProviderFixtureContractSuite = ({
112
142
  title: expect.any(String),
113
143
  description: expect.any(String)
114
144
  }));
115
- const globalNav = unwrapProviderResult(await provider.navigationQuery?.(event, { where: { _locale: "de" } }));
145
+ const globalNavWire = toContentProviderNavigationQuery({ where: { locale: "de" } });
146
+ const globalNav = unwrapProviderResult(await provider.navigationQuery?.(event, globalNavWire.query, globalNavWire.options));
116
147
  const globalNavPaths = collectNavPaths(globalNav || []);
117
148
  expect(globalNavPaths).toContain("/dokumentation/einstieg");
118
149
  expect(globalNavPaths.length).toBeGreaterThan(0);
119
- const resolvedLocaleNav = unwrapProviderResult(await provider.navigationQuery?.(event, {
150
+ const resolvedLocaleNavWire = toContentProviderNavigationQuery({
120
151
  collection: "docs",
121
152
  resolveLocale: { locale: "de", fallback: false, exact: true }
122
- }));
153
+ });
154
+ const resolvedLocaleNav = unwrapProviderResult(await provider.navigationQuery?.(event, resolvedLocaleNavWire.query, resolvedLocaleNavWire.options));
123
155
  expect(collectNavPaths(resolvedLocaleNav || [])).toContain("/dokumentation/einstieg");
156
+ expect(JSON.stringify(nav)).not.toContain("Draft Roadmap");
157
+ });
158
+ test(`${name} produces surroundings entries`, async () => {
159
+ const provider = await loadProvider();
160
+ const event = createEvent();
161
+ if (!caps.surroundings) {
162
+ await expectUnsupportedOperation(() => provider.surroundings?.(event, "docs", "/de/dokumentation/einstieg/installation", { locale: "de" }), "surroundings");
163
+ return;
164
+ }
124
165
  const surround = unwrapProviderResult(await provider.surroundings?.(event, "docs", "/de/dokumentation/einstieg/installation", { locale: "de" }));
125
166
  expect(surround).toHaveLength(2);
126
- expect(surround?.map((item) => item?._path)).toContain("/dokumentation/einstieg/alltag");
167
+ expect(surround?.map((item) => item?.unprefixedPath)).toContain("/dokumentation/einstieg/alltag");
168
+ });
169
+ test(`${name} produces search sections`, async () => {
170
+ const provider = await loadProvider();
171
+ const event = createEvent();
172
+ if (!caps.searchSections) {
173
+ await expectUnsupportedOperation(() => provider.searchSections?.(event, "docs", { locale: "de" }), "search sections");
174
+ return;
175
+ }
127
176
  const sections = unwrapProviderResult(await provider.searchSections?.(event, "docs", { locale: "de" }));
128
177
  expect(sections?.some((section) => section.id.startsWith("/de/dokumentation/einstieg") || section.id.startsWith("/dokumentation/einstieg"))).toBe(true);
129
178
  expect(JSON.stringify(sections)).not.toContain("Draft Roadmap");
179
+ });
180
+ test(`${name} produces sitemap entries`, async () => {
181
+ const provider = await loadProvider();
182
+ const event = createEvent();
183
+ if (!caps.sitemap) {
184
+ await expectUnsupportedOperation(() => provider.sitemapEntries?.(event, { include: ["docs"] }), "sitemap entries");
185
+ return;
186
+ }
130
187
  const sitemap = unwrapProviderResult(await provider.sitemapEntries?.(event, { include: ["docs"] }));
131
188
  expect(sitemap?.some((entry) => entry.loc.endsWith("/de/dokumentation/einstieg"))).toBe(true);
132
189
  expect(JSON.stringify(sitemap)).not.toContain("/docs/draft-roadmap");
133
- expect(JSON.stringify(nav)).not.toContain("Draft Roadmap");
134
190
  });
135
191
  test(`${name} emits cache hints for rendered content`, async () => {
192
+ if (!caps.routeBackedCollections) return;
136
193
  const provider = await loadProvider();
137
194
  const event = createEvent();
138
195
  await provider.page?.(event, "posts", "/de/magazin/mehrsprachiges-onboarding");
@@ -154,38 +211,34 @@ export const runSaasProviderFixtureContractSuite = ({
154
211
  }
155
212
  });
156
213
  test(`${name} supports data-only collection reads and rejects data-only sitemap access`, async () => {
214
+ if (!caps.dataCollections) return;
157
215
  const provider = await loadProvider();
158
- const response = unwrapProviderResult(await provider.query(createEvent(), {
216
+ const response = unwrapProviderResult(await provider.query(createEvent(), toContentProviderQuery({
159
217
  collection: "versions",
160
218
  resolveLocale: { locale: "en", fallback: false },
161
219
  sort: [{ date: -1 }]
162
- }));
220
+ })));
163
221
  expect(response.result[0]?.title).toBe("Launch readiness");
164
- await expect(provider.sitemapEntries?.(createEvent(), { include: ["versions"] })).rejects.toMatchObject({
165
- statusCode: 400,
166
- statusMessage: "data_collection_sitemap_access",
167
- data: {
168
- code: "data_collection_sitemap_access",
169
- collection: "versions"
170
- }
171
- });
222
+ if (caps.sitemap) {
223
+ await expect(provider.sitemapEntries?.(createEvent(), { include: ["versions"] })).rejects.toMatchObject({
224
+ statusCode: 400,
225
+ statusMessage: "data_collection_sitemap_access",
226
+ data: {
227
+ code: "data_collection_sitemap_access",
228
+ collection: "versions"
229
+ }
230
+ });
231
+ }
172
232
  });
173
233
  test(`${name} fails loudly for unsupported query operators and unknown collections`, async () => {
174
234
  const provider = await loadProvider();
175
- await expect(provider.query(createEvent(), {
235
+ expect(() => toContentProviderQuery({
176
236
  collection: "posts",
177
237
  where: { title: { $near: "launch" } }
178
- })).rejects.toMatchObject({
179
- statusCode: 400,
180
- statusMessage: "unsupported_query_operator",
181
- data: {
182
- code: "unsupported_query_operator",
183
- operator: "$near"
184
- }
185
- });
186
- await expect(provider.query(createEvent(), {
238
+ })).toThrow(/Unsupported content query operator: \$near/);
239
+ await expect(provider.query(createEvent(), toContentProviderQuery({
187
240
  collection: "missing"
188
- })).rejects.toMatchObject({
241
+ }))).rejects.toMatchObject({
189
242
  statusCode: 400,
190
243
  statusMessage: "unknown_collection",
191
244
  data: {
@@ -63,5 +63,5 @@ export declare const createProviderFixtureEvent: (options?: ProviderFixtureEvent
63
63
  export declare const createFixtureContentProvider: (fixture: ProviderFixture, name?: string) => ContentProvider & {
64
64
  cache: ProviderFixtureCacheState;
65
65
  };
66
- export declare const createSaasProviderFixture: () => ProviderFixture;
66
+ export declare const createDefaultProviderFixture: () => ProviderFixture;
67
67
  export declare const createAuthorDependencyProviderFixture: () => ProviderFixture;