@unchainedshop/cockpit-api 1.0.33 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +330 -65
- package/dist/client.d.ts +72 -0
- package/dist/client.js +101 -0
- package/dist/cockpit-logger.d.ts +8 -10
- package/dist/cockpit-logger.js +7 -21
- package/dist/core/cache.d.ts +19 -0
- package/dist/core/cache.js +32 -0
- package/dist/core/config.d.ts +45 -0
- package/dist/core/config.js +32 -0
- package/dist/core/http.d.ts +34 -0
- package/dist/core/http.js +98 -0
- package/dist/core/index.d.ts +14 -0
- package/dist/core/index.js +10 -0
- package/dist/core/locale.d.ts +11 -0
- package/dist/core/locale.js +17 -0
- package/dist/core/query-string.d.ts +18 -0
- package/dist/core/query-string.js +20 -0
- package/dist/core/url-builder.d.ts +22 -0
- package/dist/core/url-builder.js +35 -0
- package/dist/core/validation.d.ts +13 -0
- package/dist/core/validation.js +22 -0
- package/dist/fetch/client.d.ts +85 -0
- package/dist/fetch/client.js +143 -0
- package/dist/fetch/index.d.ts +19 -0
- package/dist/fetch/index.js +18 -0
- package/dist/index.d.ts +28 -2
- package/dist/index.js +13 -3
- package/dist/methods/assets.d.ts +65 -0
- package/dist/methods/assets.js +37 -0
- package/dist/methods/content.d.ts +77 -0
- package/dist/methods/content.js +79 -0
- package/dist/methods/graphql.d.ts +9 -0
- package/dist/methods/graphql.js +13 -0
- package/dist/methods/index.d.ts +22 -0
- package/dist/methods/index.js +12 -0
- package/dist/methods/localize.d.ts +12 -0
- package/dist/methods/localize.js +17 -0
- package/dist/methods/menus.d.ts +42 -0
- package/dist/methods/menus.js +25 -0
- package/dist/methods/pages.d.ts +49 -0
- package/dist/methods/pages.js +34 -0
- package/dist/methods/routes.d.ts +46 -0
- package/dist/methods/routes.js +24 -0
- package/dist/methods/search.d.ts +27 -0
- package/dist/methods/search.js +16 -0
- package/dist/methods/system.d.ts +15 -0
- package/dist/methods/system.js +14 -0
- package/dist/schema/executor.d.ts +67 -0
- package/dist/schema/executor.js +81 -0
- package/dist/schema/index.d.ts +26 -0
- package/dist/schema/index.js +26 -0
- package/dist/schema/schema-builder.d.ts +42 -0
- package/dist/schema/schema-builder.js +77 -0
- package/dist/transformers/asset-path.d.ts +20 -0
- package/dist/transformers/asset-path.js +40 -0
- package/dist/transformers/compose.d.ts +9 -0
- package/dist/transformers/compose.js +14 -0
- package/dist/transformers/image-path.d.ts +28 -0
- package/dist/transformers/image-path.js +60 -0
- package/dist/transformers/index.d.ts +8 -0
- package/dist/transformers/index.js +9 -0
- package/dist/transformers/page-link.d.ts +18 -0
- package/dist/transformers/page-link.js +45 -0
- package/dist/utils/index.d.ts +5 -0
- package/dist/utils/index.js +5 -0
- package/dist/utils/route-map.d.ts +12 -0
- package/dist/utils/route-map.js +86 -0
- package/dist/utils/tenant.d.ts +71 -0
- package/dist/utils/tenant.js +88 -0
- package/package.json +50 -8
- package/.github/workflows/npm-publish.yml +0 -33
- package/.nvmrc +0 -1
- package/dist/api.d.ts +0 -70
- package/dist/api.js +0 -303
- package/dist/api.js.map +0 -1
- package/dist/cockpit-logger.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/src/api.ts +0 -350
- package/src/cockpit-logger.ts +0 -24
- package/src/index.ts +0 -3
- package/tsconfig.json +0 -23
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cache management with LRU cache and tenant isolation
|
|
3
|
+
*/
|
|
4
|
+
import { LRUCache } from "lru-cache";
|
|
5
|
+
/**
|
|
6
|
+
* Creates a cache manager with prefixed keys
|
|
7
|
+
* Each call creates a new LRU cache instance - no shared state
|
|
8
|
+
*/
|
|
9
|
+
export function createCacheManager(cachePrefix, options = {}) {
|
|
10
|
+
const cache = new LRUCache({
|
|
11
|
+
max: options.max ?? 100,
|
|
12
|
+
ttl: options.ttl ?? 100000,
|
|
13
|
+
allowStale: false,
|
|
14
|
+
});
|
|
15
|
+
const prefixedKey = (key) => `${cachePrefix}${key}`;
|
|
16
|
+
return {
|
|
17
|
+
get(key) {
|
|
18
|
+
return cache.get(prefixedKey(key));
|
|
19
|
+
},
|
|
20
|
+
set(key, value) {
|
|
21
|
+
cache.set(prefixedKey(key), value);
|
|
22
|
+
},
|
|
23
|
+
clear(pattern) {
|
|
24
|
+
const prefix = pattern !== undefined ? `${cachePrefix}${pattern}` : cachePrefix;
|
|
25
|
+
for (const key of cache.keys()) {
|
|
26
|
+
if (key.startsWith(prefix)) {
|
|
27
|
+
cache.delete(key);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration management for Cockpit API client
|
|
3
|
+
*/
|
|
4
|
+
export interface CockpitAPIOptions {
|
|
5
|
+
/** Cockpit CMS endpoint URL (falls back to COCKPIT_GRAPHQL_ENDPOINT env var) */
|
|
6
|
+
endpoint?: string;
|
|
7
|
+
/** Tenant name for multi-tenant setups */
|
|
8
|
+
tenant?: string;
|
|
9
|
+
/** API key (falls back to COCKPIT_SECRET env var) */
|
|
10
|
+
apiKey?: string;
|
|
11
|
+
/** Use admin access with API key */
|
|
12
|
+
useAdminAccess?: boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Default language that maps to Cockpit's "default" locale.
|
|
15
|
+
* When a request uses this language, it will be sent as "default" to Cockpit.
|
|
16
|
+
* @default "de"
|
|
17
|
+
*/
|
|
18
|
+
defaultLanguage?: string;
|
|
19
|
+
/** Cache configuration */
|
|
20
|
+
cache?: {
|
|
21
|
+
/** Max entries (falls back to COCKPIT_CACHE_MAX env var, default: 100) */
|
|
22
|
+
max?: number;
|
|
23
|
+
/** TTL in ms (falls back to COCKPIT_CACHE_TTL env var, default: 100000) */
|
|
24
|
+
ttl?: number;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Preload route replacements during client initialization.
|
|
28
|
+
* When true, fetches page routes to enable `pages://id` link resolution in responses.
|
|
29
|
+
* When false (default), skips the network request for faster cold starts.
|
|
30
|
+
* @default false
|
|
31
|
+
*/
|
|
32
|
+
preloadRoutes?: boolean;
|
|
33
|
+
}
|
|
34
|
+
export interface CockpitConfig {
|
|
35
|
+
readonly endpoint: URL;
|
|
36
|
+
readonly tenant?: string;
|
|
37
|
+
readonly apiKey?: string;
|
|
38
|
+
readonly useAdminAccess: boolean;
|
|
39
|
+
readonly defaultLanguage: string;
|
|
40
|
+
readonly cachePrefix: string;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Creates an immutable configuration object for the Cockpit API client
|
|
44
|
+
*/
|
|
45
|
+
export declare function createConfig(options?: CockpitAPIOptions): CockpitConfig;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration management for Cockpit API client
|
|
3
|
+
*/
|
|
4
|
+
import { resolveApiKey } from "../utils/tenant.js";
|
|
5
|
+
/** Valid tenant format: alphanumeric, hyphens, underscores only */
|
|
6
|
+
const VALID_TENANT_PATTERN = /^[a-z0-9_-]+$/i;
|
|
7
|
+
/**
|
|
8
|
+
* Creates an immutable configuration object for the Cockpit API client
|
|
9
|
+
*/
|
|
10
|
+
export function createConfig(options = {}) {
|
|
11
|
+
const endpointStr = options.endpoint ?? process.env["COCKPIT_GRAPHQL_ENDPOINT"];
|
|
12
|
+
if (endpointStr === undefined || endpointStr === "") {
|
|
13
|
+
throw new Error("Cockpit: endpoint is required (provide via options or COCKPIT_GRAPHQL_ENDPOINT env var)");
|
|
14
|
+
}
|
|
15
|
+
// Validate tenant format to prevent path traversal
|
|
16
|
+
if (options.tenant !== undefined &&
|
|
17
|
+
!VALID_TENANT_PATTERN.test(options.tenant)) {
|
|
18
|
+
throw new Error("Cockpit: Invalid tenant format (only alphanumeric, hyphens, and underscores allowed)");
|
|
19
|
+
}
|
|
20
|
+
const endpoint = new URL(endpointStr);
|
|
21
|
+
const apiKey = resolveApiKey(options.tenant, options);
|
|
22
|
+
// Build config object with all properties before freezing
|
|
23
|
+
const config = Object.freeze({
|
|
24
|
+
endpoint,
|
|
25
|
+
useAdminAccess: options.useAdminAccess ?? false,
|
|
26
|
+
defaultLanguage: options.defaultLanguage ?? "de",
|
|
27
|
+
cachePrefix: `${endpointStr}:${options.tenant ?? "default"}:`,
|
|
28
|
+
...(options.tenant !== undefined && { tenant: options.tenant }),
|
|
29
|
+
...(apiKey !== undefined && { apiKey }),
|
|
30
|
+
});
|
|
31
|
+
return config;
|
|
32
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP client for Cockpit API requests
|
|
3
|
+
*/
|
|
4
|
+
import type { CockpitConfig } from "./config.ts";
|
|
5
|
+
import type { ResponseTransformer } from "../transformers/image-path.ts";
|
|
6
|
+
/**
|
|
7
|
+
* Per-request options that can override client-level settings
|
|
8
|
+
*/
|
|
9
|
+
export interface HttpFetchOptions extends RequestInit {
|
|
10
|
+
/** Override the client-level useAdminAccess setting for this request */
|
|
11
|
+
useAdminAccess?: boolean;
|
|
12
|
+
}
|
|
13
|
+
export interface HttpClient {
|
|
14
|
+
/**
|
|
15
|
+
* Make a GET request expecting JSON response
|
|
16
|
+
*/
|
|
17
|
+
fetch<T>(url: URL | string, options?: HttpFetchOptions): Promise<T | null>;
|
|
18
|
+
/**
|
|
19
|
+
* Make a GET request expecting text response (e.g., URL strings)
|
|
20
|
+
*/
|
|
21
|
+
fetchText(url: URL | string, options?: HttpFetchOptions): Promise<string | null>;
|
|
22
|
+
/**
|
|
23
|
+
* Make a POST request with JSON body
|
|
24
|
+
*/
|
|
25
|
+
post<T>(url: URL | string, body: unknown, options?: HttpFetchOptions): Promise<T | null>;
|
|
26
|
+
/**
|
|
27
|
+
* Make a DELETE request
|
|
28
|
+
*/
|
|
29
|
+
delete<T>(url: URL | string, options?: HttpFetchOptions): Promise<T | null>;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Creates an HTTP client with authentication and response transformation
|
|
33
|
+
*/
|
|
34
|
+
export declare function createHttpClient(config: CockpitConfig, transformer: ResponseTransformer): HttpClient;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP client for Cockpit API requests
|
|
3
|
+
*/
|
|
4
|
+
import { logger } from "../cockpit-logger.js";
|
|
5
|
+
/**
|
|
6
|
+
* Normalizes headers to Record<string, string>
|
|
7
|
+
*/
|
|
8
|
+
function normalizeHeaders(headers) {
|
|
9
|
+
if (!headers || typeof headers !== "object" || Array.isArray(headers)) {
|
|
10
|
+
return {};
|
|
11
|
+
}
|
|
12
|
+
return headers;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Prepares request options for POST/DELETE requests
|
|
16
|
+
*/
|
|
17
|
+
function prepareJsonRequestOptions(options, method, body) {
|
|
18
|
+
const { useAdminAccess, headers, ...restOptions } = options;
|
|
19
|
+
const customHeaders = normalizeHeaders(headers);
|
|
20
|
+
const fetchOpts = {
|
|
21
|
+
...restOptions,
|
|
22
|
+
method,
|
|
23
|
+
headers: { "Content-Type": "application/json", ...customHeaders },
|
|
24
|
+
};
|
|
25
|
+
if (body !== undefined)
|
|
26
|
+
fetchOpts.body = JSON.stringify(body);
|
|
27
|
+
if (useAdminAccess !== undefined)
|
|
28
|
+
fetchOpts.useAdminAccess = useAdminAccess;
|
|
29
|
+
return fetchOpts;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Creates an HTTP client with authentication and response transformation
|
|
33
|
+
*/
|
|
34
|
+
export function createHttpClient(config, transformer) {
|
|
35
|
+
/**
|
|
36
|
+
* Build headers with optional admin access override
|
|
37
|
+
* @param custom - Custom headers to include
|
|
38
|
+
* @param useAdminAccess - Per-request override (undefined = use config default)
|
|
39
|
+
*/
|
|
40
|
+
const buildHeaders = (custom = {}, useAdminAccess) => {
|
|
41
|
+
const headers = { ...custom };
|
|
42
|
+
// Use per-request setting if provided, otherwise fall back to config
|
|
43
|
+
const shouldUseAdmin = useAdminAccess ?? config.useAdminAccess;
|
|
44
|
+
if (shouldUseAdmin && config.apiKey !== undefined) {
|
|
45
|
+
headers["api-Key"] = config.apiKey;
|
|
46
|
+
}
|
|
47
|
+
return headers;
|
|
48
|
+
};
|
|
49
|
+
const handleErrorResponse = async (response) => {
|
|
50
|
+
const errorText = await response.text();
|
|
51
|
+
logger.error(`Cockpit: Error accessing ${response.url}`, {
|
|
52
|
+
status: response.status,
|
|
53
|
+
body: errorText,
|
|
54
|
+
});
|
|
55
|
+
throw new Error(`Cockpit: Error accessing ${response.url} (${String(response.status)}): ${errorText}`);
|
|
56
|
+
};
|
|
57
|
+
const handleJsonResponse = async (response) => {
|
|
58
|
+
if (response.status === 404)
|
|
59
|
+
return null;
|
|
60
|
+
if (!response.ok)
|
|
61
|
+
return handleErrorResponse(response);
|
|
62
|
+
const json = await response.json();
|
|
63
|
+
return transformer.transform(json);
|
|
64
|
+
};
|
|
65
|
+
const handleTextResponse = async (response) => {
|
|
66
|
+
if (response.status === 404)
|
|
67
|
+
return null;
|
|
68
|
+
if (!response.ok)
|
|
69
|
+
return handleErrorResponse(response);
|
|
70
|
+
return response.text();
|
|
71
|
+
};
|
|
72
|
+
const doFetch = async (url, options = {}) => {
|
|
73
|
+
const { useAdminAccess, ...fetchOptions } = options;
|
|
74
|
+
logger.debug(`Cockpit: Requesting ${String(url)}`);
|
|
75
|
+
return fetch(url, {
|
|
76
|
+
...fetchOptions,
|
|
77
|
+
headers: buildHeaders(fetchOptions.headers, useAdminAccess),
|
|
78
|
+
});
|
|
79
|
+
};
|
|
80
|
+
const fetchData = async (url, options = {}) => {
|
|
81
|
+
const response = await doFetch(url, options);
|
|
82
|
+
return handleJsonResponse(response);
|
|
83
|
+
};
|
|
84
|
+
const fetchTextData = async (url, options = {}) => {
|
|
85
|
+
const response = await doFetch(url, options);
|
|
86
|
+
return handleTextResponse(response);
|
|
87
|
+
};
|
|
88
|
+
return {
|
|
89
|
+
fetch: fetchData,
|
|
90
|
+
fetchText: fetchTextData,
|
|
91
|
+
async post(url, body, options = {}) {
|
|
92
|
+
return fetchData(url, prepareJsonRequestOptions(options, "POST", body));
|
|
93
|
+
},
|
|
94
|
+
async delete(url, options = {}) {
|
|
95
|
+
return fetchData(url, prepareJsonRequestOptions(options, "DELETE"));
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core module exports
|
|
3
|
+
*/
|
|
4
|
+
export { buildQueryString, encodeQueryParam } from "./query-string.ts";
|
|
5
|
+
export type { CacheManager, CacheOptions } from "./cache.ts";
|
|
6
|
+
export { createCacheManager } from "./cache.ts";
|
|
7
|
+
export type { CockpitConfig } from "./config.ts";
|
|
8
|
+
export { createConfig } from "./config.ts";
|
|
9
|
+
export type { UrlBuilder, UrlBuildOptions } from "./url-builder.ts";
|
|
10
|
+
export { createUrlBuilder } from "./url-builder.ts";
|
|
11
|
+
export type { HttpClient } from "./http.ts";
|
|
12
|
+
export { createHttpClient } from "./http.ts";
|
|
13
|
+
export { requireParam, validatePathSegment } from "./validation.ts";
|
|
14
|
+
export { createLocaleNormalizer } from "./locale.ts";
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core module exports
|
|
3
|
+
*/
|
|
4
|
+
export { buildQueryString, encodeQueryParam } from "./query-string.js";
|
|
5
|
+
export { createCacheManager } from "./cache.js";
|
|
6
|
+
export { createConfig } from "./config.js";
|
|
7
|
+
export { createUrlBuilder } from "./url-builder.js";
|
|
8
|
+
export { createHttpClient } from "./http.js";
|
|
9
|
+
export { requireParam, validatePathSegment } from "./validation.js";
|
|
10
|
+
export { createLocaleNormalizer } from "./locale.js";
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared locale utilities
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Creates a locale normalizer for the given default language.
|
|
6
|
+
* Maps the default language to Cockpit's "default" locale.
|
|
7
|
+
*
|
|
8
|
+
* @param defaultLanguage - The language that should map to "default"
|
|
9
|
+
* @returns A function that normalizes locale strings
|
|
10
|
+
*/
|
|
11
|
+
export declare function createLocaleNormalizer(defaultLanguage: string): (locale?: string) => string;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared locale utilities
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Creates a locale normalizer for the given default language.
|
|
6
|
+
* Maps the default language to Cockpit's "default" locale.
|
|
7
|
+
*
|
|
8
|
+
* @param defaultLanguage - The language that should map to "default"
|
|
9
|
+
* @returns A function that normalizes locale strings
|
|
10
|
+
*/
|
|
11
|
+
export function createLocaleNormalizer(defaultLanguage) {
|
|
12
|
+
return (locale) => {
|
|
13
|
+
if (locale === undefined || locale === defaultLanguage)
|
|
14
|
+
return "default";
|
|
15
|
+
return locale;
|
|
16
|
+
};
|
|
17
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Query string encoding utilities
|
|
3
|
+
*/
|
|
4
|
+
/** Primitive values that can be used as query parameters */
|
|
5
|
+
export type QueryParamPrimitive = string | number | boolean | null | undefined;
|
|
6
|
+
/** Object values that will be JSON stringified */
|
|
7
|
+
export type QueryParamObject = Record<string, QueryParamPrimitive | QueryParamPrimitive[]>;
|
|
8
|
+
/** All allowed query parameter value types */
|
|
9
|
+
export type QueryParamValue = QueryParamPrimitive | QueryParamObject | QueryParamValue[];
|
|
10
|
+
/**
|
|
11
|
+
* Encodes a single query parameter key-value pair
|
|
12
|
+
*/
|
|
13
|
+
export declare const encodeQueryParam: (key: string, value: QueryParamValue) => string;
|
|
14
|
+
/**
|
|
15
|
+
* Builds a query string from an object of parameters
|
|
16
|
+
* Filters out null and undefined values
|
|
17
|
+
*/
|
|
18
|
+
export declare const buildQueryString: (params: Record<string, QueryParamValue>) => string | null;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Query string encoding utilities
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Encodes a single query parameter key-value pair
|
|
6
|
+
*/
|
|
7
|
+
export const encodeQueryParam = (key, value) => {
|
|
8
|
+
const encodedValue = typeof value === "string" ? value : JSON.stringify(value);
|
|
9
|
+
return `${encodeURIComponent(key)}=${encodeURIComponent(encodedValue)}`;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Builds a query string from an object of parameters
|
|
13
|
+
* Filters out null and undefined values
|
|
14
|
+
*/
|
|
15
|
+
export const buildQueryString = (params) => {
|
|
16
|
+
const entries = Object.entries(params).filter(([, v]) => v !== undefined && v !== null);
|
|
17
|
+
if (!entries.length)
|
|
18
|
+
return null;
|
|
19
|
+
return entries.map(([key, value]) => encodeQueryParam(key, value)).join("&");
|
|
20
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* URL construction utilities
|
|
3
|
+
*/
|
|
4
|
+
import type { CockpitConfig } from "./config.ts";
|
|
5
|
+
export interface UrlBuildOptions {
|
|
6
|
+
locale?: string;
|
|
7
|
+
queryParams?: Record<string, unknown>;
|
|
8
|
+
}
|
|
9
|
+
export interface UrlBuilder {
|
|
10
|
+
/**
|
|
11
|
+
* Build a URL for an API endpoint
|
|
12
|
+
*/
|
|
13
|
+
build(path: string, options?: UrlBuildOptions): URL;
|
|
14
|
+
/**
|
|
15
|
+
* Get the GraphQL endpoint URL
|
|
16
|
+
*/
|
|
17
|
+
graphqlEndpoint(): URL;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Creates a URL builder for the given configuration
|
|
21
|
+
*/
|
|
22
|
+
export declare function createUrlBuilder(config: CockpitConfig): UrlBuilder;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* URL construction utilities
|
|
3
|
+
*/
|
|
4
|
+
import { buildQueryString } from "./query-string.js";
|
|
5
|
+
import { createLocaleNormalizer } from "./locale.js";
|
|
6
|
+
/**
|
|
7
|
+
* Creates a URL builder for the given configuration
|
|
8
|
+
*/
|
|
9
|
+
export function createUrlBuilder(config) {
|
|
10
|
+
const apiBasePath = config.tenant !== undefined ? `/:${config.tenant}/api` : "/api";
|
|
11
|
+
const normalizeLocale = createLocaleNormalizer(config.defaultLanguage);
|
|
12
|
+
return {
|
|
13
|
+
build(path, options = {}) {
|
|
14
|
+
const { locale = "default", queryParams = {} } = options;
|
|
15
|
+
const normalizedLocale = normalizeLocale(locale);
|
|
16
|
+
const url = new URL(config.endpoint);
|
|
17
|
+
url.pathname = `${apiBasePath}${path}`;
|
|
18
|
+
const queryString = buildQueryString({
|
|
19
|
+
...queryParams,
|
|
20
|
+
locale: normalizedLocale,
|
|
21
|
+
});
|
|
22
|
+
if (queryString !== null) {
|
|
23
|
+
url.search = queryString;
|
|
24
|
+
}
|
|
25
|
+
return url;
|
|
26
|
+
},
|
|
27
|
+
graphqlEndpoint() {
|
|
28
|
+
const url = new URL(config.endpoint);
|
|
29
|
+
if (config.tenant !== undefined) {
|
|
30
|
+
url.pathname = `/:${config.tenant}${url.pathname}`;
|
|
31
|
+
}
|
|
32
|
+
return url;
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared validation utilities
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Validates that a required parameter is present
|
|
6
|
+
* @throws Error if value is undefined, null, or empty string
|
|
7
|
+
*/
|
|
8
|
+
export declare const requireParam: (value: unknown, name: string) => void;
|
|
9
|
+
/**
|
|
10
|
+
* Validates path segment format to prevent path traversal
|
|
11
|
+
* @throws Error if value contains invalid characters
|
|
12
|
+
*/
|
|
13
|
+
export declare const validatePathSegment: (value: string, name: string) => void;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared validation utilities
|
|
3
|
+
*/
|
|
4
|
+
/** Valid path segment format: alphanumeric, hyphens, underscores only */
|
|
5
|
+
const VALID_PATH_SEGMENT = /^[a-zA-Z0-9_-]+$/;
|
|
6
|
+
/**
|
|
7
|
+
* Validates that a required parameter is present
|
|
8
|
+
* @throws Error if value is undefined, null, or empty string
|
|
9
|
+
*/
|
|
10
|
+
export const requireParam = (value, name) => {
|
|
11
|
+
if (value === undefined || value === null || value === "")
|
|
12
|
+
throw new Error(`Cockpit: Please provide ${name}`);
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Validates path segment format to prevent path traversal
|
|
16
|
+
* @throws Error if value contains invalid characters
|
|
17
|
+
*/
|
|
18
|
+
export const validatePathSegment = (value, name) => {
|
|
19
|
+
if (!VALID_PATH_SEGMENT.test(value)) {
|
|
20
|
+
throw new Error(`Cockpit: Invalid ${name} format (only alphanumeric, hyphens, and underscores allowed)`);
|
|
21
|
+
}
|
|
22
|
+
};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lightweight fetch client for Cockpit CMS
|
|
3
|
+
*
|
|
4
|
+
* Designed for edge/RSC environments where the full CockpitAPI
|
|
5
|
+
* factory is too heavy. This client:
|
|
6
|
+
* - Has no async initialization
|
|
7
|
+
* - No caching (relies on platform caching)
|
|
8
|
+
* - No response transformation
|
|
9
|
+
* - Minimal memory footprint
|
|
10
|
+
*/
|
|
11
|
+
import type { CockpitPage } from "../methods/pages.ts";
|
|
12
|
+
import type { CockpitContentItem } from "../methods/content.ts";
|
|
13
|
+
/**
|
|
14
|
+
* Request cache mode for fetch requests
|
|
15
|
+
*/
|
|
16
|
+
export type FetchCacheMode = "default" | "force-cache" | "no-cache" | "no-store" | "only-if-cached" | "reload";
|
|
17
|
+
/**
|
|
18
|
+
* Options for creating a lightweight fetch client
|
|
19
|
+
*/
|
|
20
|
+
export interface FetchClientOptions {
|
|
21
|
+
/** Cockpit CMS endpoint URL */
|
|
22
|
+
endpoint?: string;
|
|
23
|
+
/** Tenant ID for multi-tenant setups */
|
|
24
|
+
tenant?: string | null;
|
|
25
|
+
/**
|
|
26
|
+
* Default language that maps to Cockpit's "default" locale.
|
|
27
|
+
* When a request uses this language, it will be sent as "default" to Cockpit.
|
|
28
|
+
* @default "de"
|
|
29
|
+
*/
|
|
30
|
+
defaultLanguage?: string;
|
|
31
|
+
/** Request cache mode (default: "no-store") */
|
|
32
|
+
cache?: FetchCacheMode;
|
|
33
|
+
/** Additional request headers */
|
|
34
|
+
headers?: Record<string, string>;
|
|
35
|
+
/** API key for authenticated requests */
|
|
36
|
+
apiKey?: string;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Query parameters for page/content requests
|
|
40
|
+
*/
|
|
41
|
+
export interface PageFetchParams {
|
|
42
|
+
/** Locale for the request (the configured defaultLanguage maps to "default") */
|
|
43
|
+
locale?: string;
|
|
44
|
+
/** Populate depth for linked content */
|
|
45
|
+
populate?: number;
|
|
46
|
+
/** Additional query parameters */
|
|
47
|
+
[key: string]: string | number | boolean | undefined;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Lightweight fetch client interface
|
|
51
|
+
*/
|
|
52
|
+
export interface FetchClient {
|
|
53
|
+
/** Fetch a page by route */
|
|
54
|
+
pageByRoute<T = CockpitPage>(route: string, params?: PageFetchParams): Promise<T | null>;
|
|
55
|
+
/** Fetch pages list */
|
|
56
|
+
pages<T = CockpitPage>(params?: PageFetchParams): Promise<T[] | null>;
|
|
57
|
+
/** Fetch a page by ID */
|
|
58
|
+
pageById<T = CockpitPage>(id: string, params?: PageFetchParams): Promise<T | null>;
|
|
59
|
+
/** Fetch content items */
|
|
60
|
+
getContentItems<T = CockpitContentItem>(model: string, params?: PageFetchParams): Promise<T[] | null>;
|
|
61
|
+
/** Fetch a single content item */
|
|
62
|
+
getContentItem<T = unknown>(model: string, id?: string, params?: PageFetchParams): Promise<T | null>;
|
|
63
|
+
/** Raw fetch for custom paths */
|
|
64
|
+
fetchRaw<T = unknown>(path: string, params?: Record<string, string | number | boolean | undefined>): Promise<T>;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Creates a lightweight fetch client for Cockpit CMS
|
|
68
|
+
*
|
|
69
|
+
* @example
|
|
70
|
+
* ```typescript
|
|
71
|
+
* import { createFetchClient } from "@unchainedshop/cockpit-api/fetch";
|
|
72
|
+
*
|
|
73
|
+
* const cockpit = createFetchClient({
|
|
74
|
+
* endpoint: process.env.NEXT_PUBLIC_COCKPIT_ENDPOINT,
|
|
75
|
+
* tenant: "mytenant",
|
|
76
|
+
* });
|
|
77
|
+
*
|
|
78
|
+
* // Fetch a page by route
|
|
79
|
+
* const page = await cockpit.pageByRoute("/about", { locale: "en" });
|
|
80
|
+
*
|
|
81
|
+
* // Fetch content items
|
|
82
|
+
* const items = await cockpit.getContentItems("news", { locale: "de", limit: 10 });
|
|
83
|
+
* ```
|
|
84
|
+
*/
|
|
85
|
+
export declare function createFetchClient(options?: FetchClientOptions): FetchClient;
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lightweight fetch client for Cockpit CMS
|
|
3
|
+
*
|
|
4
|
+
* Designed for edge/RSC environments where the full CockpitAPI
|
|
5
|
+
* factory is too heavy. This client:
|
|
6
|
+
* - Has no async initialization
|
|
7
|
+
* - No caching (relies on platform caching)
|
|
8
|
+
* - No response transformation
|
|
9
|
+
* - Minimal memory footprint
|
|
10
|
+
*/
|
|
11
|
+
import { createLocaleNormalizer } from "../core/locale.js";
|
|
12
|
+
/**
|
|
13
|
+
* Build the API base URL for the given endpoint and tenant
|
|
14
|
+
*/
|
|
15
|
+
function buildApiBaseUrl(endpoint, tenant) {
|
|
16
|
+
const url = new URL(endpoint);
|
|
17
|
+
const basePath = tenant !== undefined && tenant !== null ? `/:${tenant}/api` : "/api";
|
|
18
|
+
return `${url.origin}${basePath}`;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Build query string from params object, filtering undefined values
|
|
22
|
+
*/
|
|
23
|
+
function buildQueryString(params) {
|
|
24
|
+
const filtered = Object.entries(params)
|
|
25
|
+
.filter(([, value]) => value !== undefined)
|
|
26
|
+
.map(([key, value]) => [key, String(value)]);
|
|
27
|
+
return new URLSearchParams(filtered).toString();
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Creates a lightweight fetch client for Cockpit CMS
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* ```typescript
|
|
34
|
+
* import { createFetchClient } from "@unchainedshop/cockpit-api/fetch";
|
|
35
|
+
*
|
|
36
|
+
* const cockpit = createFetchClient({
|
|
37
|
+
* endpoint: process.env.NEXT_PUBLIC_COCKPIT_ENDPOINT,
|
|
38
|
+
* tenant: "mytenant",
|
|
39
|
+
* });
|
|
40
|
+
*
|
|
41
|
+
* // Fetch a page by route
|
|
42
|
+
* const page = await cockpit.pageByRoute("/about", { locale: "en" });
|
|
43
|
+
*
|
|
44
|
+
* // Fetch content items
|
|
45
|
+
* const items = await cockpit.getContentItems("news", { locale: "de", limit: 10 });
|
|
46
|
+
* ```
|
|
47
|
+
*/
|
|
48
|
+
export function createFetchClient(options = {}) {
|
|
49
|
+
const { endpoint = process.env["COCKPIT_GRAPHQL_ENDPOINT"] ??
|
|
50
|
+
process.env["NEXT_PUBLIC_COCKPIT_ENDPOINT"], tenant = null, defaultLanguage = "de", cache = "no-store", headers = {}, apiKey, } = options;
|
|
51
|
+
if (endpoint === undefined || endpoint === "") {
|
|
52
|
+
throw new Error("Cockpit: endpoint is required (provide via options or COCKPIT_GRAPHQL_ENDPOINT env var)");
|
|
53
|
+
}
|
|
54
|
+
const baseUrl = buildApiBaseUrl(endpoint, tenant);
|
|
55
|
+
const normalizeLocale = createLocaleNormalizer(defaultLanguage);
|
|
56
|
+
const requestHeaders = { ...headers };
|
|
57
|
+
if (apiKey !== undefined) {
|
|
58
|
+
requestHeaders["api-Key"] = apiKey;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Fetch raw JSON from a Cockpit API path
|
|
62
|
+
*/
|
|
63
|
+
async function fetchRaw(path, params = {}) {
|
|
64
|
+
const queryString = buildQueryString(params);
|
|
65
|
+
const url = queryString
|
|
66
|
+
? `${baseUrl}${path}?${queryString}`
|
|
67
|
+
: `${baseUrl}${path}`;
|
|
68
|
+
const fetchInit = { cache };
|
|
69
|
+
if (Object.keys(requestHeaders).length > 0) {
|
|
70
|
+
fetchInit.headers = requestHeaders;
|
|
71
|
+
}
|
|
72
|
+
const response = await fetch(url, fetchInit);
|
|
73
|
+
if (!response.ok) {
|
|
74
|
+
if (response.status === 404) {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
throw new Error(`Cockpit: Error fetching ${url} (${String(response.status)})`);
|
|
78
|
+
}
|
|
79
|
+
return response.json();
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
/**
|
|
83
|
+
* Fetch a page by route
|
|
84
|
+
*/
|
|
85
|
+
async pageByRoute(route, params = {}) {
|
|
86
|
+
const { locale, populate, ...rest } = params;
|
|
87
|
+
return fetchRaw("/pages/page", {
|
|
88
|
+
route,
|
|
89
|
+
locale: normalizeLocale(locale),
|
|
90
|
+
populate,
|
|
91
|
+
...rest,
|
|
92
|
+
});
|
|
93
|
+
},
|
|
94
|
+
/**
|
|
95
|
+
* Fetch pages list
|
|
96
|
+
*/
|
|
97
|
+
async pages(params = {}) {
|
|
98
|
+
const { locale, ...rest } = params;
|
|
99
|
+
return fetchRaw("/pages/pages", {
|
|
100
|
+
locale: normalizeLocale(locale),
|
|
101
|
+
...rest,
|
|
102
|
+
});
|
|
103
|
+
},
|
|
104
|
+
/**
|
|
105
|
+
* Fetch a page by ID
|
|
106
|
+
*/
|
|
107
|
+
async pageById(id, params = {}) {
|
|
108
|
+
const { locale, populate, ...rest } = params;
|
|
109
|
+
return fetchRaw(`/pages/page/${id}`, {
|
|
110
|
+
locale: normalizeLocale(locale),
|
|
111
|
+
populate,
|
|
112
|
+
...rest,
|
|
113
|
+
});
|
|
114
|
+
},
|
|
115
|
+
/**
|
|
116
|
+
* Fetch content items
|
|
117
|
+
*/
|
|
118
|
+
async getContentItems(model, params = {}) {
|
|
119
|
+
const { locale, ...rest } = params;
|
|
120
|
+
return fetchRaw(`/content/items/${model}`, {
|
|
121
|
+
locale: normalizeLocale(locale),
|
|
122
|
+
...rest,
|
|
123
|
+
});
|
|
124
|
+
},
|
|
125
|
+
/**
|
|
126
|
+
* Fetch a single content item
|
|
127
|
+
*/
|
|
128
|
+
async getContentItem(model, id, params = {}) {
|
|
129
|
+
const { locale, ...rest } = params;
|
|
130
|
+
const path = id !== undefined
|
|
131
|
+
? `/content/item/${model}/${id}`
|
|
132
|
+
: `/content/item/${model}`;
|
|
133
|
+
return fetchRaw(path, {
|
|
134
|
+
locale: normalizeLocale(locale),
|
|
135
|
+
...rest,
|
|
136
|
+
});
|
|
137
|
+
},
|
|
138
|
+
/**
|
|
139
|
+
* Raw fetch for custom paths
|
|
140
|
+
*/
|
|
141
|
+
fetchRaw: fetchRaw,
|
|
142
|
+
};
|
|
143
|
+
}
|