@lupinum/ginko-content 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "Ginko Content release compatibility",
3
3
  "releaseStack": {
4
- "@lupinum/ginko-content": "0.2.0",
4
+ "@lupinum/ginko-content": "0.2.1",
5
5
  "@lupinum/ginko-cms": "0.1.2",
6
6
  "@lupinum/ginko-cms-convex": "0.1.1",
7
7
  "@lupinum/ginko-cms-contract": "0.1.1"
package/dist/module.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lupinum/ginko-content",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "configKey": "content",
5
5
  "compatibility": {
6
6
  "nuxt": ">=4.0.0"
package/dist/module.mjs CHANGED
@@ -31,7 +31,7 @@ export { agentMetadataFields, defineAgentAppPage, defineAgentMarkdownPolicy, def
31
31
  import { collectTopLevelReferenceFieldsByTarget } from '../dist/core/references/schema';
32
32
 
33
33
  const name = "@lupinum/ginko-content";
34
- const version = "0.2.0";
34
+ const version = "0.2.1";
35
35
 
36
36
  const CONFIG_FILES = [
37
37
  "content.config.ts",
@@ -274,7 +274,9 @@ const generatedContentServerValueNames = [
274
274
  "PROVIDER_QUERY_VERSION",
275
275
  "toContentProviderQuery",
276
276
  "toContentProviderNavigationQuery",
277
- "createContentProviderError"
277
+ "createContentProviderError",
278
+ "normalizeProviderDocument",
279
+ "shapeProviderDocument"
278
280
  ];
279
281
  const generatedContentServerTypeSpecs = [
280
282
  { local: "ContentCacheAdapter", exported: "ContentCacheAdapter" },
@@ -288,6 +290,8 @@ const generatedContentServerTypeSpecs = [
288
290
  { local: "ContentProviderResult<T>", exported: "ContentProviderResult<T>" },
289
291
  { local: "MaybeContentProviderResult<T>", exported: "MaybeContentProviderResult<T>" },
290
292
  { local: "ContentProviderErrorCode", exported: "ContentProviderErrorCode" },
293
+ { local: "ProviderDocumentInput", exported: "ProviderDocumentInput" },
294
+ { local: "ShapeProviderDocumentOptions", exported: "ShapeProviderDocumentOptions" },
291
295
  { local: "VercelContentCacheOptions", exported: "VercelContentCacheOptions" }
292
296
  ];
293
297
  const registerRuntimeImports = (resolveRuntimeModule) => {
@@ -12,6 +12,8 @@ export type { ContentProviderQuery, ContentProviderNavigationOptions, ContentQue
12
12
  export { PROVIDER_QUERY_VERSION, toContentProviderQuery, toContentProviderNavigationQuery } from './provider-query';
13
13
  export { createContentProviderError } from './provider-errors';
14
14
  export type { ContentProviderErrorCode } from './provider-errors';
15
+ export { normalizeProviderDocument, shapeProviderDocument } from '../runtime/server/provider-document.js';
16
+ export type { ProviderDocumentInput, ShapeProviderDocumentOptions } from '../runtime/server/provider-document.js';
15
17
  export declare const contentProviderResultMarker = "__ginkoContentProviderResult";
16
18
  export interface ContentProviderResult<T = unknown> {
17
19
  readonly [contentProviderResultMarker]: true;
@@ -1,5 +1,6 @@
1
1
  export { PROVIDER_QUERY_VERSION, toContentProviderQuery, toContentProviderNavigationQuery } from "./provider-query.js";
2
2
  export { createContentProviderError } from "./provider-errors.js";
3
+ export { normalizeProviderDocument, shapeProviderDocument } from "../runtime/server/provider-document.js";
3
4
  export const contentProviderResultMarker = "__ginkoContentProviderResult";
4
5
  export const withContentCache = (data, cache) => ({
5
6
  [contentProviderResultMarker]: true,
@@ -28,6 +28,23 @@ export interface AuthorDependencyContractOptions {
28
28
  key: string;
29
29
  }>;
30
30
  }
31
+ export declare const LEGACY_PROVIDER_ENVELOPE_FIELDS: readonly ["_id", "_collection", "_locale", "_path", "_canonicalKey", "_type", "_draft", "_partial", "_source", "_file", "_stem", "_dir", "_basename", "_extension", "_requestedLocale", "_resolvedLocale", "_availableLocales", "_variantPaths", "_requestedPath", "_requestedRef", "_requestedRoute", "_resolvedRefs", "_fallback", "_empty", "_navigation", "_navigationPath", "_navigationKind", "_key", "_output"];
32
+ export declare const unwrapProviderContractResult: <T>(value: T) => T extends {
33
+ data: infer Data;
34
+ } ? Data : T;
35
+ /**
36
+ * Assert that a provider operation rejects with the typed
37
+ * `unsupported_provider_operation` error when its capability is declared false.
38
+ * The `run` callback may return a promise or throw synchronously.
39
+ */
40
+ export declare const expectUnsupportedProviderOperation: (run: () => Promise<unknown> | undefined, operation: string) => Promise<void>;
41
+ export declare const expectUnsupportedProviderQueryShape: (run: () => Promise<unknown> | undefined, field: string) => Promise<void>;
42
+ export declare const expectNoLegacyProviderEnvelopeFields: (value: unknown) => void;
43
+ export declare const expectProviderDocumentEnvelope: (document: unknown, options?: {
44
+ locale?: string;
45
+ defaultLocale?: string;
46
+ }) => void;
47
+ export declare const expectProviderCapabilities: (provider: ContentProvider, expected: ContentProviderCapabilities) => void;
31
48
  export declare const runProviderContractSuite: ({ name, expectedProviderName, loadProvider, createEvent, expectedCapabilities, collectNavPaths }: ProviderContractSuiteOptions) => void;
32
49
  export declare const createAuthorDependencyContractProvider: () => {
33
50
  fixture: import("./provider-fixture").ProviderFixture;
@@ -5,17 +5,133 @@ const defaultCollectNavPaths = (items) => items.flatMap((item) => [
5
5
  item.unprefixedPath,
6
6
  ...defaultCollectNavPaths(item.children || [])
7
7
  ].filter(Boolean));
8
- const unwrapProviderResult = (value) => value && typeof value === "object" && value[contentProviderResultMarker] ? value.data : value;
9
- const expectUnsupportedOperation = (run, operation) => expect(Promise.resolve().then(run)).rejects.toMatchObject({
8
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
9
+ export const LEGACY_PROVIDER_ENVELOPE_FIELDS = [
10
+ "_id",
11
+ "_collection",
12
+ "_locale",
13
+ "_path",
14
+ "_canonicalKey",
15
+ "_type",
16
+ "_draft",
17
+ "_partial",
18
+ "_source",
19
+ "_file",
20
+ "_stem",
21
+ "_dir",
22
+ "_basename",
23
+ "_extension",
24
+ "_requestedLocale",
25
+ "_resolvedLocale",
26
+ "_availableLocales",
27
+ "_variantPaths",
28
+ "_requestedPath",
29
+ "_requestedRef",
30
+ "_requestedRoute",
31
+ "_resolvedRefs",
32
+ "_fallback",
33
+ "_empty",
34
+ "_navigation",
35
+ "_navigationPath",
36
+ "_navigationKind",
37
+ "_key",
38
+ "_output"
39
+ ];
40
+ const legacyProviderEnvelopeFieldSet = new Set(LEGACY_PROVIDER_ENVELOPE_FIELDS);
41
+ const collectLegacyProviderEnvelopeFields = (value, path = "$", seen = /* @__PURE__ */ new WeakSet(), matches = []) => {
42
+ if (!value || typeof value !== "object") {
43
+ return matches;
44
+ }
45
+ if (seen.has(value)) {
46
+ return matches;
47
+ }
48
+ seen.add(value);
49
+ if (Array.isArray(value)) {
50
+ value.forEach((item, index) => {
51
+ collectLegacyProviderEnvelopeFields(item, `${path}[${index}]`, seen, matches);
52
+ });
53
+ return matches;
54
+ }
55
+ for (const [key, item] of Object.entries(value)) {
56
+ const itemPath = `${path}.${key}`;
57
+ if (legacyProviderEnvelopeFieldSet.has(key)) {
58
+ matches.push(itemPath);
59
+ }
60
+ collectLegacyProviderEnvelopeFields(item, itemPath, seen, matches);
61
+ }
62
+ return matches;
63
+ };
64
+ export const unwrapProviderContractResult = (value) => value && typeof value === "object" && value[contentProviderResultMarker] ? value.data : value;
65
+ export const expectUnsupportedProviderOperation = (run, operation) => expect(Promise.resolve().then(run)).rejects.toMatchObject({
10
66
  statusCode: 400,
11
67
  statusMessage: "unsupported_provider_operation",
12
68
  data: { code: "unsupported_provider_operation", operation }
13
69
  });
14
- const expectUnsupportedQueryShape = (run, field) => expect(Promise.resolve().then(run)).rejects.toMatchObject({
70
+ export const expectUnsupportedProviderQueryShape = (run, field) => expect(Promise.resolve().then(run)).rejects.toMatchObject({
15
71
  statusCode: 400,
16
72
  statusMessage: "unsupported_query_shape",
17
73
  data: { code: "unsupported_query_shape", field }
18
74
  });
75
+ export const expectNoLegacyProviderEnvelopeFields = (value) => {
76
+ expect(collectLegacyProviderEnvelopeFields(value)).toEqual([]);
77
+ };
78
+ export const expectProviderDocumentEnvelope = (document, options = {}) => {
79
+ expectNoLegacyProviderEnvelopeFields(document);
80
+ expect(isRecord(document)).toBe(true);
81
+ const record = document;
82
+ expect(record).toEqual(expect.objectContaining({
83
+ id: expect.any(String),
84
+ collection: expect.any(String),
85
+ type: expect.any(String),
86
+ path: expect.any(String),
87
+ locale: expect.any(String),
88
+ canonicalKey: expect.any(String),
89
+ unprefixedPath: expect.any(String),
90
+ variants: expect.any(Array),
91
+ localePaths: expect.any(Object),
92
+ stem: expect.any(String)
93
+ }));
94
+ expect(Object.prototype.hasOwnProperty.call(record, "body")).toBe(true);
95
+ expect(record.resolved).toEqual(expect.objectContaining({
96
+ locale: expect.any(String),
97
+ fallback: expect.any(Boolean),
98
+ path: expect.any(String)
99
+ }));
100
+ if (options.locale !== void 0) {
101
+ expect(record.locale).toBe(options.locale);
102
+ }
103
+ if (options.defaultLocale !== void 0) {
104
+ expect(record.defaultLocale).toBe(options.defaultLocale);
105
+ }
106
+ if (record.file !== void 0) {
107
+ expect(record.file).toEqual(expect.objectContaining({
108
+ path: expect.any(String),
109
+ extension: expect.any(String)
110
+ }));
111
+ }
112
+ if (record.extension !== void 0) {
113
+ expect(record.extension).toEqual(expect.any(String));
114
+ }
115
+ if (record.title !== void 0) {
116
+ expect(record.title).toEqual(expect.any(String));
117
+ }
118
+ if (record.description !== void 0) {
119
+ expect(record.description).toEqual(expect.any(String));
120
+ }
121
+ };
122
+ export const expectProviderCapabilities = (provider, expected) => {
123
+ expect(provider.capabilities).toMatchObject({
124
+ ...expected,
125
+ query: {
126
+ limit: expected.query.limit,
127
+ skip: expected.query.skip,
128
+ count: expected.query.count
129
+ }
130
+ });
131
+ expect(provider.capabilities.query.operators).toEqual(
132
+ expect.arrayContaining(expected.query.operators)
133
+ );
134
+ };
19
135
  export const runProviderContractSuite = ({
20
136
  name,
21
137
  expectedProviderName,
@@ -28,27 +144,18 @@ export const runProviderContractSuite = ({
28
144
  test(`${name} exposes explicit capabilities`, async () => {
29
145
  const provider = await loadProvider();
30
146
  expect(provider.name).toBe(expectedProviderName);
31
- expect(provider.capabilities).toMatchObject({
32
- ...caps,
33
- query: {
34
- limit: caps.query.limit,
35
- skip: caps.query.skip,
36
- count: caps.query.count
37
- }
38
- });
39
- expect(provider.capabilities.query.operators).toEqual(
40
- expect.arrayContaining(caps.query.operators)
41
- );
147
+ expectProviderCapabilities(provider, caps);
42
148
  });
43
149
  test(`${name} returns complete localized page and route metadata`, async () => {
44
150
  const provider = await loadProvider();
45
151
  const event = createEvent();
46
152
  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");
153
+ await expectUnsupportedProviderOperation(() => provider.page?.(event, "docs", "/de/dokumentation/einstieg"), "route-backed pages");
154
+ await expectUnsupportedProviderOperation(() => provider.routeMeta?.(event, "docs", "/de/dokumentation/einstieg", { locale: "de" }), "route metadata");
49
155
  return;
50
156
  }
51
- const page = unwrapProviderResult(await provider.page?.(event, "docs", "/de/dokumentation/einstieg"));
157
+ const page = unwrapProviderContractResult(await provider.page?.(event, "docs", "/de/dokumentation/einstieg"));
158
+ expectNoLegacyProviderEnvelopeFields(page);
52
159
  expect(page).toMatchObject({
53
160
  title: "Einstieg",
54
161
  locale: "de",
@@ -62,7 +169,7 @@ export const runProviderContractSuite = ({
62
169
  });
63
170
  expect(page?.variants).toEqual(expect.any(Array));
64
171
  expect(page?.localePaths).toEqual(expect.any(Object));
65
- const routeMeta = unwrapProviderResult(await provider.routeMeta?.(event, "docs", "/de/dokumentation/einstieg", { locale: "de" }));
172
+ const routeMeta = unwrapProviderContractResult(await provider.routeMeta?.(event, "docs", "/de/dokumentation/einstieg", { locale: "de" }));
66
173
  expect(routeMeta).toMatchObject({
67
174
  locale: "de",
68
175
  path: expect.stringMatching(/\/dokumentation\/einstieg$/),
@@ -72,19 +179,19 @@ export const runProviderContractSuite = ({
72
179
  locale: "de"
73
180
  })
74
181
  });
75
- const miss = unwrapProviderResult(await provider.page?.(createEvent(), "docs", "/de/dokumentation/not-found"));
182
+ const miss = unwrapProviderContractResult(await provider.page?.(createEvent(), "docs", "/de/dokumentation/not-found"));
76
183
  expect(miss).toBeNull();
77
184
  });
78
185
  test(`${name} supports list queries with locale, ordering, limit, count, and projection`, async () => {
79
186
  const provider = await loadProvider();
80
187
  if (!caps.query.limit) {
81
- await expectUnsupportedQueryShape(() => provider.query(createEvent(), toContentProviderQuery({
188
+ await expectUnsupportedProviderQueryShape(() => provider.query(createEvent(), toContentProviderQuery({
82
189
  collection: "posts",
83
190
  resolveLocale: { locale: "de", fallback: false },
84
191
  limit: 1
85
192
  })), "limit");
86
193
  } else {
87
- const response = unwrapProviderResult(await provider.query(createEvent(), toContentProviderQuery({
194
+ const response = unwrapProviderContractResult(await provider.query(createEvent(), toContentProviderQuery({
88
195
  collection: "posts",
89
196
  resolveLocale: { locale: "de", fallback: false },
90
197
  sort: [{ date: -1 }],
@@ -101,13 +208,13 @@ export const runProviderContractSuite = ({
101
208
  expect(typeof response.total).toBe("number");
102
209
  }
103
210
  if (!caps.query.count) {
104
- await expectUnsupportedQueryShape(() => provider.query(createEvent(), toContentProviderQuery({
211
+ await expectUnsupportedProviderQueryShape(() => provider.query(createEvent(), toContentProviderQuery({
105
212
  collection: "posts",
106
213
  resolveLocale: { locale: "de", fallback: false },
107
214
  count: true
108
215
  })), "count");
109
216
  } else {
110
- const countResponse = unwrapProviderResult(await provider.query(createEvent(), toContentProviderQuery({
217
+ const countResponse = unwrapProviderContractResult(await provider.query(createEvent(), toContentProviderQuery({
111
218
  collection: "posts",
112
219
  resolveLocale: { locale: "de", fallback: false },
113
220
  count: true
@@ -119,12 +226,12 @@ export const runProviderContractSuite = ({
119
226
  const provider = await loadProvider();
120
227
  const event = createEvent();
121
228
  if (!caps.navigation) {
122
- await expectUnsupportedOperation(() => provider.navigation?.(event, "docs", { locale: "de" }), "navigation");
229
+ await expectUnsupportedProviderOperation(() => provider.navigation?.(event, "docs", { locale: "de" }), "navigation");
123
230
  const wire = toContentProviderNavigationQuery({ where: { locale: "de" } });
124
- await expectUnsupportedOperation(() => provider.navigationQuery?.(event, wire.query, wire.options), "navigation");
231
+ await expectUnsupportedProviderOperation(() => provider.navigationQuery?.(event, wire.query, wire.options), "navigation");
125
232
  return;
126
233
  }
127
- const nav = unwrapProviderResult(await provider.navigation?.(event, "docs", { locale: "de" }));
234
+ const nav = unwrapProviderContractResult(await provider.navigation?.(event, "docs", { locale: "de" }));
128
235
  expect(collectNavPaths(nav || [])).toContain("/dokumentation/einstieg");
129
236
  expect(nav?.[0]).toEqual(expect.objectContaining({
130
237
  locale: "de",
@@ -137,13 +244,13 @@ export const runProviderContractSuite = ({
137
244
  expect(navPaths.indexOf("/dokumentation/einstieg")).toBeLessThan(
138
245
  navPaths.indexOf("/dokumentation/einstieg/installation")
139
246
  );
140
- const fieldNav = unwrapProviderResult(await provider.navigation?.(event, "docs", ["description"]));
247
+ const fieldNav = unwrapProviderContractResult(await provider.navigation?.(event, "docs", ["description"]));
141
248
  expect(fieldNav?.[0]).toEqual(expect.objectContaining({
142
249
  title: expect.any(String),
143
250
  description: expect.any(String)
144
251
  }));
145
252
  const globalNavWire = toContentProviderNavigationQuery({ where: { locale: "de" } });
146
- const globalNav = unwrapProviderResult(await provider.navigationQuery?.(event, globalNavWire.query, globalNavWire.options));
253
+ const globalNav = unwrapProviderContractResult(await provider.navigationQuery?.(event, globalNavWire.query, globalNavWire.options));
147
254
  const globalNavPaths = collectNavPaths(globalNav || []);
148
255
  expect(globalNavPaths).toContain("/dokumentation/einstieg");
149
256
  expect(globalNavPaths.length).toBeGreaterThan(0);
@@ -151,7 +258,7 @@ export const runProviderContractSuite = ({
151
258
  collection: "docs",
152
259
  resolveLocale: { locale: "de", fallback: false, exact: true }
153
260
  });
154
- const resolvedLocaleNav = unwrapProviderResult(await provider.navigationQuery?.(event, resolvedLocaleNavWire.query, resolvedLocaleNavWire.options));
261
+ const resolvedLocaleNav = unwrapProviderContractResult(await provider.navigationQuery?.(event, resolvedLocaleNavWire.query, resolvedLocaleNavWire.options));
155
262
  expect(collectNavPaths(resolvedLocaleNav || [])).toContain("/dokumentation/einstieg");
156
263
  expect(JSON.stringify(nav)).not.toContain("Draft Roadmap");
157
264
  });
@@ -159,10 +266,10 @@ export const runProviderContractSuite = ({
159
266
  const provider = await loadProvider();
160
267
  const event = createEvent();
161
268
  if (!caps.surroundings) {
162
- await expectUnsupportedOperation(() => provider.surroundings?.(event, "docs", "/de/dokumentation/einstieg/installation", { locale: "de" }), "surroundings");
269
+ await expectUnsupportedProviderOperation(() => provider.surroundings?.(event, "docs", "/de/dokumentation/einstieg/installation", { locale: "de" }), "surroundings");
163
270
  return;
164
271
  }
165
- const surround = unwrapProviderResult(await provider.surroundings?.(event, "docs", "/de/dokumentation/einstieg/installation", { locale: "de" }));
272
+ const surround = unwrapProviderContractResult(await provider.surroundings?.(event, "docs", "/de/dokumentation/einstieg/installation", { locale: "de" }));
166
273
  expect(surround).toHaveLength(2);
167
274
  expect(surround?.map((item) => item?.unprefixedPath)).toContain("/dokumentation/einstieg/alltag");
168
275
  });
@@ -170,10 +277,10 @@ export const runProviderContractSuite = ({
170
277
  const provider = await loadProvider();
171
278
  const event = createEvent();
172
279
  if (!caps.searchSections) {
173
- await expectUnsupportedOperation(() => provider.searchSections?.(event, "docs", { locale: "de" }), "search sections");
280
+ await expectUnsupportedProviderOperation(() => provider.searchSections?.(event, "docs", { locale: "de" }), "search sections");
174
281
  return;
175
282
  }
176
- const sections = unwrapProviderResult(await provider.searchSections?.(event, "docs", { locale: "de" }));
283
+ const sections = unwrapProviderContractResult(await provider.searchSections?.(event, "docs", { locale: "de" }));
177
284
  expect(sections?.some((section) => section.id.startsWith("/de/dokumentation/einstieg") || section.id.startsWith("/dokumentation/einstieg"))).toBe(true);
178
285
  expect(JSON.stringify(sections)).not.toContain("Draft Roadmap");
179
286
  });
@@ -181,10 +288,10 @@ export const runProviderContractSuite = ({
181
288
  const provider = await loadProvider();
182
289
  const event = createEvent();
183
290
  if (!caps.sitemap) {
184
- await expectUnsupportedOperation(() => provider.sitemapEntries?.(event, { include: ["docs"] }), "sitemap entries");
291
+ await expectUnsupportedProviderOperation(() => provider.sitemapEntries?.(event, { include: ["docs"] }), "sitemap entries");
185
292
  return;
186
293
  }
187
- const sitemap = unwrapProviderResult(await provider.sitemapEntries?.(event, { include: ["docs"] }));
294
+ const sitemap = unwrapProviderContractResult(await provider.sitemapEntries?.(event, { include: ["docs"] }));
188
295
  expect(sitemap?.some((entry) => entry.loc.endsWith("/de/dokumentation/einstieg"))).toBe(true);
189
296
  expect(JSON.stringify(sitemap)).not.toContain("/docs/draft-roadmap");
190
297
  });
@@ -213,7 +320,7 @@ export const runProviderContractSuite = ({
213
320
  test(`${name} supports data-only collection reads and rejects data-only sitemap access`, async () => {
214
321
  if (!caps.dataCollections) return;
215
322
  const provider = await loadProvider();
216
- const response = unwrapProviderResult(await provider.query(createEvent(), toContentProviderQuery({
323
+ const response = unwrapProviderContractResult(await provider.query(createEvent(), toContentProviderQuery({
217
324
  collection: "versions",
218
325
  resolveLocale: { locale: "en", fallback: false },
219
326
  sort: [{ date: -1 }]
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "framework": "vue",
3
3
  "name": "@lupinum/ginko-content",
4
- "version": "0.2.0",
4
+ "version": "0.2.1",
5
5
  "contributions": {
6
6
  "html": {
7
7
  "description-markup": "markdown",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lupinum/ginko-content",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Collection-first content engine for Nuxt",
5
5
  "homepage": "https://ginko-content.nuxt.dev",
6
6
  "bugs": {