cms-renderer 0.0.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/.turbo/turbo-check-types.log +2 -0
- package/README.md +362 -0
- package/lib/__tests__/enrich-block-images.test.ts +394 -0
- package/lib/block-renderer.tsx +60 -0
- package/lib/cms-api.ts +86 -0
- package/lib/data-utils.ts +572 -0
- package/lib/image/lazy-load.ts +209 -0
- package/lib/markdown-utils.ts +368 -0
- package/lib/renderer.tsx +189 -0
- package/lib/result.ts +450 -0
- package/lib/schema.ts +74 -0
- package/lib/trpc.ts +28 -0
- package/lib/types.ts +201 -0
- package/next.config.ts +39 -0
- package/package.json +61 -0
- package/postcss.config.mjs +5 -0
- package/tsconfig.json +12 -0
- package/tsconfig.tsbuildinfo +1 -0
package/lib/renderer.tsx
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Catch-all Route Handler for Parametric Routes
|
|
3
|
+
*
|
|
4
|
+
* Handles routes with multiple segments like /us/en/products.
|
|
5
|
+
* Uses the CMS API to fetch and render blocks.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
isArticlePublished,
|
|
10
|
+
isValidBlockSchemaName,
|
|
11
|
+
normalizeArticleContent,
|
|
12
|
+
} from '@repo/cms-schema/blocks';
|
|
13
|
+
import type { Metadata } from 'next';
|
|
14
|
+
import { unstable_noStore } from 'next/cache';
|
|
15
|
+
import { notFound } from 'next/navigation';
|
|
16
|
+
import { BlockRenderer } from './block-renderer';
|
|
17
|
+
import { getCmsClient } from './cms-api';
|
|
18
|
+
import type { BlockComponentRegistry, BlockData } from './types';
|
|
19
|
+
|
|
20
|
+
type PageProps = {
|
|
21
|
+
params: Promise<{ slug: string[] }>;
|
|
22
|
+
registry?: Partial<BlockComponentRegistry>;
|
|
23
|
+
/** API key for CMS API authentication */
|
|
24
|
+
apiKey?: string;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Force dynamic rendering to ensure routes are always fresh.
|
|
29
|
+
* This prevents Next.js from caching pages when routes are published.
|
|
30
|
+
*/
|
|
31
|
+
export const dynamic = 'force-dynamic';
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Catch-all route handler for parametric routes.
|
|
35
|
+
*
|
|
36
|
+
* Handles paths like:
|
|
37
|
+
* - /us/en/products -> slug = ['us', 'en', 'products']
|
|
38
|
+
* - /about -> slug = ['about']
|
|
39
|
+
*
|
|
40
|
+
* Reconstructs the full path and fetches route via tRPC.
|
|
41
|
+
*/
|
|
42
|
+
export default async function ParametricRoutePage({ params, registry, apiKey }: PageProps) {
|
|
43
|
+
// Prevent any caching - ensure we always fetch fresh route data
|
|
44
|
+
unstable_noStore();
|
|
45
|
+
|
|
46
|
+
const { slug } = await params;
|
|
47
|
+
|
|
48
|
+
// Reconstruct full path from slug segments and normalize it
|
|
49
|
+
const rawPath = `/${slug.join('/')}`;
|
|
50
|
+
const path = normalizePath(rawPath);
|
|
51
|
+
|
|
52
|
+
// Get CMS API client with optional API key
|
|
53
|
+
const client = getCmsClient({ apiKey });
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
// Fetch route by path via CMS API
|
|
57
|
+
const { route } = await client.route.getByPath.query({ path });
|
|
58
|
+
|
|
59
|
+
// Only show Live routes on public website
|
|
60
|
+
if (route.state !== 'Live') {
|
|
61
|
+
console.error(`Route found but not Live. Path: ${path}, State: ${route.state}`);
|
|
62
|
+
notFound();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Fetch all blocks by ID via CMS API (skip any that fail)
|
|
66
|
+
const blockPromises = route.block_ids.map(async (blockId) => {
|
|
67
|
+
try {
|
|
68
|
+
const result = await client.block.getById.query({ id: blockId });
|
|
69
|
+
return result.block;
|
|
70
|
+
} catch (error) {
|
|
71
|
+
// Log error but don't fail the entire page
|
|
72
|
+
console.error(`Failed to fetch block ${blockId}:`, error);
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
const blockResults = await Promise.all(blockPromises);
|
|
77
|
+
|
|
78
|
+
// Transform blocks to BlockData format for BlockRenderer
|
|
79
|
+
// Filter out any blocks that failed to load
|
|
80
|
+
const blocks: BlockData[] = [];
|
|
81
|
+
|
|
82
|
+
for (const block of blockResults) {
|
|
83
|
+
if (!block || block.published_content === null) continue;
|
|
84
|
+
|
|
85
|
+
const content = block.published_content as Record<string, unknown> | null;
|
|
86
|
+
if (!content) continue;
|
|
87
|
+
|
|
88
|
+
// Handle 'article' blocks separately before checking schema type
|
|
89
|
+
if (block.schema_name === 'article') {
|
|
90
|
+
const article = normalizeArticleContent(content);
|
|
91
|
+
const isPublished = article ? isArticlePublished(article) : null;
|
|
92
|
+
if (article && isPublished) {
|
|
93
|
+
blocks.push({ id: block.id, type: 'article', content: article });
|
|
94
|
+
}
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Skip blocks with invalid schema names (after handling 'article')
|
|
99
|
+
if (!isValidBlockSchemaName(block.schema_name)) {
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// For all block types, map schema_name to type and include content
|
|
104
|
+
// Image references are automatically resolved by block.getById
|
|
105
|
+
blocks.push({
|
|
106
|
+
id: block.id,
|
|
107
|
+
type: block.schema_name,
|
|
108
|
+
content,
|
|
109
|
+
} as BlockData);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return (
|
|
113
|
+
<main>
|
|
114
|
+
{blocks.map((block) => (
|
|
115
|
+
<BlockRenderer registry={registry ?? {}} key={block.id} block={block} />
|
|
116
|
+
))}
|
|
117
|
+
</main>
|
|
118
|
+
);
|
|
119
|
+
} catch (error) {
|
|
120
|
+
// Log error for debugging
|
|
121
|
+
console.error(`Route fetch error for path: ${path}`, error);
|
|
122
|
+
|
|
123
|
+
// If route not found or param validation fails, show 404
|
|
124
|
+
// TRPCClientError has data.code for the error code
|
|
125
|
+
const errorCode =
|
|
126
|
+
error instanceof Error && 'data' in error
|
|
127
|
+
? (error as { data?: { code?: string } }).data?.code
|
|
128
|
+
: error instanceof Error && 'code' in error
|
|
129
|
+
? (error as { code: string }).code
|
|
130
|
+
: undefined;
|
|
131
|
+
|
|
132
|
+
if (errorCode === 'NOT_FOUND' || errorCode === 'P0002') {
|
|
133
|
+
notFound();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Re-throw other errors
|
|
137
|
+
throw error;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// -----------------------------------------------------------------------------
|
|
142
|
+
// Metadata
|
|
143
|
+
// -----------------------------------------------------------------------------
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Generate metadata for the page.
|
|
147
|
+
* Uses Next.js 15+ async params pattern.
|
|
148
|
+
*/
|
|
149
|
+
export async function generateMetadata({ params, apiKey }: PageProps): Promise<Metadata> {
|
|
150
|
+
const { slug } = await params;
|
|
151
|
+
const rawPath = `/${slug.join('/')}`;
|
|
152
|
+
const path = normalizePath(rawPath);
|
|
153
|
+
const client = getCmsClient({ apiKey });
|
|
154
|
+
|
|
155
|
+
try {
|
|
156
|
+
const { route } = await client.route.getByPath.query({ path });
|
|
157
|
+
return {
|
|
158
|
+
title: `${route.path} | Website`,
|
|
159
|
+
description: `Content page: ${route.path}`,
|
|
160
|
+
};
|
|
161
|
+
} catch {
|
|
162
|
+
return {
|
|
163
|
+
title: 'Page Not Found | Website',
|
|
164
|
+
description: 'The requested page could not be found.',
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function normalizePath(path: string): string {
|
|
170
|
+
if (!path || path === '/') {
|
|
171
|
+
return '/';
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Remove trailing slashes, ensure leading slash
|
|
175
|
+
let normalized = path.trim();
|
|
176
|
+
|
|
177
|
+
// Remove trailing slashes (but keep root "/")
|
|
178
|
+
normalized = normalized.replace(/\/+$/, '');
|
|
179
|
+
|
|
180
|
+
// Ensure leading slash
|
|
181
|
+
if (!normalized.startsWith('/')) {
|
|
182
|
+
normalized = `/${normalized}`;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Collapse multiple consecutive slashes to single slash
|
|
186
|
+
normalized = normalized.replace(/\/+/g, '/');
|
|
187
|
+
|
|
188
|
+
return normalized;
|
|
189
|
+
}
|
package/lib/result.ts
ADDED
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Result Type Utilities
|
|
3
|
+
*
|
|
4
|
+
* Go-style error handling with discriminated union types.
|
|
5
|
+
* Provides type-safe success/error handling without exceptions.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```ts
|
|
9
|
+
* const [err, data] = await handle(async () => {
|
|
10
|
+
* const response = await fetch(url);
|
|
11
|
+
* if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
12
|
+
* return response.json();
|
|
13
|
+
* });
|
|
14
|
+
*
|
|
15
|
+
* if (err) {
|
|
16
|
+
* console.error('Failed:', err.message);
|
|
17
|
+
* return { error: err.message };
|
|
18
|
+
* }
|
|
19
|
+
* return { data };
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
// -----------------------------------------------------------------------------
|
|
24
|
+
// Result Type
|
|
25
|
+
// -----------------------------------------------------------------------------
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* A discriminated union representing either success or failure.
|
|
29
|
+
*
|
|
30
|
+
* @template T - The success value type
|
|
31
|
+
* @template E - The error type (defaults to Error)
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* ```ts
|
|
35
|
+
* function divide(a: number, b: number): Result<number, string> {
|
|
36
|
+
* if (b === 0) return err('Division by zero');
|
|
37
|
+
* return ok(a / b);
|
|
38
|
+
* }
|
|
39
|
+
*
|
|
40
|
+
* const result = divide(10, 2);
|
|
41
|
+
* if (isOk(result)) {
|
|
42
|
+
* console.log('Result:', result.value); // 5
|
|
43
|
+
* } else {
|
|
44
|
+
* console.error('Error:', result.error);
|
|
45
|
+
* }
|
|
46
|
+
* ```
|
|
47
|
+
*/
|
|
48
|
+
export type Result<T, E = Error> = { ok: true; value: T } | { ok: false; error: E };
|
|
49
|
+
|
|
50
|
+
// -----------------------------------------------------------------------------
|
|
51
|
+
// Constructors
|
|
52
|
+
// -----------------------------------------------------------------------------
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Creates a success Result.
|
|
56
|
+
*
|
|
57
|
+
* @param value - The success value
|
|
58
|
+
* @returns A success Result containing the value
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* ```ts
|
|
62
|
+
* const result = ok(42);
|
|
63
|
+
* // { ok: true, value: 42 }
|
|
64
|
+
* ```
|
|
65
|
+
*/
|
|
66
|
+
export function ok<T>(value: T): Result<T, never> {
|
|
67
|
+
return { ok: true, value };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Creates a failure Result.
|
|
72
|
+
*
|
|
73
|
+
* @param error - The error value
|
|
74
|
+
* @returns A failure Result containing the error
|
|
75
|
+
*
|
|
76
|
+
* @example
|
|
77
|
+
* ```ts
|
|
78
|
+
* const result = err(new Error('Something went wrong'));
|
|
79
|
+
* // { ok: false, error: Error('Something went wrong') }
|
|
80
|
+
* ```
|
|
81
|
+
*/
|
|
82
|
+
export function err<E>(error: E): Result<never, E> {
|
|
83
|
+
return { ok: false, error };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// -----------------------------------------------------------------------------
|
|
87
|
+
// Type Guards
|
|
88
|
+
// -----------------------------------------------------------------------------
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Type guard to check if a Result is successful.
|
|
92
|
+
*
|
|
93
|
+
* @param result - The Result to check
|
|
94
|
+
* @returns true if the Result is a success
|
|
95
|
+
*
|
|
96
|
+
* @example
|
|
97
|
+
* ```ts
|
|
98
|
+
* const result = fetchData();
|
|
99
|
+
* if (isOk(result)) {
|
|
100
|
+
* // TypeScript knows result.value exists here
|
|
101
|
+
* console.log(result.value);
|
|
102
|
+
* }
|
|
103
|
+
* ```
|
|
104
|
+
*/
|
|
105
|
+
export function isOk<T, E>(result: Result<T, E>): result is { ok: true; value: T } {
|
|
106
|
+
return result.ok === true;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Type guard to check if a Result is a failure.
|
|
111
|
+
*
|
|
112
|
+
* @param result - The Result to check
|
|
113
|
+
* @returns true if the Result is a failure
|
|
114
|
+
*
|
|
115
|
+
* @example
|
|
116
|
+
* ```ts
|
|
117
|
+
* const result = fetchData();
|
|
118
|
+
* if (isErr(result)) {
|
|
119
|
+
* // TypeScript knows result.error exists here
|
|
120
|
+
* console.error(result.error);
|
|
121
|
+
* }
|
|
122
|
+
* ```
|
|
123
|
+
*/
|
|
124
|
+
export function isErr<T, E>(result: Result<T, E>): result is { ok: false; error: E } {
|
|
125
|
+
return result.ok === false;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// -----------------------------------------------------------------------------
|
|
129
|
+
// Extractors
|
|
130
|
+
// -----------------------------------------------------------------------------
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Extracts the value from a Result, throwing if it's an error.
|
|
134
|
+
*
|
|
135
|
+
* @param result - The Result to unwrap
|
|
136
|
+
* @returns The success value
|
|
137
|
+
* @throws The error if Result is a failure
|
|
138
|
+
*
|
|
139
|
+
* @example
|
|
140
|
+
* ```ts
|
|
141
|
+
* const result = ok(42);
|
|
142
|
+
* const value = unwrap(result); // 42
|
|
143
|
+
*
|
|
144
|
+
* const errorResult = err(new Error('fail'));
|
|
145
|
+
* const value2 = unwrap(errorResult); // throws Error('fail')
|
|
146
|
+
* ```
|
|
147
|
+
*/
|
|
148
|
+
export function unwrap<T, E>(result: Result<T, E>): T {
|
|
149
|
+
if (isOk(result)) {
|
|
150
|
+
return result.value;
|
|
151
|
+
}
|
|
152
|
+
throw result.error;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Extracts the value from a Result, returning a default on error.
|
|
157
|
+
*
|
|
158
|
+
* @param result - The Result to unwrap
|
|
159
|
+
* @param defaultValue - The value to return if Result is an error
|
|
160
|
+
* @returns The success value or the default value
|
|
161
|
+
*
|
|
162
|
+
* @example
|
|
163
|
+
* ```ts
|
|
164
|
+
* const result = err(new Error('fail'));
|
|
165
|
+
* const value = unwrapOr(result, 0); // 0
|
|
166
|
+
*
|
|
167
|
+
* const okResult = ok(42);
|
|
168
|
+
* const value2 = unwrapOr(okResult, 0); // 42
|
|
169
|
+
* ```
|
|
170
|
+
*/
|
|
171
|
+
export function unwrapOr<T, E>(result: Result<T, E>, defaultValue: T): T {
|
|
172
|
+
if (isOk(result)) {
|
|
173
|
+
return result.value;
|
|
174
|
+
}
|
|
175
|
+
return defaultValue;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Extracts the value from a Result, computing a default on error.
|
|
180
|
+
*
|
|
181
|
+
* @param result - The Result to unwrap
|
|
182
|
+
* @param fn - Function to compute the default value from the error
|
|
183
|
+
* @returns The success value or the computed default
|
|
184
|
+
*
|
|
185
|
+
* @example
|
|
186
|
+
* ```ts
|
|
187
|
+
* const result = err(new Error('not found'));
|
|
188
|
+
* const value = unwrapOrElse(result, (e) => {
|
|
189
|
+
* console.error('Error:', e.message);
|
|
190
|
+
* return [];
|
|
191
|
+
* });
|
|
192
|
+
* ```
|
|
193
|
+
*/
|
|
194
|
+
export function unwrapOrElse<T, E>(result: Result<T, E>, fn: (error: E) => T): T {
|
|
195
|
+
if (isOk(result)) {
|
|
196
|
+
return result.value;
|
|
197
|
+
}
|
|
198
|
+
return fn(result.error);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// -----------------------------------------------------------------------------
|
|
202
|
+
// Transformers
|
|
203
|
+
// -----------------------------------------------------------------------------
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Maps a successful Result's value.
|
|
207
|
+
*
|
|
208
|
+
* @param result - The Result to map
|
|
209
|
+
* @param fn - Function to transform the value
|
|
210
|
+
* @returns A new Result with the transformed value, or the original error
|
|
211
|
+
*
|
|
212
|
+
* @example
|
|
213
|
+
* ```ts
|
|
214
|
+
* const result = ok(5);
|
|
215
|
+
* const doubled = map(result, (n) => n * 2); // ok(10)
|
|
216
|
+
*
|
|
217
|
+
* const errorResult = err('fail');
|
|
218
|
+
* const still = map(errorResult, (n) => n * 2); // err('fail')
|
|
219
|
+
* ```
|
|
220
|
+
*/
|
|
221
|
+
export function map<T, U, E>(result: Result<T, E>, fn: (value: T) => U): Result<U, E> {
|
|
222
|
+
if (isOk(result)) {
|
|
223
|
+
return ok(fn(result.value));
|
|
224
|
+
}
|
|
225
|
+
return result;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Maps a failed Result's error.
|
|
230
|
+
*
|
|
231
|
+
* @param result - The Result to map
|
|
232
|
+
* @param fn - Function to transform the error
|
|
233
|
+
* @returns A new Result with the transformed error, or the original value
|
|
234
|
+
*
|
|
235
|
+
* @example
|
|
236
|
+
* ```ts
|
|
237
|
+
* const result = err('not found');
|
|
238
|
+
* const mapped = mapErr(result, (e) => new Error(e)); // err(Error('not found'))
|
|
239
|
+
* ```
|
|
240
|
+
*/
|
|
241
|
+
export function mapErr<T, E, F>(result: Result<T, E>, fn: (error: E) => F): Result<T, F> {
|
|
242
|
+
if (isErr(result)) {
|
|
243
|
+
return err(fn(result.error));
|
|
244
|
+
}
|
|
245
|
+
return result;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Chains Result-returning operations.
|
|
250
|
+
*
|
|
251
|
+
* @param result - The Result to chain from
|
|
252
|
+
* @param fn - Function that returns a new Result
|
|
253
|
+
* @returns The chained Result
|
|
254
|
+
*
|
|
255
|
+
* @example
|
|
256
|
+
* ```ts
|
|
257
|
+
* function parse(input: string): Result<number, string> {
|
|
258
|
+
* const n = parseInt(input, 10);
|
|
259
|
+
* return isNaN(n) ? err('not a number') : ok(n);
|
|
260
|
+
* }
|
|
261
|
+
*
|
|
262
|
+
* function double(n: number): Result<number, string> {
|
|
263
|
+
* return ok(n * 2);
|
|
264
|
+
* }
|
|
265
|
+
*
|
|
266
|
+
* const result = flatMap(parse('5'), double); // ok(10)
|
|
267
|
+
* const fail = flatMap(parse('abc'), double); // err('not a number')
|
|
268
|
+
* ```
|
|
269
|
+
*/
|
|
270
|
+
export function flatMap<T, U, E>(
|
|
271
|
+
result: Result<T, E>,
|
|
272
|
+
fn: (value: T) => Result<U, E>
|
|
273
|
+
): Result<U, E> {
|
|
274
|
+
if (isOk(result)) {
|
|
275
|
+
return fn(result.value);
|
|
276
|
+
}
|
|
277
|
+
return result;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// -----------------------------------------------------------------------------
|
|
281
|
+
// Async Helpers
|
|
282
|
+
// -----------------------------------------------------------------------------
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Wraps a Promise in a Result type.
|
|
286
|
+
*
|
|
287
|
+
* @param promise - The Promise to wrap
|
|
288
|
+
* @returns A Promise that resolves to a Result
|
|
289
|
+
*
|
|
290
|
+
* @example
|
|
291
|
+
* ```ts
|
|
292
|
+
* const result = await fromPromise(fetch('/api/data'));
|
|
293
|
+
* if (isErr(result)) {
|
|
294
|
+
* console.error('Fetch failed:', result.error);
|
|
295
|
+
* return;
|
|
296
|
+
* }
|
|
297
|
+
* const response = result.value;
|
|
298
|
+
* ```
|
|
299
|
+
*/
|
|
300
|
+
export async function fromPromise<T>(promise: Promise<T>): Promise<Result<T, Error>> {
|
|
301
|
+
try {
|
|
302
|
+
const value = await promise;
|
|
303
|
+
return ok(value);
|
|
304
|
+
} catch (error) {
|
|
305
|
+
return err(error instanceof Error ? error : new Error(String(error)));
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Wraps a throwing function in a Result type.
|
|
311
|
+
*
|
|
312
|
+
* @param fn - The function to wrap
|
|
313
|
+
* @returns A Result containing the return value or the thrown error
|
|
314
|
+
*
|
|
315
|
+
* @example
|
|
316
|
+
* ```ts
|
|
317
|
+
* const result = tryCatch(() => JSON.parse(input));
|
|
318
|
+
* if (isErr(result)) {
|
|
319
|
+
* console.error('Invalid JSON:', result.error);
|
|
320
|
+
* return null;
|
|
321
|
+
* }
|
|
322
|
+
* return result.value;
|
|
323
|
+
* ```
|
|
324
|
+
*/
|
|
325
|
+
export function tryCatch<T>(fn: () => T): Result<T, Error> {
|
|
326
|
+
try {
|
|
327
|
+
return ok(fn());
|
|
328
|
+
} catch (error) {
|
|
329
|
+
return err(error instanceof Error ? error : new Error(String(error)));
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Wraps an async function in a Result type.
|
|
335
|
+
*
|
|
336
|
+
* @param fn - The async function to wrap
|
|
337
|
+
* @returns A Promise that resolves to a Result
|
|
338
|
+
*
|
|
339
|
+
* @example
|
|
340
|
+
* ```ts
|
|
341
|
+
* const result = await tryCatchAsync(async () => {
|
|
342
|
+
* const response = await fetch('/api/data');
|
|
343
|
+
* if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
344
|
+
* return response.json();
|
|
345
|
+
* });
|
|
346
|
+
* ```
|
|
347
|
+
*/
|
|
348
|
+
export async function tryCatchAsync<T>(fn: () => Promise<T>): Promise<Result<T, Error>> {
|
|
349
|
+
try {
|
|
350
|
+
const value = await fn();
|
|
351
|
+
return ok(value);
|
|
352
|
+
} catch (error) {
|
|
353
|
+
return err(error instanceof Error ? error : new Error(String(error)));
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// -----------------------------------------------------------------------------
|
|
358
|
+
// Tuple Helpers (Go-style)
|
|
359
|
+
// -----------------------------------------------------------------------------
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Go-style tuple for error handling: [error, value]
|
|
363
|
+
*
|
|
364
|
+
* @example
|
|
365
|
+
* ```ts
|
|
366
|
+
* const [err, data] = await handle(fetchData);
|
|
367
|
+
* if (err) {
|
|
368
|
+
* console.error(err);
|
|
369
|
+
* return;
|
|
370
|
+
* }
|
|
371
|
+
* console.log(data);
|
|
372
|
+
* ```
|
|
373
|
+
*/
|
|
374
|
+
export type GoTuple<T, E = Error> = [E, undefined] | [undefined, T];
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Converts a Result to a Go-style tuple.
|
|
378
|
+
*
|
|
379
|
+
* @param result - The Result to convert
|
|
380
|
+
* @returns A tuple of [error, value]
|
|
381
|
+
*
|
|
382
|
+
* @example
|
|
383
|
+
* ```ts
|
|
384
|
+
* const result = await fetchData();
|
|
385
|
+
* const [err, data] = toTuple(result);
|
|
386
|
+
* ```
|
|
387
|
+
*/
|
|
388
|
+
export function toTuple<T, E>(result: Result<T, E>): GoTuple<T, E> {
|
|
389
|
+
if (isOk(result)) {
|
|
390
|
+
return [undefined, result.value];
|
|
391
|
+
}
|
|
392
|
+
return [result.error, undefined];
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Wraps an async function and returns a Go-style tuple.
|
|
397
|
+
*
|
|
398
|
+
* @param fn - The async function to wrap
|
|
399
|
+
* @returns A Promise that resolves to [error, value] tuple
|
|
400
|
+
*
|
|
401
|
+
* @example
|
|
402
|
+
* ```ts
|
|
403
|
+
* const [err, data] = await handle(async () => {
|
|
404
|
+
* const res = await fetch('/api/users');
|
|
405
|
+
* if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
406
|
+
* return res.json();
|
|
407
|
+
* });
|
|
408
|
+
*
|
|
409
|
+
* if (err) {
|
|
410
|
+
* console.error('Failed to fetch users:', err.message);
|
|
411
|
+
* return [];
|
|
412
|
+
* }
|
|
413
|
+
* return data;
|
|
414
|
+
* ```
|
|
415
|
+
*/
|
|
416
|
+
export async function handle<T>(fn: () => Promise<T>): Promise<GoTuple<T, Error>> {
|
|
417
|
+
try {
|
|
418
|
+
const value = await fn();
|
|
419
|
+
return [undefined, value];
|
|
420
|
+
} catch (error) {
|
|
421
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
422
|
+
return [err, undefined];
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Wraps a synchronous function and returns a Go-style tuple.
|
|
428
|
+
*
|
|
429
|
+
* @param fn - The function to wrap
|
|
430
|
+
* @returns A tuple of [error, value]
|
|
431
|
+
*
|
|
432
|
+
* @example
|
|
433
|
+
* ```ts
|
|
434
|
+
* const [err, parsed] = handleSync(() => JSON.parse(input));
|
|
435
|
+
* if (err) {
|
|
436
|
+
* console.error('Invalid JSON:', err.message);
|
|
437
|
+
* return null;
|
|
438
|
+
* }
|
|
439
|
+
* return parsed;
|
|
440
|
+
* ```
|
|
441
|
+
*/
|
|
442
|
+
export function handleSync<T>(fn: () => T): GoTuple<T, Error> {
|
|
443
|
+
try {
|
|
444
|
+
const value = fn();
|
|
445
|
+
return [undefined, value];
|
|
446
|
+
} catch (error) {
|
|
447
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
448
|
+
return [err, undefined];
|
|
449
|
+
}
|
|
450
|
+
}
|
package/lib/schema.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// Fetch schema from the CMS for a headless setup
|
|
2
|
+
// This should look like this
|
|
3
|
+
//
|
|
4
|
+
// import { schema, configureSchema } from "@profound/cms";
|
|
5
|
+
//
|
|
6
|
+
// // Option 1: Use default config (reads from CMS_API_URL env var)
|
|
7
|
+
// const footerData = schema.name("footer").fetchAll();
|
|
8
|
+
//
|
|
9
|
+
// // Option 2: Configure with custom API base and fetch options
|
|
10
|
+
// const customSchema = configureSchema({
|
|
11
|
+
// apiBase: "https://my-cms.example.com/api",
|
|
12
|
+
// fetchOptions: { headers: { Authorization: "Bearer token" } },
|
|
13
|
+
// });
|
|
14
|
+
// const footerData = customSchema.name("footer").fetchAll();
|
|
15
|
+
//
|
|
16
|
+
// console.log(footerData); # [{ logo: "<cloudflare_signed_img_url>", "Footer_text": "Profound" }]
|
|
17
|
+
|
|
18
|
+
const DEFAULT_API_BASE = process.env.NEXT_PUBLIC_CMS_API_URL ?? 'http://localhost:4000/api';
|
|
19
|
+
|
|
20
|
+
interface SchemaConfig {
|
|
21
|
+
apiBase?: string;
|
|
22
|
+
fetchOptions?: RequestInit;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface SchemaQuery {
|
|
26
|
+
fetchAll: <T = Record<string, unknown>>() => Promise<T[]>;
|
|
27
|
+
fetchSingle: <T = Record<string, unknown>>() => Promise<T | null>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface SchemaClient {
|
|
31
|
+
name: (schemaName: string) => SchemaQuery;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function createSchemaQuery(
|
|
35
|
+
apiBase: string,
|
|
36
|
+
schemaName: string,
|
|
37
|
+
fetchOptions: RequestInit = {}
|
|
38
|
+
): SchemaQuery {
|
|
39
|
+
const baseUrl = `${apiBase}/schemas/${schemaName}`;
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
async fetchAll<T = Record<string, unknown>>(): Promise<T[]> {
|
|
43
|
+
const response = await fetch(baseUrl, fetchOptions);
|
|
44
|
+
|
|
45
|
+
if (!response.ok) {
|
|
46
|
+
throw new Error(`Failed to fetch schema "${schemaName}": ${response.statusText}`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return response.json();
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
async fetchSingle<T = Record<string, unknown>>(): Promise<T | null> {
|
|
53
|
+
const response = await fetch(`${baseUrl}?limit=1`, fetchOptions);
|
|
54
|
+
|
|
55
|
+
if (!response.ok) {
|
|
56
|
+
throw new Error(`Failed to fetch schema "${schemaName}": ${response.statusText}`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const data = await response.json();
|
|
60
|
+
return Array.isArray(data) ? (data[0] ?? null) : data;
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function configureSchema(config: SchemaConfig = {}): SchemaClient {
|
|
66
|
+
const apiBase = config.apiBase || DEFAULT_API_BASE;
|
|
67
|
+
const fetchOptions = config.fetchOptions || {};
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
name: (schemaName: string): SchemaQuery => createSchemaQuery(apiBase, schemaName, fetchOptions),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export const schema: SchemaClient = configureSchema();
|