@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.
- package/README.md +7 -5
- package/dist/core/provider-errors.d.ts +1 -1
- package/dist/core/provider-errors.js +1 -1
- package/dist/core/query/execute.js +7 -7
- package/dist/core/query/filter.d.ts +1 -0
- package/dist/core/query/filter.js +5 -1
- package/dist/core/query/operators.d.ts +6 -0
- package/dist/core/query/operators.js +13 -0
- package/dist/core/visibility.js +2 -2
- package/dist/features/agent/agent-markdown.d.ts +1 -0
- package/dist/features/agent/walker.js +7 -1
- package/dist/features/collections/resolve.d.ts +2 -3
- package/dist/features/query/context.d.ts +8 -2
- package/dist/features/query/documents.d.ts +4 -3
- package/dist/features/query/documents.js +55 -39
- package/dist/features/query/errors.d.ts +2 -0
- package/dist/features/query/errors.js +8 -0
- package/dist/features/query/navigation.js +5 -1
- package/dist/features/query/pagination.d.ts +2 -5
- package/dist/features/query/pagination.js +32 -62
- package/dist/features/query/populate.d.ts +3 -1
- package/dist/features/query/populate.js +38 -12
- package/dist/features/query/responses.d.ts +1 -0
- package/dist/features/query/responses.js +4 -0
- package/dist/features/query/unified.d.ts +3 -2
- package/dist/features/query/unified.js +7 -4
- package/dist/features/search/sections.d.ts +9 -3
- package/dist/features/search/sections.js +1 -1
- package/dist/module.d.mts +1 -3
- package/dist/module.json +1 -1
- package/dist/module.mjs +119 -92
- package/dist/parsers/markdown-plugins.js +2 -2
- package/dist/public/client.d.ts +3 -3
- package/dist/public/client.js +1 -0
- package/dist/public/server.d.ts +2 -2
- package/dist/public/server.js +1 -0
- package/dist/runtime/app/components/ContentRenderer.vue +5 -3
- package/dist/runtime/app/composables/query-api.d.ts +3 -2
- package/dist/runtime/app/composables/query-api.js +4 -0
- package/dist/runtime/app/composables/search.js +1 -5
- package/dist/runtime/app/composables/use-content-page.d.ts +6 -3
- package/dist/runtime/app/composables/use-content-page.js +11 -3
- package/dist/runtime/app/composables/utils.d.ts +3 -4
- package/dist/runtime/app/composables/utils.js +6 -6
- package/dist/runtime/server/agent-http.d.ts +3 -0
- package/dist/runtime/server/agent-http.js +42 -9
- package/dist/runtime/server/agent-markdown.js +4 -1
- package/dist/runtime/server/agent-site.d.ts +2 -0
- package/dist/runtime/server/agent-site.js +23 -5
- package/dist/runtime/server/api/query.d.ts +1 -1
- package/dist/runtime/server/api/query.js +19 -10
- package/dist/runtime/server/index.d.ts +1 -1
- package/dist/runtime/server/index.js +1 -1
- package/dist/runtime/server/middleware/agent-markdown.js +2 -3
- package/dist/runtime/server/plugins/agent-errors.d.ts +2 -0
- package/dist/runtime/server/plugins/agent-errors.js +23 -0
- package/dist/runtime/server/provider-query.js +5 -10
- package/dist/runtime/server/query-api.d.ts +3 -2
- package/dist/runtime/server/query-api.js +30 -8
- package/dist/runtime/server/query-http-validation.d.ts +2 -2
- package/dist/runtime/server/query-http-validation.js +15 -1
- package/dist/runtime/server/search.js +2 -6
- package/dist/runtime/utils/query.d.ts +2 -2
- package/dist/storage/references.d.ts +0 -3
- package/dist/storage/references.js +0 -21
- package/dist/types/config.d.ts +5 -3
- package/dist/types/content.d.ts +1 -3
- package/dist/types/query-parts/public.d.ts +5 -0
- package/dist/types/query-parts/transport.d.ts +4 -0
- package/dist/web-types.json +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
</p>
|
|
16
16
|
|
|
17
17
|
> [!WARNING]
|
|
18
|
-
> Version `0.
|
|
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.
|
|
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.
|
|
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 (
|
|
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
|
|
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
|
|
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,
|
|
5
|
-
import {
|
|
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
|
-
|
|
119
|
-
|
|
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
|
|
128
|
+
throw createCoreProviderError(
|
|
129
129
|
"unsupported_query_shape",
|
|
130
130
|
"The filesystem provider received an invalid cursor.",
|
|
131
131
|
{ provider: "filesystem", field: "paging.after" }
|
|
@@ -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, {
|
package/dist/core/visibility.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
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
|
|
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" }
|
|
@@ -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
|
|
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<
|
|
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 {
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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 {
|
|
2
|
+
import { withNotFoundFallback } from "./errors.js";
|
|
3
3
|
import { ensureCollectionName } from "./handles.js";
|
|
4
4
|
import { resolveFallback } from "./locale-options.js";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
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,
|
|
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 =
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
|
56
|
+
doc,
|
|
61
57
|
explain
|
|
62
58
|
};
|
|
63
59
|
}
|
|
64
60
|
export async function resolveDocumentOnly(context, handle, options) {
|
|
65
|
-
const result = await resolveDocument(context,
|
|
61
|
+
const result = await resolveDocument(context, handle, options);
|
|
66
62
|
return result.doc;
|
|
67
63
|
}
|
|
68
|
-
export async function resolveManyDocuments(context,
|
|
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
|
-
|
|
103
|
+
count: true
|
|
82
104
|
});
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
}
|
|
@@ -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
|
|
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,
|
|
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,
|
|
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 {
|
|
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 {
|
|
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
|
-
|
|
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 =
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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:
|
|
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 =
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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:
|
|
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 {};
|