@stackonward/cms-client 0.0.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.
- package/CHANGELOG.md +9 -0
- package/LICENSE +21 -0
- package/README.md +62 -0
- package/dist/index.d.mts +86 -0
- package/dist/index.d.ts +86 -0
- package/dist/index.mjs +221 -0
- package/dist/routes.d.mts +54 -0
- package/dist/routes.d.ts +54 -0
- package/dist/routes.mjs +148 -0
- package/dist/types.d.mts +311 -0
- package/dist/types.d.ts +311 -0
- package/dist/types.mjs +1 -0
- package/dist/webhook.d.mts +10 -0
- package/dist/webhook.d.ts +10 -0
- package/dist/webhook.mjs +14 -0
- package/package.json +74 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.0.1
|
|
4
|
+
|
|
5
|
+
- Established the framework-neutral typed CMS delivery client.
|
|
6
|
+
- Added article, author, category, search, site configuration, redirect, preview,
|
|
7
|
+
and sitemap contracts.
|
|
8
|
+
- Added configurable request headers and error normalization without embedding a
|
|
9
|
+
product or platform protocol.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024-present yhy
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# @stackonward/cms-client
|
|
2
|
+
|
|
3
|
+
Framework-agnostic typed client for a CMS delivery API. It provides article,
|
|
4
|
+
author, category, comment, sitemap, redirect, series, site-configuration, and
|
|
5
|
+
webhook-signature contracts while leaving backend-specific headers and error
|
|
6
|
+
envelopes to adapters.
|
|
7
|
+
|
|
8
|
+
## Install
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
pnpm add @stackonward/cms-client
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Usage
|
|
15
|
+
|
|
16
|
+
```typescript
|
|
17
|
+
import { createCmsClient } from '@stackonward/cms-client'
|
|
18
|
+
|
|
19
|
+
const cms = createCmsClient({
|
|
20
|
+
baseUrl: 'https://cms.example.com/api',
|
|
21
|
+
defaultLocale: 'en',
|
|
22
|
+
timeout: 10_000,
|
|
23
|
+
headers: { 'X-Customer-Scope': 'customer_alpha' },
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
const article = await cms.getArticle('getting-started')
|
|
27
|
+
const sitemap = await cms.getSitemap('en')
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Configuration
|
|
31
|
+
|
|
32
|
+
| Option | Required | Description |
|
|
33
|
+
| ----------------- | -------- | ---------------------------------------------------- |
|
|
34
|
+
| `baseUrl` | Yes | CMS delivery API base URL |
|
|
35
|
+
| `defaultLocale` | No | Locale added to eligible reads; defaults to `en` |
|
|
36
|
+
| `fetch` | No | Fetch-compatible transport implementation |
|
|
37
|
+
| `timeout` | No | Request timeout in milliseconds; defaults to `10000` |
|
|
38
|
+
| `headers` | No | Static or request-aware header provider |
|
|
39
|
+
| `errorNormalizer` | No | Backend error-envelope adapter |
|
|
40
|
+
|
|
41
|
+
The generic client does not know product codes, preview-header ownership, or a
|
|
42
|
+
specific backend error envelope. OneX consumers use
|
|
43
|
+
`@stackonward/onex-cms-client` to bind those contracts explicitly.
|
|
44
|
+
|
|
45
|
+
## Entry points
|
|
46
|
+
|
|
47
|
+
| Entry point | Content |
|
|
48
|
+
| --------------------------------- | ---------------------------------------- |
|
|
49
|
+
| `@stackonward/cms-client` | Client, routes, errors, and public types |
|
|
50
|
+
| `@stackonward/cms-client/types` | CMS request and response types |
|
|
51
|
+
| `@stackonward/cms-client/routes` | Resource route policy helpers |
|
|
52
|
+
| `@stackonward/cms-client/webhook` | HMAC-SHA256 webhook verification |
|
|
53
|
+
|
|
54
|
+
## Compatibility
|
|
55
|
+
|
|
56
|
+
- Fetch-compatible ESM runtime
|
|
57
|
+
- Node.js 18+ for webhook verification
|
|
58
|
+
- TypeScript declarations included
|
|
59
|
+
|
|
60
|
+
## License
|
|
61
|
+
|
|
62
|
+
[MIT](./LICENSE)
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { ArticleListParams, Paginated, ArticleListItem, ArticleDetail, ArticleSearchParams, ArticleSearchResultItem, PaginationParams, Author, CategoryNode, Comment, CreateCommentPayload, SitemapEntry, RedirectRule, SiteConfigQuery, SiteConfig, Series } from './types.mjs';
|
|
2
|
+
export { AuthorProfile, AuthorProfileCta, AuthorProfileQuestion, AuthorProfileSection, AuthorProfileStat, AuthorSocialLink, HreflangEntry, ListResponse, LocalizedOgMetaMap, LocalizedSeoMetaMap, LocalizedString, LocalizedTwitterMetaMap, OgMeta, PaginationMeta, RedirectMatchType, RevalidatePayload, SeoMeta, SiteDeliveryMode, SiteDomain, SiteDomainType, SitemapImageEntry, SitemapVideoEntry, TwitterMeta, WebhookEventType } from './types.mjs';
|
|
3
|
+
import { HeaderProvider, ErrorNormalizer, NormalizedApiError } from '@stackonward/api-client';
|
|
4
|
+
export { BuildRoutePathOptions, BuildRouteUrlOptions, DEFAULT_ROUTE_RESOURCES, ResolvedRouteLocalePolicy, ResolvedRoutePolicy, RouteLocalePolicy, RouteParams, RoutePolicy, RoutePolicyConfig, RouteResourceDefaults, RouteResources, buildRoutePath, buildRouteUrl, extractRouteParams, extractSourceRouteParams, formatRoutePolicy, hasCustomArticleRoute, isRoutePolicy, mergeRouteParams, resolveRoutePolicy } from './routes.mjs';
|
|
5
|
+
|
|
6
|
+
/** CMS 客户端配置 */
|
|
7
|
+
interface CmsClientConfig {
|
|
8
|
+
/** CMS API 基础 URL */
|
|
9
|
+
baseUrl: string;
|
|
10
|
+
/** 默认语言 */
|
|
11
|
+
defaultLocale?: string;
|
|
12
|
+
/** 自定义 fetch 实现 */
|
|
13
|
+
fetch?: typeof globalThis.fetch;
|
|
14
|
+
/** 请求超时(毫秒),默认 10000 */
|
|
15
|
+
timeout?: number;
|
|
16
|
+
/** 请求头提供器 */
|
|
17
|
+
headers?: HeaderProvider;
|
|
18
|
+
/** 后端错误格式适配器 */
|
|
19
|
+
errorNormalizer?: ErrorNormalizer;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** CMS Public API 客户端 */
|
|
23
|
+
declare class CmsClient {
|
|
24
|
+
private readonly config;
|
|
25
|
+
private readonly api;
|
|
26
|
+
constructor(config: CmsClientConfig);
|
|
27
|
+
/** 获取文章分页列表 */
|
|
28
|
+
getArticles(params?: ArticleListParams): Promise<Paginated<ArticleListItem>>;
|
|
29
|
+
/** 获取文章详情 */
|
|
30
|
+
getArticle(slug: string, locale?: string): Promise<ArticleDetail>;
|
|
31
|
+
/** 获取相关文章 */
|
|
32
|
+
getRelatedArticles(slug: string, options?: {
|
|
33
|
+
limit?: number;
|
|
34
|
+
locale?: string;
|
|
35
|
+
}): Promise<ArticleListItem[]>;
|
|
36
|
+
/** 搜索已发布文章 */
|
|
37
|
+
searchArticles(params: ArticleSearchParams): Promise<Paginated<ArticleSearchResultItem>>;
|
|
38
|
+
/** 获取作者分页列表 */
|
|
39
|
+
getAuthorsPage(options?: {
|
|
40
|
+
locale?: string;
|
|
41
|
+
} & PaginationParams): Promise<Paginated<Author>>;
|
|
42
|
+
/** 获取所有作者 */
|
|
43
|
+
getAuthors(options?: {
|
|
44
|
+
locale?: string;
|
|
45
|
+
} & PaginationParams): Promise<Author[]>;
|
|
46
|
+
/** 获取作者详情 */
|
|
47
|
+
getAuthor(slug: string, locale?: string): Promise<Author>;
|
|
48
|
+
/** 获取作者的文章分页列表 */
|
|
49
|
+
getAuthorArticlesPage(slug: string, options?: {
|
|
50
|
+
is_pinned?: boolean;
|
|
51
|
+
locale?: string;
|
|
52
|
+
} & PaginationParams): Promise<Paginated<ArticleListItem>>;
|
|
53
|
+
/** 获取作者的文章列表 */
|
|
54
|
+
getAuthorArticles(slug: string, options?: {
|
|
55
|
+
is_pinned?: boolean;
|
|
56
|
+
locale?: string;
|
|
57
|
+
} & PaginationParams): Promise<ArticleListItem[]>;
|
|
58
|
+
/** 获取分类树 */
|
|
59
|
+
getCategories(): Promise<CategoryNode[]>;
|
|
60
|
+
/** 获取文章评论树 */
|
|
61
|
+
getComments(articleSlug: string): Promise<Comment[]>;
|
|
62
|
+
/** 发表评论 */
|
|
63
|
+
createComment(articleSlug: string, payload: CreateCommentPayload): Promise<void>;
|
|
64
|
+
/** 获取 Sitemap 条目 */
|
|
65
|
+
getSitemap(locale?: string): Promise<SitemapEntry[]>;
|
|
66
|
+
/** 获取重定向规则 */
|
|
67
|
+
getRedirects(): Promise<RedirectRule[]>;
|
|
68
|
+
/** 获取站点运行时配置 */
|
|
69
|
+
getSiteConfig(options?: SiteConfigQuery): Promise<SiteConfig>;
|
|
70
|
+
/** 获取文章系列 */
|
|
71
|
+
getSeries(slug: string, locale?: string): Promise<Series>;
|
|
72
|
+
private request;
|
|
73
|
+
}
|
|
74
|
+
/** 创建 CMS 客户端实例 */
|
|
75
|
+
declare function createCmsClient(config: CmsClientConfig): CmsClient;
|
|
76
|
+
|
|
77
|
+
/** CMS API 错误(NormalizedApiError 子类,可被 api-client 的 normalizer 直接抛出) */
|
|
78
|
+
declare class CmsApiError extends NormalizedApiError {
|
|
79
|
+
readonly errorCode: string;
|
|
80
|
+
constructor(statusCode: number, errorCode: string, message: string, body?: unknown);
|
|
81
|
+
get isNotFound(): boolean;
|
|
82
|
+
get isUnauthorized(): boolean;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export { ArticleDetail, ArticleListItem, ArticleListParams, ArticleSearchParams, ArticleSearchResultItem, Author, CategoryNode, CmsApiError, CmsClient, Comment, CreateCommentPayload, Paginated, PaginationParams, RedirectRule, Series, SiteConfig, SiteConfigQuery, SitemapEntry, createCmsClient };
|
|
86
|
+
export type { CmsClientConfig };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { ArticleListParams, Paginated, ArticleListItem, ArticleDetail, ArticleSearchParams, ArticleSearchResultItem, PaginationParams, Author, CategoryNode, Comment, CreateCommentPayload, SitemapEntry, RedirectRule, SiteConfigQuery, SiteConfig, Series } from './types.js';
|
|
2
|
+
export { AuthorProfile, AuthorProfileCta, AuthorProfileQuestion, AuthorProfileSection, AuthorProfileStat, AuthorSocialLink, HreflangEntry, ListResponse, LocalizedOgMetaMap, LocalizedSeoMetaMap, LocalizedString, LocalizedTwitterMetaMap, OgMeta, PaginationMeta, RedirectMatchType, RevalidatePayload, SeoMeta, SiteDeliveryMode, SiteDomain, SiteDomainType, SitemapImageEntry, SitemapVideoEntry, TwitterMeta, WebhookEventType } from './types.js';
|
|
3
|
+
import { HeaderProvider, ErrorNormalizer, NormalizedApiError } from '@stackonward/api-client';
|
|
4
|
+
export { BuildRoutePathOptions, BuildRouteUrlOptions, DEFAULT_ROUTE_RESOURCES, ResolvedRouteLocalePolicy, ResolvedRoutePolicy, RouteLocalePolicy, RouteParams, RoutePolicy, RoutePolicyConfig, RouteResourceDefaults, RouteResources, buildRoutePath, buildRouteUrl, extractRouteParams, extractSourceRouteParams, formatRoutePolicy, hasCustomArticleRoute, isRoutePolicy, mergeRouteParams, resolveRoutePolicy } from './routes.js';
|
|
5
|
+
|
|
6
|
+
/** CMS 客户端配置 */
|
|
7
|
+
interface CmsClientConfig {
|
|
8
|
+
/** CMS API 基础 URL */
|
|
9
|
+
baseUrl: string;
|
|
10
|
+
/** 默认语言 */
|
|
11
|
+
defaultLocale?: string;
|
|
12
|
+
/** 自定义 fetch 实现 */
|
|
13
|
+
fetch?: typeof globalThis.fetch;
|
|
14
|
+
/** 请求超时(毫秒),默认 10000 */
|
|
15
|
+
timeout?: number;
|
|
16
|
+
/** 请求头提供器 */
|
|
17
|
+
headers?: HeaderProvider;
|
|
18
|
+
/** 后端错误格式适配器 */
|
|
19
|
+
errorNormalizer?: ErrorNormalizer;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** CMS Public API 客户端 */
|
|
23
|
+
declare class CmsClient {
|
|
24
|
+
private readonly config;
|
|
25
|
+
private readonly api;
|
|
26
|
+
constructor(config: CmsClientConfig);
|
|
27
|
+
/** 获取文章分页列表 */
|
|
28
|
+
getArticles(params?: ArticleListParams): Promise<Paginated<ArticleListItem>>;
|
|
29
|
+
/** 获取文章详情 */
|
|
30
|
+
getArticle(slug: string, locale?: string): Promise<ArticleDetail>;
|
|
31
|
+
/** 获取相关文章 */
|
|
32
|
+
getRelatedArticles(slug: string, options?: {
|
|
33
|
+
limit?: number;
|
|
34
|
+
locale?: string;
|
|
35
|
+
}): Promise<ArticleListItem[]>;
|
|
36
|
+
/** 搜索已发布文章 */
|
|
37
|
+
searchArticles(params: ArticleSearchParams): Promise<Paginated<ArticleSearchResultItem>>;
|
|
38
|
+
/** 获取作者分页列表 */
|
|
39
|
+
getAuthorsPage(options?: {
|
|
40
|
+
locale?: string;
|
|
41
|
+
} & PaginationParams): Promise<Paginated<Author>>;
|
|
42
|
+
/** 获取所有作者 */
|
|
43
|
+
getAuthors(options?: {
|
|
44
|
+
locale?: string;
|
|
45
|
+
} & PaginationParams): Promise<Author[]>;
|
|
46
|
+
/** 获取作者详情 */
|
|
47
|
+
getAuthor(slug: string, locale?: string): Promise<Author>;
|
|
48
|
+
/** 获取作者的文章分页列表 */
|
|
49
|
+
getAuthorArticlesPage(slug: string, options?: {
|
|
50
|
+
is_pinned?: boolean;
|
|
51
|
+
locale?: string;
|
|
52
|
+
} & PaginationParams): Promise<Paginated<ArticleListItem>>;
|
|
53
|
+
/** 获取作者的文章列表 */
|
|
54
|
+
getAuthorArticles(slug: string, options?: {
|
|
55
|
+
is_pinned?: boolean;
|
|
56
|
+
locale?: string;
|
|
57
|
+
} & PaginationParams): Promise<ArticleListItem[]>;
|
|
58
|
+
/** 获取分类树 */
|
|
59
|
+
getCategories(): Promise<CategoryNode[]>;
|
|
60
|
+
/** 获取文章评论树 */
|
|
61
|
+
getComments(articleSlug: string): Promise<Comment[]>;
|
|
62
|
+
/** 发表评论 */
|
|
63
|
+
createComment(articleSlug: string, payload: CreateCommentPayload): Promise<void>;
|
|
64
|
+
/** 获取 Sitemap 条目 */
|
|
65
|
+
getSitemap(locale?: string): Promise<SitemapEntry[]>;
|
|
66
|
+
/** 获取重定向规则 */
|
|
67
|
+
getRedirects(): Promise<RedirectRule[]>;
|
|
68
|
+
/** 获取站点运行时配置 */
|
|
69
|
+
getSiteConfig(options?: SiteConfigQuery): Promise<SiteConfig>;
|
|
70
|
+
/** 获取文章系列 */
|
|
71
|
+
getSeries(slug: string, locale?: string): Promise<Series>;
|
|
72
|
+
private request;
|
|
73
|
+
}
|
|
74
|
+
/** 创建 CMS 客户端实例 */
|
|
75
|
+
declare function createCmsClient(config: CmsClientConfig): CmsClient;
|
|
76
|
+
|
|
77
|
+
/** CMS API 错误(NormalizedApiError 子类,可被 api-client 的 normalizer 直接抛出) */
|
|
78
|
+
declare class CmsApiError extends NormalizedApiError {
|
|
79
|
+
readonly errorCode: string;
|
|
80
|
+
constructor(statusCode: number, errorCode: string, message: string, body?: unknown);
|
|
81
|
+
get isNotFound(): boolean;
|
|
82
|
+
get isUnauthorized(): boolean;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export { ArticleDetail, ArticleListItem, ArticleListParams, ArticleSearchParams, ArticleSearchResultItem, Author, CategoryNode, CmsApiError, CmsClient, Comment, CreateCommentPayload, Paginated, PaginationParams, RedirectRule, Series, SiteConfig, SiteConfigQuery, SitemapEntry, createCmsClient };
|
|
86
|
+
export type { CmsClientConfig };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { NormalizedApiError, createApiClient, ApiHttpError } from '@stackonward/api-client';
|
|
2
|
+
export { DEFAULT_ROUTE_RESOURCES, buildRoutePath, buildRouteUrl, extractRouteParams, extractSourceRouteParams, formatRoutePolicy, hasCustomArticleRoute, isRoutePolicy, mergeRouteParams, resolveRoutePolicy } from './routes.mjs';
|
|
3
|
+
|
|
4
|
+
const DEFAULT_CONFIG = {
|
|
5
|
+
defaultLocale: "en",
|
|
6
|
+
timeout: 1e4
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
class CmsApiError extends NormalizedApiError {
|
|
10
|
+
errorCode;
|
|
11
|
+
constructor(statusCode, errorCode, message, body) {
|
|
12
|
+
super({ kind: "http", statusCode, code: errorCode, message, body });
|
|
13
|
+
this.name = "CmsApiError";
|
|
14
|
+
this.errorCode = errorCode;
|
|
15
|
+
}
|
|
16
|
+
get isNotFound() {
|
|
17
|
+
return this.statusCode === 404;
|
|
18
|
+
}
|
|
19
|
+
get isUnauthorized() {
|
|
20
|
+
return this.statusCode === 401;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function resolveHeaders(provider, context) {
|
|
25
|
+
if (!provider) {
|
|
26
|
+
return { Accept: "application/json" };
|
|
27
|
+
}
|
|
28
|
+
const configured = typeof provider === "function" ? await provider(context) : provider;
|
|
29
|
+
const headers = new Headers(configured);
|
|
30
|
+
if (!headers.has("Accept")) {
|
|
31
|
+
headers.set("Accept", "application/json");
|
|
32
|
+
}
|
|
33
|
+
return headers;
|
|
34
|
+
}
|
|
35
|
+
function normalizeListResponse(body, endpoint) {
|
|
36
|
+
if (Array.isArray(body)) {
|
|
37
|
+
return body;
|
|
38
|
+
}
|
|
39
|
+
if (typeof body === "object" && body !== null && "data" in body) {
|
|
40
|
+
const data = body.data;
|
|
41
|
+
if (Array.isArray(data)) {
|
|
42
|
+
return data;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
throw new CmsApiError(
|
|
46
|
+
500,
|
|
47
|
+
"INVALID_LIST_RESPONSE",
|
|
48
|
+
`CMS API returned invalid list response for ${endpoint}`,
|
|
49
|
+
body
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
const cmsErrorNormalizer = {
|
|
53
|
+
normalize(raw) {
|
|
54
|
+
if (raw instanceof CmsApiError) return raw;
|
|
55
|
+
if (raw instanceof ApiHttpError) {
|
|
56
|
+
return new CmsApiError(
|
|
57
|
+
raw.status,
|
|
58
|
+
"HTTP_ERROR",
|
|
59
|
+
`CMS request failed with status ${raw.status}`,
|
|
60
|
+
raw.data
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
if (raw instanceof NormalizedApiError) {
|
|
64
|
+
return new CmsApiError(
|
|
65
|
+
raw.statusCode ?? 0,
|
|
66
|
+
String(raw.code ?? raw.kind).toUpperCase(),
|
|
67
|
+
raw.message,
|
|
68
|
+
raw.body
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
return new CmsApiError(
|
|
72
|
+
0,
|
|
73
|
+
"NETWORK_ERROR",
|
|
74
|
+
raw instanceof Error ? raw.message : "CMS request failed",
|
|
75
|
+
raw
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
class CmsClient {
|
|
80
|
+
config;
|
|
81
|
+
api;
|
|
82
|
+
constructor(config) {
|
|
83
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
84
|
+
this.api = createApiClient({
|
|
85
|
+
baseUrl: this.config.baseUrl,
|
|
86
|
+
timeout: this.config.timeout,
|
|
87
|
+
fetch: config.fetch,
|
|
88
|
+
headers: (context) => resolveHeaders(config.headers, context),
|
|
89
|
+
errorNormalizer: config.errorNormalizer ?? cmsErrorNormalizer
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
// ─── 文章 ───────────────────────────────────────────
|
|
93
|
+
/** 获取文章分页列表 */
|
|
94
|
+
async getArticles(params) {
|
|
95
|
+
return this.request(
|
|
96
|
+
"GET",
|
|
97
|
+
"/articles",
|
|
98
|
+
params
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
/** 获取文章详情 */
|
|
102
|
+
async getArticle(slug, locale) {
|
|
103
|
+
return this.request("GET", `/articles/${encodeURIComponent(slug)}`, { locale });
|
|
104
|
+
}
|
|
105
|
+
/** 获取相关文章 */
|
|
106
|
+
async getRelatedArticles(slug, options) {
|
|
107
|
+
const endpoint = `/articles/${encodeURIComponent(slug)}/related`;
|
|
108
|
+
const result = await this.request(
|
|
109
|
+
"GET",
|
|
110
|
+
endpoint,
|
|
111
|
+
options
|
|
112
|
+
);
|
|
113
|
+
return normalizeListResponse(result, endpoint);
|
|
114
|
+
}
|
|
115
|
+
/** 搜索已发布文章 */
|
|
116
|
+
async searchArticles(params) {
|
|
117
|
+
return this.request(
|
|
118
|
+
"GET",
|
|
119
|
+
"/search",
|
|
120
|
+
params
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
// ─── 作者 ───────────────────────────────────────────
|
|
124
|
+
/** 获取作者分页列表 */
|
|
125
|
+
async getAuthorsPage(options) {
|
|
126
|
+
return this.request("GET", "/authors", options);
|
|
127
|
+
}
|
|
128
|
+
/** 获取所有作者 */
|
|
129
|
+
async getAuthors(options) {
|
|
130
|
+
const result = await this.getAuthorsPage(options);
|
|
131
|
+
return normalizeListResponse(result, "/authors");
|
|
132
|
+
}
|
|
133
|
+
/** 获取作者详情 */
|
|
134
|
+
async getAuthor(slug, locale) {
|
|
135
|
+
return this.request("GET", `/authors/${encodeURIComponent(slug)}`, { locale });
|
|
136
|
+
}
|
|
137
|
+
/** 获取作者的文章分页列表 */
|
|
138
|
+
async getAuthorArticlesPage(slug, options) {
|
|
139
|
+
return this.request(
|
|
140
|
+
"GET",
|
|
141
|
+
`/authors/${encodeURIComponent(slug)}/articles`,
|
|
142
|
+
options
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
/** 获取作者的文章列表 */
|
|
146
|
+
async getAuthorArticles(slug, options) {
|
|
147
|
+
const result = await this.getAuthorArticlesPage(slug, options);
|
|
148
|
+
return normalizeListResponse(
|
|
149
|
+
result,
|
|
150
|
+
`/authors/${encodeURIComponent(slug)}/articles`
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
// ─── 分类 ───────────────────────────────────────────
|
|
154
|
+
/** 获取分类树 */
|
|
155
|
+
async getCategories() {
|
|
156
|
+
const result = await this.request(
|
|
157
|
+
"GET",
|
|
158
|
+
"/categories"
|
|
159
|
+
);
|
|
160
|
+
return normalizeListResponse(result, "/categories");
|
|
161
|
+
}
|
|
162
|
+
// ─── 评论 ───────────────────────────────────────────
|
|
163
|
+
/** 获取文章评论树 */
|
|
164
|
+
async getComments(articleSlug) {
|
|
165
|
+
return this.request("GET", `/articles/${encodeURIComponent(articleSlug)}/comments`);
|
|
166
|
+
}
|
|
167
|
+
/** 发表评论 */
|
|
168
|
+
async createComment(articleSlug, payload) {
|
|
169
|
+
return this.request(
|
|
170
|
+
"POST",
|
|
171
|
+
`/articles/${encodeURIComponent(articleSlug)}/comments`,
|
|
172
|
+
void 0,
|
|
173
|
+
payload
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
// ─── Sitemap / 重定向 / 系列 ──────────────────────
|
|
177
|
+
/** 获取 Sitemap 条目 */
|
|
178
|
+
async getSitemap(locale) {
|
|
179
|
+
const result = await this.request(
|
|
180
|
+
"GET",
|
|
181
|
+
"/sitemap",
|
|
182
|
+
{ locale }
|
|
183
|
+
);
|
|
184
|
+
return normalizeListResponse(result, "/sitemap");
|
|
185
|
+
}
|
|
186
|
+
/** 获取重定向规则 */
|
|
187
|
+
async getRedirects() {
|
|
188
|
+
const result = await this.request(
|
|
189
|
+
"GET",
|
|
190
|
+
"/redirects"
|
|
191
|
+
);
|
|
192
|
+
return normalizeListResponse(result, "/redirects");
|
|
193
|
+
}
|
|
194
|
+
/** 获取站点运行时配置 */
|
|
195
|
+
async getSiteConfig(options) {
|
|
196
|
+
return this.request("GET", "/site", options, void 0, {
|
|
197
|
+
defaultLocale: false
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
/** 获取文章系列 */
|
|
201
|
+
async getSeries(slug, locale) {
|
|
202
|
+
return this.request("GET", `/series/${encodeURIComponent(slug)}`, { locale });
|
|
203
|
+
}
|
|
204
|
+
// ─── 内部请求方法 ──────────────────────────────────
|
|
205
|
+
async request(method, path, query, body, options) {
|
|
206
|
+
let finalQuery = query;
|
|
207
|
+
if (method === "GET" && options?.defaultLocale !== false && this.config.defaultLocale) {
|
|
208
|
+
const q = { ...query ?? {} };
|
|
209
|
+
if (q.locale === void 0 || q.locale === null) {
|
|
210
|
+
q.locale = this.config.defaultLocale;
|
|
211
|
+
}
|
|
212
|
+
finalQuery = q;
|
|
213
|
+
}
|
|
214
|
+
return this.api.request(method, path, { query: finalQuery, body });
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
function createCmsClient(config) {
|
|
218
|
+
return new CmsClient(config);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export { CmsApiError, CmsClient, createCmsClient };
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
type RouteParams = Record<string, string | number | boolean | null | undefined>;
|
|
2
|
+
interface RouteResources {
|
|
3
|
+
index?: string;
|
|
4
|
+
category?: string;
|
|
5
|
+
article?: string;
|
|
6
|
+
author?: string;
|
|
7
|
+
[resource: string]: string | undefined;
|
|
8
|
+
}
|
|
9
|
+
interface RouteLocalePolicy {
|
|
10
|
+
defaultLocale?: string;
|
|
11
|
+
prefixes?: Record<string, string>;
|
|
12
|
+
includeDefaultLocale?: boolean;
|
|
13
|
+
}
|
|
14
|
+
interface RoutePolicy {
|
|
15
|
+
resources?: RouteResources;
|
|
16
|
+
sourceResources?: RouteResources;
|
|
17
|
+
locale?: RouteLocalePolicy;
|
|
18
|
+
}
|
|
19
|
+
interface RoutePolicyConfig {
|
|
20
|
+
defaultLocale?: string;
|
|
21
|
+
localePrefixes?: Record<string, string>;
|
|
22
|
+
routes?: RoutePolicy;
|
|
23
|
+
}
|
|
24
|
+
interface BuildRoutePathOptions {
|
|
25
|
+
includeLocale?: boolean;
|
|
26
|
+
}
|
|
27
|
+
interface BuildRouteUrlOptions extends BuildRoutePathOptions {
|
|
28
|
+
}
|
|
29
|
+
interface ResolvedRouteLocalePolicy {
|
|
30
|
+
defaultLocale: string;
|
|
31
|
+
prefixes: Record<string, string>;
|
|
32
|
+
includeDefaultLocale: boolean;
|
|
33
|
+
}
|
|
34
|
+
type RouteResourceDefaults = RouteResources & Required<Pick<RouteResources, 'article' | 'author' | 'category' | 'index'>>;
|
|
35
|
+
interface ResolvedRoutePolicy {
|
|
36
|
+
resources: RouteResourceDefaults;
|
|
37
|
+
sourceResources: RouteResourceDefaults;
|
|
38
|
+
locale: ResolvedRouteLocalePolicy;
|
|
39
|
+
}
|
|
40
|
+
declare const DEFAULT_ROUTE_RESOURCES: RouteResourceDefaults;
|
|
41
|
+
declare function resolveRoutePolicy(config?: RoutePolicyConfig): ResolvedRoutePolicy;
|
|
42
|
+
declare function buildRoutePath(policy: RoutePolicy, resource: string, params?: RouteParams, options?: BuildRoutePathOptions): string;
|
|
43
|
+
declare function buildRouteUrl(baseUrl: string, policy: null | RoutePolicy | undefined, resource: string, params?: RouteParams, options?: BuildRouteUrlOptions): string;
|
|
44
|
+
declare function isRoutePolicy(value: unknown): value is RoutePolicy;
|
|
45
|
+
declare function hasCustomArticleRoute(policy?: null | RoutePolicy): boolean;
|
|
46
|
+
declare function formatRoutePolicy(policy?: null | RoutePolicy): string;
|
|
47
|
+
declare function extractRouteParams(pattern: string, path: string): RouteParams | null;
|
|
48
|
+
declare function extractSourceRouteParams(policy: RoutePolicy, resource: string, path: string): RouteParams | null;
|
|
49
|
+
declare function mergeRouteParams(left: RouteParams | null | undefined, right: RouteParams | null | undefined): {
|
|
50
|
+
[x: string]: string | number | boolean | null | undefined;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export { DEFAULT_ROUTE_RESOURCES, buildRoutePath, buildRouteUrl, extractRouteParams, extractSourceRouteParams, formatRoutePolicy, hasCustomArticleRoute, isRoutePolicy, mergeRouteParams, resolveRoutePolicy };
|
|
54
|
+
export type { BuildRoutePathOptions, BuildRouteUrlOptions, ResolvedRouteLocalePolicy, ResolvedRoutePolicy, RouteLocalePolicy, RouteParams, RoutePolicy, RoutePolicyConfig, RouteResourceDefaults, RouteResources };
|
package/dist/routes.d.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
type RouteParams = Record<string, string | number | boolean | null | undefined>;
|
|
2
|
+
interface RouteResources {
|
|
3
|
+
index?: string;
|
|
4
|
+
category?: string;
|
|
5
|
+
article?: string;
|
|
6
|
+
author?: string;
|
|
7
|
+
[resource: string]: string | undefined;
|
|
8
|
+
}
|
|
9
|
+
interface RouteLocalePolicy {
|
|
10
|
+
defaultLocale?: string;
|
|
11
|
+
prefixes?: Record<string, string>;
|
|
12
|
+
includeDefaultLocale?: boolean;
|
|
13
|
+
}
|
|
14
|
+
interface RoutePolicy {
|
|
15
|
+
resources?: RouteResources;
|
|
16
|
+
sourceResources?: RouteResources;
|
|
17
|
+
locale?: RouteLocalePolicy;
|
|
18
|
+
}
|
|
19
|
+
interface RoutePolicyConfig {
|
|
20
|
+
defaultLocale?: string;
|
|
21
|
+
localePrefixes?: Record<string, string>;
|
|
22
|
+
routes?: RoutePolicy;
|
|
23
|
+
}
|
|
24
|
+
interface BuildRoutePathOptions {
|
|
25
|
+
includeLocale?: boolean;
|
|
26
|
+
}
|
|
27
|
+
interface BuildRouteUrlOptions extends BuildRoutePathOptions {
|
|
28
|
+
}
|
|
29
|
+
interface ResolvedRouteLocalePolicy {
|
|
30
|
+
defaultLocale: string;
|
|
31
|
+
prefixes: Record<string, string>;
|
|
32
|
+
includeDefaultLocale: boolean;
|
|
33
|
+
}
|
|
34
|
+
type RouteResourceDefaults = RouteResources & Required<Pick<RouteResources, 'article' | 'author' | 'category' | 'index'>>;
|
|
35
|
+
interface ResolvedRoutePolicy {
|
|
36
|
+
resources: RouteResourceDefaults;
|
|
37
|
+
sourceResources: RouteResourceDefaults;
|
|
38
|
+
locale: ResolvedRouteLocalePolicy;
|
|
39
|
+
}
|
|
40
|
+
declare const DEFAULT_ROUTE_RESOURCES: RouteResourceDefaults;
|
|
41
|
+
declare function resolveRoutePolicy(config?: RoutePolicyConfig): ResolvedRoutePolicy;
|
|
42
|
+
declare function buildRoutePath(policy: RoutePolicy, resource: string, params?: RouteParams, options?: BuildRoutePathOptions): string;
|
|
43
|
+
declare function buildRouteUrl(baseUrl: string, policy: null | RoutePolicy | undefined, resource: string, params?: RouteParams, options?: BuildRouteUrlOptions): string;
|
|
44
|
+
declare function isRoutePolicy(value: unknown): value is RoutePolicy;
|
|
45
|
+
declare function hasCustomArticleRoute(policy?: null | RoutePolicy): boolean;
|
|
46
|
+
declare function formatRoutePolicy(policy?: null | RoutePolicy): string;
|
|
47
|
+
declare function extractRouteParams(pattern: string, path: string): RouteParams | null;
|
|
48
|
+
declare function extractSourceRouteParams(policy: RoutePolicy, resource: string, path: string): RouteParams | null;
|
|
49
|
+
declare function mergeRouteParams(left: RouteParams | null | undefined, right: RouteParams | null | undefined): {
|
|
50
|
+
[x: string]: string | number | boolean | null | undefined;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export { DEFAULT_ROUTE_RESOURCES, buildRoutePath, buildRouteUrl, extractRouteParams, extractSourceRouteParams, formatRoutePolicy, hasCustomArticleRoute, isRoutePolicy, mergeRouteParams, resolveRoutePolicy };
|
|
54
|
+
export type { BuildRoutePathOptions, BuildRouteUrlOptions, ResolvedRouteLocalePolicy, ResolvedRoutePolicy, RouteLocalePolicy, RouteParams, RoutePolicy, RoutePolicyConfig, RouteResourceDefaults, RouteResources };
|