@lupinum/ginko-content 0.4.0-rc.2 → 1.0.0-beta.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.
Files changed (71) hide show
  1. package/README.md +7 -5
  2. package/dist/core/provider-errors.d.ts +1 -1
  3. package/dist/core/provider-errors.js +1 -1
  4. package/dist/core/query/execute.js +7 -7
  5. package/dist/core/query/filter.d.ts +1 -0
  6. package/dist/core/query/filter.js +5 -1
  7. package/dist/core/query/operators.d.ts +6 -0
  8. package/dist/core/query/operators.js +13 -0
  9. package/dist/core/visibility.js +2 -2
  10. package/dist/features/agent/agent-markdown.d.ts +1 -0
  11. package/dist/features/agent/walker.js +7 -1
  12. package/dist/features/collections/resolve.d.ts +2 -3
  13. package/dist/features/query/context.d.ts +8 -2
  14. package/dist/features/query/documents.d.ts +4 -3
  15. package/dist/features/query/documents.js +55 -39
  16. package/dist/features/query/errors.d.ts +2 -0
  17. package/dist/features/query/errors.js +8 -0
  18. package/dist/features/query/navigation.js +5 -1
  19. package/dist/features/query/pagination.d.ts +2 -5
  20. package/dist/features/query/pagination.js +32 -62
  21. package/dist/features/query/populate.d.ts +3 -1
  22. package/dist/features/query/populate.js +38 -12
  23. package/dist/features/query/responses.d.ts +1 -0
  24. package/dist/features/query/responses.js +4 -0
  25. package/dist/features/query/unified.d.ts +3 -2
  26. package/dist/features/query/unified.js +7 -4
  27. package/dist/features/search/sections.d.ts +9 -3
  28. package/dist/features/search/sections.js +1 -1
  29. package/dist/module.d.mts +1 -3
  30. package/dist/module.json +1 -1
  31. package/dist/module.mjs +119 -92
  32. package/dist/parsers/markdown-plugins.js +2 -2
  33. package/dist/public/client.d.ts +3 -3
  34. package/dist/public/client.js +1 -0
  35. package/dist/public/server.d.ts +2 -2
  36. package/dist/public/server.js +1 -0
  37. package/dist/runtime/app/components/ContentRenderer.vue +5 -3
  38. package/dist/runtime/app/composables/query-api.d.ts +3 -2
  39. package/dist/runtime/app/composables/query-api.js +4 -0
  40. package/dist/runtime/app/composables/search.js +1 -5
  41. package/dist/runtime/app/composables/use-content-page.d.ts +6 -3
  42. package/dist/runtime/app/composables/use-content-page.js +11 -3
  43. package/dist/runtime/app/composables/utils.d.ts +3 -4
  44. package/dist/runtime/app/composables/utils.js +6 -6
  45. package/dist/runtime/server/agent-http.d.ts +3 -0
  46. package/dist/runtime/server/agent-http.js +42 -9
  47. package/dist/runtime/server/agent-markdown.js +4 -1
  48. package/dist/runtime/server/agent-site.d.ts +2 -0
  49. package/dist/runtime/server/agent-site.js +23 -5
  50. package/dist/runtime/server/api/query.d.ts +1 -1
  51. package/dist/runtime/server/api/query.js +19 -10
  52. package/dist/runtime/server/index.d.ts +1 -1
  53. package/dist/runtime/server/index.js +1 -1
  54. package/dist/runtime/server/middleware/agent-markdown.js +2 -3
  55. package/dist/runtime/server/plugins/agent-errors.d.ts +2 -0
  56. package/dist/runtime/server/plugins/agent-errors.js +23 -0
  57. package/dist/runtime/server/provider-query.js +5 -10
  58. package/dist/runtime/server/query-api.d.ts +3 -2
  59. package/dist/runtime/server/query-api.js +30 -8
  60. package/dist/runtime/server/query-http-validation.d.ts +2 -2
  61. package/dist/runtime/server/query-http-validation.js +15 -1
  62. package/dist/runtime/server/search.js +2 -6
  63. package/dist/runtime/utils/query.d.ts +2 -2
  64. package/dist/storage/references.d.ts +0 -3
  65. package/dist/storage/references.js +0 -21
  66. package/dist/types/config.d.ts +5 -3
  67. package/dist/types/content.d.ts +1 -3
  68. package/dist/types/query-parts/public.d.ts +5 -0
  69. package/dist/types/query-parts/transport.d.ts +4 -0
  70. package/dist/web-types.json +1 -1
  71. package/package.json +1 -1
package/README.md CHANGED
@@ -15,7 +15,7 @@
15
15
  </p>
16
16
 
17
17
  > [!WARNING]
18
- > Version `0.4.0-rc.2` is a release candidate. Install it from npm's `next`
18
+ > Version `1.0.0-beta.2` is a prerelease. Install it from npm's `next`
19
19
  > channel. The stable `0.3` line remains on `latest`.
20
20
 
21
21
  ## Why use this package?
@@ -40,13 +40,13 @@ output.
40
40
  Use the Nuxt CLI:
41
41
 
42
42
  ```bash
43
- npx nuxi module add @lupinum/ginko-content@0.4.0-rc.2
43
+ npx nuxi module add @lupinum/ginko-content@1.0.0-beta.2
44
44
  ```
45
45
 
46
46
  Or install and register the module by hand:
47
47
 
48
48
  ```bash
49
- pnpm add @lupinum/ginko-content@0.4.0-rc.2
49
+ pnpm add @lupinum/ginko-content@1.0.0-beta.2
50
50
  ```
51
51
 
52
52
  ```ts
@@ -93,9 +93,9 @@ import { pages } from '~~/content.config'
93
93
 
94
94
  definePageMeta({ key: route => route.path })
95
95
 
96
- const { page } = await useContentPage(pages)
96
+ const { page, status } = await useContentPage(pages)
97
97
 
98
- if (!page.value) {
98
+ if (status.value === 'not-found') {
99
99
  throw createError({ statusCode: 404, statusMessage: 'Page not found', fatal: true })
100
100
  }
101
101
  </script>
@@ -111,8 +111,10 @@ if (!page.value) {
111
111
  - YAML, JSON, and CSV ingestion.
112
112
  - Typed collections and frontmatter.
113
113
  - Route-aware pages and server-side queries.
114
+ - Exact filtered counts and server-resolved references.
114
115
  - Locale-aware routes and fallback rules.
115
116
  - Navigation, search, sitemap, and prerender helpers.
117
+ - Agent-readable Markdown and recoverable Markdown 404s.
116
118
  - A provider contract for remote or database-backed sources.
117
119
 
118
120
  Optional features stay explicit. Install `pagefind`, `katex`, or
@@ -7,4 +7,4 @@ export interface ContentProviderError extends Error {
7
7
  code: ContentProviderErrorCode;
8
8
  };
9
9
  }
10
- export declare const createContentProviderError: (code: ContentProviderErrorCode, message: string, details?: Record<string, unknown>) => ContentProviderError;
10
+ export declare const createCoreProviderError: (code: ContentProviderErrorCode, message: string, details?: Record<string, unknown>) => ContentProviderError;
@@ -19,7 +19,7 @@ export const statusForProviderError = {
19
19
  unsupported_provider_prerender: 400,
20
20
  unsupported_filesystem_preview: 400
21
21
  };
22
- export const createContentProviderError = (code, message, details = {}) => Object.assign(new Error(message), {
22
+ export const createCoreProviderError = (code, message, details = {}) => Object.assign(new Error(message), {
23
23
  statusCode: statusForProviderError[code],
24
24
  statusMessage: code,
25
25
  data: {
@@ -1,8 +1,8 @@
1
1
  import { isPlanRegex } from "./plan.js";
2
2
  import { resolveLocaleChain, sortLocalesCanonically } from "../content/locale.js";
3
3
  import { getGraphCanonicalVariants, resolveGraphCanonicalKey, resolveGraphRouteVariant, resolveGraphVariant, selectGraphDocuments } from "../content/graph.js";
4
- import { ensureArray, get, sortList, withKeys, withoutKeys } from "./operators.js";
5
- import { createContentProviderError } from "../provider-errors.js";
4
+ import { ensureArray, get, projectDocumentFields, sortList } from "./operators.js";
5
+ import { createCoreProviderError } from "../provider-errors.js";
6
6
  const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
7
7
  const reviveRegex = (value) => isPlanRegex(value) ? new RegExp(value.source, value.flags) : value;
8
8
  const includesEntry = (haystack, entry) => typeof haystack === "string" ? isPlanRegex(entry) ? reviveRegex(entry).test(haystack) : haystack.includes(String(entry)) : haystack.some((value) => compareOperators.eq(value, entry));
@@ -114,10 +114,10 @@ const applyQueryPlanSort = (matched, plan) => {
114
114
  }
115
115
  };
116
116
  export const applyQueryPlanProjection = (items, plan) => {
117
- return items.map((item) => {
118
- const without = withoutKeys([...plan.projection.without])(item);
119
- return withKeys([...plan.projection.only])(without);
120
- });
117
+ return items.map((item) => projectDocumentFields(
118
+ item,
119
+ plan.projection
120
+ ));
121
121
  };
122
122
  const FILESYSTEM_CURSOR_PREFIX = "o:";
123
123
  const encodeFilesystemCursor = (offset) => {
@@ -125,7 +125,7 @@ const encodeFilesystemCursor = (offset) => {
125
125
  return typeof Buffer !== "undefined" ? Buffer.from(raw).toString("base64") : btoa(raw);
126
126
  };
127
127
  const invalidFilesystemCursor = () => {
128
- throw createContentProviderError(
128
+ throw createCoreProviderError(
129
129
  "unsupported_query_shape",
130
130
  "The filesystem provider received an invalid cursor.",
131
131
  { provider: "filesystem", field: "paging.after" }
@@ -38,4 +38,5 @@ export declare const compileQueryParams: (input: {
38
38
  locale?: string;
39
39
  fallback?: LocaleFallback;
40
40
  exact?: boolean;
41
+ count?: boolean;
41
42
  }) => ContentProviderQueryInput;
@@ -166,6 +166,9 @@ export const compileQueryParams = (input) => {
166
166
  if (input.exact !== void 0 && typeof input.exact !== "boolean") {
167
167
  throw new TypeError("Invalid content query exact option: expected a boolean.");
168
168
  }
169
+ if (input.count !== void 0 && typeof input.count !== "boolean") {
170
+ throw new TypeError("Invalid content query count option: expected a boolean.");
171
+ }
169
172
  const selector = compileSelector(input.by);
170
173
  const path = selector && "path" in selector ? selector.path : void 0;
171
174
  const route = selector && "route" in selector ? selector.route : void 0;
@@ -180,7 +183,8 @@ export const compileQueryParams = (input) => {
180
183
  ...sort ? { sort } : {},
181
184
  ...input.limit !== void 0 ? { limit: input.limit } : {},
182
185
  ...input.skip !== void 0 ? { skip: input.skip } : {},
183
- ...select ? { only: select } : {}
186
+ ...select ? { only: select } : {},
187
+ ...input.count ? { count: true } : {}
184
188
  };
185
189
  if (input.locale || path || route || ref || fallback !== void 0) {
186
190
  const exact = input.exact ?? (input.locale ? fallback === void 0 || fallback === false : false);
@@ -14,6 +14,12 @@ export declare const withoutKeys: (keys?: string[]) => <T extends Record<string,
14
14
  export declare const withKeys: (keys?: string[]) => <T extends Record<string, unknown>>(obj: T | undefined) => {
15
15
  [k: string]: unknown;
16
16
  } | undefined;
17
+ export interface DocumentProjection {
18
+ only?: readonly string[];
19
+ without?: readonly string[];
20
+ }
21
+ /** Apply the shared document field-selection rules at every query boundary. */
22
+ export declare const projectDocumentFields: <T extends Record<string, unknown>>(document: T, projection: DocumentProjection, guaranteed?: readonly string[]) => T;
17
23
  export declare const sortList: <T extends Record<string, unknown>>(data: T[], params: ContentQuerySortOptions) => T[];
18
24
  /**
19
25
  * Ensure result is an array
@@ -37,6 +37,19 @@ export const withKeys = (keys = []) => (obj) => {
37
37
  const { prefixes, properties } = detectProperties(keys);
38
38
  return pickObject(obj, (key) => properties.includes(key) || prefixes.includes(key.charAt(0)));
39
39
  };
40
+ export const projectDocumentFields = (document, projection, guaranteed = []) => {
41
+ const selected = projection.only ?? [];
42
+ const excluded = projection.without ?? [];
43
+ if (selected.length === 0 && excluded.length === 0) return document;
44
+ const stripped = excluded.length > 0 ? withoutKeys([...excluded])(document) : document;
45
+ if (selected.length === 0) return stripped;
46
+ const projected = withKeys([...selected])(stripped);
47
+ if (guaranteed.length === 0) return projected;
48
+ return {
49
+ ...projected,
50
+ ...withKeys([...guaranteed])(document)
51
+ };
52
+ };
40
53
  export const sortList = (data, params) => {
41
54
  const sortParams = params;
42
55
  const comperable = new Intl.Collator(sortParams.$locale, {
@@ -1,4 +1,4 @@
1
- import { createContentProviderError } from "./provider-errors.js";
1
+ import { createCoreProviderError } from "./provider-errors.js";
2
2
  export const resolveRuntimeEnvironment = () => import.meta.dev ? "development" : "production";
3
3
  export const resolveIncludeDrafts = (context) => {
4
4
  if (typeof context.includeDrafts === "boolean") {
@@ -9,7 +9,7 @@ export const resolveIncludeDrafts = (context) => {
9
9
  export const isPublicationVisible = (facts, context) => !facts.draft || resolveIncludeDrafts(context);
10
10
  export const assertFilesystemPreviewSupported = (context) => {
11
11
  if (context.environment === "production" && context.previewAuthorized) {
12
- throw createContentProviderError(
12
+ throw createCoreProviderError(
13
13
  "unsupported_filesystem_preview",
14
14
  "Production preview requires a content provider. The filesystem provider serves an immutable sealed snapshot in production and cannot overlay draft/preview content \u2014 configure a provider-owned preview workflow, or disable preview for filesystem production deployments.",
15
15
  { provider: "filesystem" }
@@ -20,6 +20,7 @@ export interface AgentMarkdown {
20
20
  description: string;
21
21
  markdown: string;
22
22
  sourceFile?: string;
23
+ order?: number;
23
24
  canonicalUrl: string;
24
25
  lastModified?: string;
25
26
  publicSignals?: AgentMarkdownPublicSignals;
@@ -21,6 +21,12 @@ const renderChildren = (node, ctx) => (node.children || []).map((child) => rende
21
21
  const block = (value) => value.trim() ? `${value.trim()}
22
22
 
23
23
  ` : "";
24
+ const codeFenceInfo = (node) => {
25
+ const language = typeof node.props?.language === "string" ? node.props.language.replace(/[\r\n]/g, " ").trim() : "";
26
+ const filename = typeof node.props?.filename === "string" ? node.props.filename.replace(/[\r\n]/g, " ").trim() : "";
27
+ const escapedFilename = filename.replace(/\\/g, "\\\\").replace(/\]/g, "\\]");
28
+ return `${language}${escapedFilename ? ` [${escapedFilename}]` : ""}`;
29
+ };
24
30
  const renderList = (node, ctx, ordered = false) => block((node.children || []).map((child, index) => {
25
31
  const [first, ...rest] = child.children || [];
26
32
  let task = "";
@@ -136,7 +142,7 @@ const renderNode = (node, ctx) => {
136
142
  case "code":
137
143
  return `\`${node.value || textValue(node)}\``;
138
144
  case "pre":
139
- return block(`\`\`\`
145
+ return block(`\`\`\`${codeFenceInfo(node)}
140
146
  ${textValue(node)}
141
147
  \`\`\``);
142
148
  case "a": {
@@ -1,5 +1,4 @@
1
- import type { ParsedContent } from '../../types/content';
2
- import { type GenerateSearchSectionsOptions } from '../search/sections';
1
+ import { type GenerateSearchSectionsOptions, type SearchablePage } from '../search/sections';
3
2
  import type { RuntimeContentI18nInput } from '../localization/config';
4
3
  type CollectionResolveRuntime = RuntimeContentI18nInput;
5
4
  export declare const resolveCollectionItemSurroundingsData: (collection: string, path: string, runtime: CollectionResolveRuntime, options: {
@@ -17,7 +16,7 @@ export declare const resolveCollectionSearchSectionsData: (collection: string, r
17
16
  locale?: string;
18
17
  activeLocale?: string;
19
18
  }) & {
20
- loadPages: (extraFields: string[]) => Promise<Array<Pick<ParsedContent, "path" | "title" | "description" | "body"> & Record<string, unknown>>>;
19
+ loadPages: (extraFields: string[]) => Promise<SearchablePage[]>;
21
20
  }) => Promise<{
22
21
  id: string;
23
22
  title: string;
@@ -1,6 +1,6 @@
1
1
  import type { NavItem } from '../../types/content';
2
2
  import type { ContentPublicQueryResponse } from '../../types/api';
3
- import type { ContentProviderQueryInput } from '../../types/query';
3
+ import type { ContentQueryTransportInput } from '../../types/query';
4
4
  import type { ResolvedCollectionLocalePolicy } from '../localization/locale-policy';
5
5
  export interface RuntimeContentConfig {
6
6
  locales?: string[];
@@ -17,6 +17,11 @@ export interface RuntimeContentConfig {
17
17
  }>;
18
18
  }
19
19
  export type ContentQueryEndpoint = 'query' | 'navigation';
20
+ /**
21
+ * Internal execution context created only by Ginko's client and server
22
+ * adapters. Query transports must consume orchestration fields such as
23
+ * `populate` and return the fully resolved public response envelope.
24
+ */
20
25
  export interface ContentQueryContext {
21
26
  runtime: RuntimeContentConfig;
22
27
  /** Optional direct provider operation used by server-side `surround()`. */
@@ -26,5 +31,6 @@ export interface ContentQueryContext {
26
31
  fallback?: boolean | readonly string[];
27
32
  select?: readonly string[];
28
33
  }) => Promise<Array<NavItem | null>>;
29
- transport: <T>(endpoint: ContentQueryEndpoint, params: ContentProviderQueryInput) => Promise<ContentPublicQueryResponse<T> | NavItem[]>;
34
+ /** Execute the complete public query contract, including population. */
35
+ transport: <T>(endpoint: ContentQueryEndpoint, params: ContentQueryTransportInput) => Promise<ContentPublicQueryResponse<T> | NavItem[]>;
30
36
  }
@@ -1,6 +1,7 @@
1
1
  import type { ContentCollectionHandle } from '../../types/config';
2
- import type { DocumentFromHandle, LocalizedDoc, ManyOptions, OneOptions, PopulateFromOptions, PopulateSpec, PopulatedDocument, ResolveOneOptions, ResolveOneResult } from '../../types/query';
2
+ import type { CountOptions, DocumentFromHandle, LocalizedDoc, ManyOptions, OneOptions, PopulateFromOptions, PopulateSpec, PopulatedDocument, ResolveOneOptions, ResolveOneResult } from '../../types/query';
3
3
  import type { ContentQueryContext } from './context';
4
- export declare function resolveDocument<const H extends ContentCollectionHandle | string, O extends ResolveOneOptions<H, PopulateSpec | undefined>>(context: ContentQueryContext, one: typeof resolveDocumentOnly, handle: H, options: O): Promise<ResolveOneResult<PopulatedDocument<DocumentFromHandle<H>, PopulateFromOptions<O>>>>;
4
+ export declare function resolveDocument<const H extends ContentCollectionHandle | string, O extends ResolveOneOptions<H, PopulateSpec | undefined>>(context: ContentQueryContext, handle: H, options: O): Promise<ResolveOneResult<PopulatedDocument<DocumentFromHandle<H>, PopulateFromOptions<O>>>>;
5
5
  export declare function resolveDocumentOnly<const H extends ContentCollectionHandle | string, O extends OneOptions<H, PopulateSpec | undefined>>(context: ContentQueryContext, handle: H, options: O): Promise<LocalizedDoc<PopulatedDocument<DocumentFromHandle<H>, PopulateFromOptions<O>>> | null>;
6
- export declare function resolveManyDocuments<const H extends ContentCollectionHandle | string, O extends ManyOptions<H, PopulateSpec | undefined>>(context: ContentQueryContext, one: typeof resolveDocumentOnly, handle: H, options?: O): Promise<Array<LocalizedDoc<PopulatedDocument<DocumentFromHandle<H>, PopulateFromOptions<O>>>>>;
6
+ export declare function resolveManyDocuments<const H extends ContentCollectionHandle | string, O extends ManyOptions<H, PopulateSpec | undefined>>(context: ContentQueryContext, handle: H, options?: O): Promise<Array<LocalizedDoc<PopulatedDocument<DocumentFromHandle<H>, PopulateFromOptions<O>>>>>;
7
+ export declare function resolveCount<const H extends ContentCollectionHandle | string, O extends CountOptions<H>>(context: ContentQueryContext, handle: H, options?: O): Promise<number>;
@@ -1,9 +1,9 @@
1
1
  import { compileQueryParams } from "../../core/query/filter.js";
2
- import { isNotFoundError } from "./errors.js";
2
+ import { withNotFoundFallback } from "./errors.js";
3
3
  import { ensureCollectionName } from "./handles.js";
4
4
  import { resolveFallback } from "./locale-options.js";
5
- import { populateDocument, populateDocuments, selectWithPopulate, validatePopulateSpec } from "./populate.js";
6
- import { unwrapListResponse, unwrapOneResponse } from "./responses.js";
5
+ import { selectWithPopulate, serializePopulateSpec, validatePopulateSpec } from "./populate.js";
6
+ import { unwrapCountResponse, unwrapFindResponse, unwrapOneResponse } from "./responses.js";
7
7
  const explainResolution = (collection, requestedBy, normalizedBy, requestedLocale, requestedFallback, doc) => {
8
8
  const resolvedLocale = doc?.resolution?.resolved?.locale || doc?.locale;
9
9
  return {
@@ -29,65 +29,81 @@ const explainResolution = (collection, requestedBy, normalizedBy, requestedLocal
29
29
  }
30
30
  };
31
31
  };
32
- export async function resolveDocument(context, one, handle, options) {
32
+ export async function resolveDocument(context, handle, options) {
33
33
  const collection = ensureCollectionName(handle);
34
34
  const runtime = context.runtime;
35
35
  validatePopulateSpec(handle, collection, runtime, options.populate);
36
36
  const by = options.by;
37
37
  const fallback = resolveFallback(options.fallback, collection, runtime);
38
- const params = compileQueryParams({
39
- collection,
40
- by,
41
- locale: options.locale,
42
- fallback,
43
- select: selectWithPopulate(options.select, options.populate)
44
- });
38
+ const params = {
39
+ ...compileQueryParams({
40
+ collection,
41
+ by,
42
+ locale: options.locale,
43
+ fallback,
44
+ select: selectWithPopulate(options.select, options.populate)
45
+ }),
46
+ ...options.populate ? { populate: serializePopulateSpec(options.populate) } : {}
47
+ };
45
48
  params.first = true;
46
- let response;
47
- try {
48
- response = await context.transport("query", params);
49
- } catch (error) {
50
- if (isNotFoundError(error)) {
51
- const explain2 = explainResolution(collection, options.by, by, options.locale, options.fallback, null);
52
- return { doc: null, explain: explain2 };
53
- }
54
- throw error;
55
- }
49
+ const response = await withNotFoundFallback(
50
+ () => context.transport("query", params),
51
+ { result: null }
52
+ );
56
53
  const doc = unwrapOneResponse(response);
57
- const populated = doc ? await populateDocument(context, one, doc, options.populate, options.locale, options.fallback) : null;
58
54
  const explain = explainResolution(collection, options.by, by, options.locale, options.fallback, doc);
59
55
  return {
60
- doc: populated,
56
+ doc,
61
57
  explain
62
58
  };
63
59
  }
64
60
  export async function resolveDocumentOnly(context, handle, options) {
65
- const result = await resolveDocument(context, resolveDocumentOnly, handle, options);
61
+ const result = await resolveDocument(context, handle, options);
66
62
  return result.doc;
67
63
  }
68
- export async function resolveManyDocuments(context, one, handle, options = {}) {
64
+ export async function resolveManyDocuments(context, handle, options = {}) {
69
65
  const collection = ensureCollectionName(handle);
70
66
  const runtime = context.runtime;
71
67
  validatePopulateSpec(handle, collection, runtime, options.populate);
72
68
  const fallback = resolveFallback(options.fallback, collection, runtime);
69
+ const params = {
70
+ ...compileQueryParams({
71
+ collection,
72
+ where: options.where,
73
+ sort: options.sort,
74
+ limit: options.limit,
75
+ skip: options.skip,
76
+ locale: options.locale,
77
+ fallback,
78
+ select: selectWithPopulate(options.select, options.populate)
79
+ }),
80
+ ...options.populate ? { populate: serializePopulateSpec(options.populate) } : {}
81
+ };
82
+ const response = await withNotFoundFallback(
83
+ () => context.transport("query", params),
84
+ { result: [], skip: 0, limit: 0, total: 0 }
85
+ );
86
+ const find = unwrapFindResponse(response);
87
+ const docs = find.result;
88
+ if (import.meta.dev && options.limit === void 0 && find.total > docs.length) {
89
+ console.warn(
90
+ `[ginko-content] many("${collection}") matched ${find.total} documents but returned ${docs.length}. Pass an explicit limit or use paginate() to read the rest.`
91
+ );
92
+ }
93
+ return docs;
94
+ }
95
+ export async function resolveCount(context, handle, options = {}) {
96
+ const collection = ensureCollectionName(handle);
97
+ const fallback = resolveFallback(options.fallback, collection, context.runtime);
73
98
  const params = compileQueryParams({
74
99
  collection,
75
100
  where: options.where,
76
- sort: options.sort,
77
- limit: options.limit,
78
- skip: options.skip,
79
101
  locale: options.locale,
80
102
  fallback,
81
- select: selectWithPopulate(options.select, options.populate)
103
+ count: true
82
104
  });
83
- let response;
84
- try {
85
- response = await context.transport("query", params);
86
- } catch (error) {
87
- if (isNotFoundError(error)) return [];
88
- throw error;
89
- }
90
- const docs = unwrapListResponse(response);
91
- const populated = await populateDocuments(context, one, docs, options.populate, options.locale, options.fallback);
92
- return populated;
105
+ return unwrapCountResponse(await withNotFoundFallback(
106
+ () => context.transport("query", params),
107
+ { result: 0 }
108
+ ));
93
109
  }
@@ -1 +1,3 @@
1
1
  export declare const isNotFoundError: (error: unknown) => boolean;
2
+ /** Apply the public query API's one shared rule for missing collections. */
3
+ export declare const withNotFoundFallback: <T>(request: () => Promise<T>, fallback: T) => Promise<T>;
@@ -2,3 +2,11 @@ export const isNotFoundError = (error) => {
2
2
  const statusCode = error?.statusCode ?? error?.response?.status;
3
3
  return statusCode === 404;
4
4
  };
5
+ export const withNotFoundFallback = async (request, fallback) => {
6
+ try {
7
+ return await request();
8
+ } catch (error) {
9
+ if (isNotFoundError(error)) return fallback;
10
+ throw error;
11
+ }
12
+ };
@@ -1,6 +1,7 @@
1
1
  import { compileQueryParams } from "../../core/query/filter.js";
2
2
  import { NAVIGATION_REQUIRED_FIELDS } from "../../types/navigation.js";
3
3
  import { ensureCollectionName } from "./handles.js";
4
+ import { withNotFoundFallback } from "./errors.js";
4
5
  import { isCollectionRouteRoot, isNavigationRootPath } from "./localized-docs.js";
5
6
  import { resolveFallback } from "./locale-options.js";
6
7
  export const navigationSelectFields = (fields = []) => [
@@ -19,7 +20,10 @@ export async function resolveNavigation(context, handle, options = {}) {
19
20
  select: navigationSelectFields(options.select),
20
21
  exact: options.fallback === void 0 ? false : void 0
21
22
  });
22
- const response = await context.transport("navigation", params);
23
+ const response = await withNotFoundFallback(
24
+ () => context.transport("navigation", params),
25
+ []
26
+ );
23
27
  const list = Array.isArray(response) ? response : Array.isArray(response?.result) ? response.result : [];
24
28
  return list;
25
29
  }
@@ -1,13 +1,10 @@
1
- import type { ParsedContent } from '../../types/content';
2
1
  import type { ContentCollectionHandle } from '../../types/config';
3
- import type { DocumentFromHandle, LocalizedDoc, OneOptions, PaginationOptions, PaginationResult, PopulateFromOptions, PopulateSpec, PopulatedDocument } from '../../types/query';
2
+ import type { DocumentFromHandle, PaginationOptions, PaginationResult, PopulateFromOptions, PopulateSpec, PopulatedDocument } from '../../types/query';
4
3
  import type { ContentQueryContext } from './context';
5
- type OneResolver = <H extends ContentCollectionHandle | string>(context: ContentQueryContext, handle: H, options: OneOptions<H>) => Promise<LocalizedDoc<ParsedContent> | null>;
6
4
  /**
7
5
  * Resolve one honest pagination page. Two discriminated
8
6
  * modes: `offset` (exact `total`/`pageCount`) and `cursor` (opaque forward
9
7
  * cursor, no synthetic total). Omitting `mode` while supplying `page` means
10
8
  * `mode: 'offset'` — the source-compatible default.
11
9
  */
12
- export declare function resolvePagination<const H extends ContentCollectionHandle | string, O extends PaginationOptions<H, PopulateSpec | undefined>>(context: ContentQueryContext, one: OneResolver, handle: H, options: O): Promise<PaginationResult<PopulatedDocument<DocumentFromHandle<H>, PopulateFromOptions<O>>>>;
13
- export {};
10
+ export declare function resolvePagination<const H extends ContentCollectionHandle | string, O extends PaginationOptions<H, PopulateSpec | undefined>>(context: ContentQueryContext, handle: H, options: O): Promise<PaginationResult<PopulatedDocument<DocumentFromHandle<H>, PopulateFromOptions<O>>>>;
@@ -1,7 +1,7 @@
1
1
  import { compileQueryParams } from "../../core/query/filter.js";
2
2
  import { ensureCollectionName } from "./handles.js";
3
3
  import { resolveFallback } from "./locale-options.js";
4
- import { populateDocuments, selectWithPopulate, validatePopulateSpec } from "./populate.js";
4
+ import { selectWithPopulate, serializePopulateSpec, validatePopulateSpec } from "./populate.js";
5
5
  import {
6
6
  assertPublicPagingLimit,
7
7
  DEFAULT_PUBLIC_PAGINATION_LIMIT,
@@ -9,7 +9,7 @@ import {
9
9
  MAX_PUBLIC_QUERY_SKIP
10
10
  } from "../../core/query/limits.js";
11
11
  import { unwrapCursorFindResponse, unwrapFindResponse } from "./responses.js";
12
- import { isNotFoundError } from "./errors.js";
12
+ import { withNotFoundFallback } from "./errors.js";
13
13
  const resolvePage = (value) => {
14
14
  if (value === void 0) return 1;
15
15
  if (typeof value !== "number" || !Number.isFinite(value) || !Number.isInteger(value) || value < 1) {
@@ -17,26 +17,7 @@ const resolvePage = (value) => {
17
17
  }
18
18
  return value;
19
19
  };
20
- const emptyOffsetPage = (page, limit) => ({
21
- mode: "offset",
22
- data: [],
23
- page,
24
- limit,
25
- total: 0,
26
- pageCount: 0,
27
- hasNext: false,
28
- hasPrevious: page > 1,
29
- nextPage: null,
30
- previousPage: page > 1 ? page - 1 : null
31
- });
32
- const emptyCursorPage = (limit) => ({
33
- mode: "cursor",
34
- data: [],
35
- limit,
36
- endCursor: null,
37
- hasNext: false
38
- });
39
- export async function resolvePagination(context, one, handle, options) {
20
+ export async function resolvePagination(context, handle, options) {
40
21
  const collection = ensureCollectionName(handle);
41
22
  const runtime = context.runtime;
42
23
  validatePopulateSpec(handle, collection, runtime, options.populate);
@@ -54,6 +35,17 @@ export async function resolvePagination(context, one, handle, options) {
54
35
  const limit = options.limit ?? DEFAULT_PUBLIC_PAGINATION_LIMIT;
55
36
  assertPublicPagingLimit(limit);
56
37
  const select = selectWithPopulate(options.select, options.populate);
38
+ const baseParams = {
39
+ ...compileQueryParams({
40
+ collection,
41
+ where: options.where,
42
+ sort: options.sort,
43
+ locale: options.locale,
44
+ fallback,
45
+ select
46
+ }),
47
+ ...options.populate ? { populate: serializePopulateSpec(options.populate) } : {}
48
+ };
57
49
  if (mode === "cursor") {
58
50
  if (options.after !== void 0 && options.after !== null) {
59
51
  if (typeof options.after !== "string") {
@@ -63,29 +55,18 @@ export async function resolvePagination(context, one, handle, options) {
63
55
  throw new TypeError(`Content pagination cursor exceeds ${MAX_PUBLIC_QUERY_CURSOR_BYTES} bytes.`);
64
56
  }
65
57
  }
66
- const params2 = compileQueryParams({
67
- collection,
68
- where: options.where,
69
- sort: options.sort,
70
- locale: options.locale,
71
- fallback,
72
- select
73
- });
74
- params2.paging = { mode: "cursor", after: options.after ?? null, limit };
75
- let response2;
76
- try {
77
- response2 = await context.transport("query", params2);
78
- } catch (error) {
79
- if (isNotFoundError(error)) {
80
- return emptyCursorPage(limit);
81
- }
82
- throw error;
83
- }
58
+ const params2 = {
59
+ ...baseParams,
60
+ paging: { mode: "cursor", after: options.after ?? null, limit }
61
+ };
62
+ const response2 = await withNotFoundFallback(
63
+ () => context.transport("query", params2),
64
+ { mode: "cursor", result: [], limit, pageInfo: { endCursor: null, hasNext: false } }
65
+ );
84
66
  const envelope2 = unwrapCursorFindResponse(response2);
85
- const populated2 = await populateDocuments(context, one, envelope2.result, options.populate, options.locale, options.fallback);
86
67
  return {
87
68
  mode: "cursor",
88
- data: populated2,
69
+ data: envelope2.result,
89
70
  limit: envelope2.limit,
90
71
  endCursor: envelope2.endCursor,
91
72
  hasNext: envelope2.hasNext
@@ -96,30 +77,19 @@ export async function resolvePagination(context, one, handle, options) {
96
77
  if (!Number.isSafeInteger(skip) || skip > MAX_PUBLIC_QUERY_SKIP) {
97
78
  throw new TypeError(`Content pagination page exceeds the maximum query skip of ${MAX_PUBLIC_QUERY_SKIP}.`);
98
79
  }
99
- const params = compileQueryParams({
100
- collection,
101
- where: options.where,
102
- sort: options.sort,
103
- locale: options.locale,
104
- fallback,
105
- select
106
- });
107
- params.paging = { mode: "offset", skip, limit };
108
- let response;
109
- try {
110
- response = await context.transport("query", params);
111
- } catch (error) {
112
- if (isNotFoundError(error)) {
113
- return emptyOffsetPage(page, limit);
114
- }
115
- throw error;
116
- }
80
+ const params = {
81
+ ...baseParams,
82
+ paging: { mode: "offset", skip, limit }
83
+ };
84
+ const response = await withNotFoundFallback(
85
+ () => context.transport("query", params),
86
+ { result: [], skip, limit, total: 0 }
87
+ );
117
88
  const envelope = unwrapFindResponse(response);
118
- const populated = await populateDocuments(context, one, envelope.result, options.populate, options.locale, options.fallback);
119
89
  const pageCount = envelope.total > 0 ? Math.ceil(envelope.total / limit) : 0;
120
90
  return {
121
91
  mode: "offset",
122
- data: populated,
92
+ data: envelope.result,
123
93
  page,
124
94
  limit,
125
95
  total: envelope.total,
@@ -1,10 +1,12 @@
1
1
  import type { ParsedContent } from '../../types/content';
2
2
  import type { ContentCollectionHandle } from '../../types/config';
3
- import type { LocaleFallback, LocalizedDoc, OneOptions, PopulateSpec, PopulatedDocument } from '../../types/query';
3
+ import type { LocaleFallback, LocalizedDoc, OneOptions, ContentQueryTransportInput, PopulateSpec, PopulatedDocument } from '../../types/query';
4
4
  import type { ContentQueryContext, RuntimeContentConfig } from './context';
5
+ export declare const serializePopulateSpec: (populate: PopulateSpec | undefined) => Record<string, string> | undefined;
5
6
  export declare const validatePopulateSpec: (source: ContentCollectionHandle | string, sourceCollection: string, runtime: RuntimeContentConfig | undefined, populate: PopulateSpec | undefined) => void;
6
7
  type PopulateReferenceResolver = (context: ContentQueryContext, target: ContentCollectionHandle | string, options: OneOptions<ContentCollectionHandle | string>) => Promise<unknown>;
7
8
  export declare const populateDocuments: <T extends ParsedContent, P extends PopulateSpec | undefined>(context: ContentQueryContext, resolveReference: PopulateReferenceResolver, docs: ReadonlyArray<LocalizedDoc<T>>, populate: P, locale: string | undefined, fallback: LocaleFallback | undefined) => Promise<Array<LocalizedDoc<PopulatedDocument<T, P>>>>;
8
9
  export declare const populateDocument: <T extends ParsedContent, P extends PopulateSpec | undefined>(context: ContentQueryContext, resolveReference: PopulateReferenceResolver, doc: LocalizedDoc<T>, populate: P, locale: string | undefined, fallback: LocaleFallback | undefined) => Promise<LocalizedDoc<PopulatedDocument<T, P>>>;
10
+ export declare const populateQueryResponse: (context: ContentQueryContext, resolveReference: PopulateReferenceResolver, response: unknown, params: ContentQueryTransportInput) => Promise<unknown>;
9
11
  export declare const selectWithPopulate: (select: ReadonlyArray<string> | undefined, populate: PopulateSpec | undefined) => string[] | undefined;
10
12
  export {};