perspectapi-ts-sdk 1.1.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/LICENSE +21 -0
- package/README.md +515 -0
- package/dist/index.d.mts +1770 -0
- package/dist/index.d.ts +1770 -0
- package/dist/index.js +1777 -0
- package/dist/index.mjs +1727 -0
- package/package.json +69 -0
- package/src/client/api-keys-client.ts +103 -0
- package/src/client/auth-client.ts +87 -0
- package/src/client/base-client.ts +100 -0
- package/src/client/categories-client.ts +151 -0
- package/src/client/checkout-client.ts +249 -0
- package/src/client/contact-client.ts +203 -0
- package/src/client/content-client.ts +112 -0
- package/src/client/organizations-client.ts +106 -0
- package/src/client/products-client.ts +247 -0
- package/src/client/sites-client.ts +148 -0
- package/src/client/webhooks-client.ts +199 -0
- package/src/index.ts +74 -0
- package/src/loaders.ts +644 -0
- package/src/perspect-api-client.ts +179 -0
- package/src/types/index.ts +399 -0
- package/src/utils/http-client.ts +268 -0
package/src/loaders.ts
ADDED
|
@@ -0,0 +1,644 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* High-level data loading helpers that wrap the PerspectAPI SDK clients.
|
|
3
|
+
* These helpers provide convenient product and content loading utilities with
|
|
4
|
+
* graceful fallbacks that can be reused across applications.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { PerspectApiClient } from './perspect-api-client';
|
|
8
|
+
import type {
|
|
9
|
+
ApiResponse,
|
|
10
|
+
MediaItem,
|
|
11
|
+
PaginatedResponse,
|
|
12
|
+
Product,
|
|
13
|
+
ProductQueryParams,
|
|
14
|
+
Content,
|
|
15
|
+
ContentStatus,
|
|
16
|
+
ContentType,
|
|
17
|
+
BlogPost,
|
|
18
|
+
CreateCheckoutSessionRequest,
|
|
19
|
+
CheckoutSession,
|
|
20
|
+
CheckoutMetadata
|
|
21
|
+
} from './types';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Logger interface so consumers can supply custom logging behaviour (or noop).
|
|
25
|
+
*/
|
|
26
|
+
export interface LoaderLogger {
|
|
27
|
+
debug?(...args: unknown[]): void;
|
|
28
|
+
info?(...args: unknown[]): void;
|
|
29
|
+
warn?(...args: unknown[]): void;
|
|
30
|
+
error?(...args: unknown[]): void;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const noopLogger: LoaderLogger = {};
|
|
34
|
+
|
|
35
|
+
const log = (
|
|
36
|
+
logger: LoaderLogger | undefined,
|
|
37
|
+
level: keyof LoaderLogger,
|
|
38
|
+
...args: unknown[]
|
|
39
|
+
) => {
|
|
40
|
+
const target = logger?.[level];
|
|
41
|
+
if (typeof target === 'function') {
|
|
42
|
+
target(...args);
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const isMediaItem = (value: unknown): value is MediaItem => {
|
|
47
|
+
if (!value || typeof value !== 'object') {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
const media = value as Record<string, unknown>;
|
|
51
|
+
return typeof media.link === 'string' && typeof media.media_id === 'string';
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const normalizeMediaList = (rawMedia: unknown): MediaItem[] => {
|
|
55
|
+
if (!Array.isArray(rawMedia)) {
|
|
56
|
+
return [];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (rawMedia.length > 0 && Array.isArray(rawMedia[0])) {
|
|
60
|
+
return (rawMedia as unknown[])
|
|
61
|
+
.flat()
|
|
62
|
+
.filter(isMediaItem);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return (rawMedia as unknown[]).filter(isMediaItem);
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const normalizeQueryParamList = (
|
|
69
|
+
value: string | number | Array<string | number> | undefined
|
|
70
|
+
): string | undefined => {
|
|
71
|
+
if (value === undefined || value === null) {
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const values = Array.isArray(value) ? value : [value];
|
|
76
|
+
const normalized = values
|
|
77
|
+
.map(item => String(item).trim())
|
|
78
|
+
.filter(item => item.length > 0);
|
|
79
|
+
|
|
80
|
+
return normalized.length > 0 ? normalized.join(',') : undefined;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Transform a PerspectAPI product payload into a friendlier shape for UI layers.
|
|
85
|
+
*/
|
|
86
|
+
export const transformProduct = (
|
|
87
|
+
perspectProduct: any,
|
|
88
|
+
logger?: LoaderLogger
|
|
89
|
+
): Product => {
|
|
90
|
+
const rawProduct = perspectProduct ?? {};
|
|
91
|
+
|
|
92
|
+
const productId =
|
|
93
|
+
rawProduct.product_id ?? rawProduct.id ?? rawProduct.sku ?? 'unknown';
|
|
94
|
+
const productName =
|
|
95
|
+
rawProduct.product ?? rawProduct.name ?? rawProduct.title ?? 'Untitled';
|
|
96
|
+
const productSlug =
|
|
97
|
+
rawProduct.slug ??
|
|
98
|
+
rawProduct.product_slug ??
|
|
99
|
+
(productId ? `product-${productId}` : 'product');
|
|
100
|
+
const slugPrefix =
|
|
101
|
+
rawProduct.slug_prefix ?? rawProduct.slugPrefix ?? rawProduct.category ?? 'artwork';
|
|
102
|
+
|
|
103
|
+
const mediaItems = normalizeMediaList(rawProduct.media);
|
|
104
|
+
const primaryImage =
|
|
105
|
+
mediaItems[0]?.link ??
|
|
106
|
+
rawProduct.image ??
|
|
107
|
+
rawProduct.image_url ??
|
|
108
|
+
rawProduct.thumbnail ??
|
|
109
|
+
'';
|
|
110
|
+
|
|
111
|
+
log(logger, 'debug', '[PerspectAPI] Transform product', {
|
|
112
|
+
productId,
|
|
113
|
+
productSlug,
|
|
114
|
+
mediaCount: mediaItems.length
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
id: typeof productId === 'string' ? productId : String(productId),
|
|
119
|
+
product: productName,
|
|
120
|
+
name: productName,
|
|
121
|
+
slug: productSlug,
|
|
122
|
+
slug_prefix: slugPrefix,
|
|
123
|
+
price: typeof rawProduct.price === 'number' ? rawProduct.price : Number(rawProduct.price ?? 0),
|
|
124
|
+
currency: rawProduct.currency ?? 'USD',
|
|
125
|
+
description: rawProduct.description ?? '',
|
|
126
|
+
description_markdown:
|
|
127
|
+
rawProduct.description_markdown ?? rawProduct.description ?? '',
|
|
128
|
+
image: primaryImage,
|
|
129
|
+
media: mediaItems,
|
|
130
|
+
gateway_product_id_live: rawProduct.gateway_product_id_live,
|
|
131
|
+
gateway_product_id_test: rawProduct.gateway_product_id_test,
|
|
132
|
+
stripe_product_id_live: rawProduct.stripe_product_id_live,
|
|
133
|
+
stripe_product_id_test: rawProduct.stripe_product_id_test,
|
|
134
|
+
isActive: rawProduct.isActive ?? rawProduct.is_active ?? true,
|
|
135
|
+
// Preserve original payload for advanced consumers
|
|
136
|
+
...rawProduct
|
|
137
|
+
};
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Transform PerspectAPI content payload to a simplified BlogPost structure.
|
|
142
|
+
*/
|
|
143
|
+
export const transformContent = (
|
|
144
|
+
perspectContent: Content | (Content & Record<string, unknown>)
|
|
145
|
+
): BlogPost => {
|
|
146
|
+
const raw = perspectContent ?? ({} as Content);
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
id: typeof raw.id === 'string' ? raw.id : String(raw.id ?? ''),
|
|
150
|
+
slug: raw.slug ?? `post-${raw.id ?? 'unknown'}`,
|
|
151
|
+
slug_prefix:
|
|
152
|
+
(raw as any).slug_prefix ??
|
|
153
|
+
(raw.pageType === 'post' ? 'blog' : 'page'),
|
|
154
|
+
page_type: raw.pageType,
|
|
155
|
+
title: raw.pageTitle,
|
|
156
|
+
content: raw.pageContent,
|
|
157
|
+
excerpt:
|
|
158
|
+
typeof raw.pageContent === 'string'
|
|
159
|
+
? `${raw.pageContent.slice(0, 200)}...`
|
|
160
|
+
: undefined,
|
|
161
|
+
published: raw.pageStatus === 'publish',
|
|
162
|
+
created_at: raw.createdAt,
|
|
163
|
+
updated_at: raw.updatedAt,
|
|
164
|
+
description: (raw as any).description,
|
|
165
|
+
published_date: (raw as any).published_date,
|
|
166
|
+
author: (raw as any).author,
|
|
167
|
+
tags: (raw as any).tags,
|
|
168
|
+
image: (raw as any).image
|
|
169
|
+
};
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
const getDefaultFallbackProducts = (siteName: string): Product[] => {
|
|
173
|
+
const safeSite = siteName.replace(/\s+/g, '-').toLowerCase();
|
|
174
|
+
|
|
175
|
+
const mediaSamples: MediaItem[] = [
|
|
176
|
+
{
|
|
177
|
+
media_id: `${safeSite}-media-1`,
|
|
178
|
+
attachment_id: 1001,
|
|
179
|
+
file_name: 'sample-artwork-1.jpg',
|
|
180
|
+
link: 'https://picsum.photos/1200/800?random=1',
|
|
181
|
+
content_type: 'image/jpeg',
|
|
182
|
+
width: 1200,
|
|
183
|
+
height: 800,
|
|
184
|
+
filesize: 245760,
|
|
185
|
+
r2_key: `${safeSite}/artwork-1.jpg`,
|
|
186
|
+
site_name: siteName
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
media_id: `${safeSite}-media-2`,
|
|
190
|
+
attachment_id: 1002,
|
|
191
|
+
file_name: 'sample-artwork-2.jpg',
|
|
192
|
+
link: 'https://picsum.photos/1200/800?random=2',
|
|
193
|
+
content_type: 'image/jpeg',
|
|
194
|
+
width: 1200,
|
|
195
|
+
height: 800,
|
|
196
|
+
filesize: 312480,
|
|
197
|
+
r2_key: `${safeSite}/artwork-2.jpg`,
|
|
198
|
+
site_name: siteName
|
|
199
|
+
}
|
|
200
|
+
];
|
|
201
|
+
|
|
202
|
+
return [
|
|
203
|
+
{
|
|
204
|
+
id: `${safeSite}-product-1`,
|
|
205
|
+
product: `${siteName} Sample Artwork 1`,
|
|
206
|
+
slug: `${safeSite}-sample-artwork-1`,
|
|
207
|
+
slug_prefix: 'artwork',
|
|
208
|
+
price: 299,
|
|
209
|
+
description: `Beautiful artwork from ${siteName}`,
|
|
210
|
+
description_markdown: `Beautiful artwork from **${siteName}**`,
|
|
211
|
+
image: 'https://picsum.photos/800/600?random=1',
|
|
212
|
+
media: mediaSamples
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
id: `${safeSite}-product-2`,
|
|
216
|
+
product: `${siteName} Sample Artwork 2`,
|
|
217
|
+
slug: `${safeSite}-sample-artwork-2`,
|
|
218
|
+
slug_prefix: 'artwork',
|
|
219
|
+
price: 499,
|
|
220
|
+
description: `Exquisite piece from ${siteName} collection`,
|
|
221
|
+
description_markdown: `Exquisite piece from **${siteName}** collection`,
|
|
222
|
+
image: 'https://picsum.photos/800/600?random=2',
|
|
223
|
+
media: mediaSamples
|
|
224
|
+
}
|
|
225
|
+
];
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
const getDefaultFallbackPosts = (siteName: string): BlogPost[] => {
|
|
229
|
+
return [
|
|
230
|
+
{
|
|
231
|
+
id: `${siteName}-post-1`,
|
|
232
|
+
slug: 'welcome-post',
|
|
233
|
+
slug_prefix: 'blog',
|
|
234
|
+
page_type: 'post',
|
|
235
|
+
title: `Welcome to ${siteName}`,
|
|
236
|
+
content: `<p>Welcome to our ${siteName} blog. Here you'll find articles about art, culture, and our collections.</p>`,
|
|
237
|
+
excerpt: `Welcome to our ${siteName} blog.`,
|
|
238
|
+
published: true,
|
|
239
|
+
created_at: new Date().toISOString()
|
|
240
|
+
}
|
|
241
|
+
];
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
export interface LoaderOptions {
|
|
245
|
+
/**
|
|
246
|
+
* Pre-configured PerspectAPI client. Pass null/undefined to force fallback behaviour.
|
|
247
|
+
*/
|
|
248
|
+
client?: PerspectApiClient | null;
|
|
249
|
+
/**
|
|
250
|
+
* Site name used for API calls and fallback data generation.
|
|
251
|
+
*/
|
|
252
|
+
siteName: string;
|
|
253
|
+
/**
|
|
254
|
+
* Optional logger to capture debug/warn/error output.
|
|
255
|
+
*/
|
|
256
|
+
logger?: LoaderLogger;
|
|
257
|
+
/**
|
|
258
|
+
* Optional fallback datasets.
|
|
259
|
+
*/
|
|
260
|
+
fallbackProducts?: Product[];
|
|
261
|
+
fallbackPosts?: BlogPost[];
|
|
262
|
+
/**
|
|
263
|
+
* Limit for list fetching (defaults to 100).
|
|
264
|
+
*/
|
|
265
|
+
limit?: number;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export interface LoadProductsOptions extends LoaderOptions {
|
|
269
|
+
/**
|
|
270
|
+
* Optional search term applied server-side.
|
|
271
|
+
*/
|
|
272
|
+
search?: string;
|
|
273
|
+
/**
|
|
274
|
+
* Offset for manual pagination.
|
|
275
|
+
*/
|
|
276
|
+
offset?: number;
|
|
277
|
+
/**
|
|
278
|
+
* Filter products by category name (case-insensitive). Accepts a single name or multiple names.
|
|
279
|
+
*/
|
|
280
|
+
category?: string | string[];
|
|
281
|
+
/**
|
|
282
|
+
* Filter products by category IDs (mixed string/number values supported).
|
|
283
|
+
*/
|
|
284
|
+
categoryIds?: Array<number | string>;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const resolveFallbackProducts = (
|
|
288
|
+
{ siteName, fallbackProducts }: Pick<LoaderOptions, 'siteName' | 'fallbackProducts'>
|
|
289
|
+
): Product[] => {
|
|
290
|
+
if (fallbackProducts && fallbackProducts.length > 0) {
|
|
291
|
+
return fallbackProducts;
|
|
292
|
+
}
|
|
293
|
+
return getDefaultFallbackProducts(siteName);
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
const resolveFallbackPosts = (
|
|
297
|
+
{ siteName, fallbackPosts }: Pick<LoaderOptions, 'siteName' | 'fallbackPosts'>
|
|
298
|
+
): BlogPost[] => {
|
|
299
|
+
if (fallbackPosts && fallbackPosts.length > 0) {
|
|
300
|
+
return fallbackPosts;
|
|
301
|
+
}
|
|
302
|
+
return getDefaultFallbackPosts(siteName);
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Load products for a site with graceful fallbacks.
|
|
307
|
+
*/
|
|
308
|
+
export async function loadProducts(options: LoadProductsOptions): Promise<Product[]> {
|
|
309
|
+
const {
|
|
310
|
+
client,
|
|
311
|
+
siteName,
|
|
312
|
+
logger = noopLogger,
|
|
313
|
+
fallbackProducts,
|
|
314
|
+
limit = 100,
|
|
315
|
+
offset,
|
|
316
|
+
search,
|
|
317
|
+
category,
|
|
318
|
+
categoryIds
|
|
319
|
+
} = options;
|
|
320
|
+
|
|
321
|
+
if (!client) {
|
|
322
|
+
log(logger, 'warn', '[PerspectAPI] No client configured, using fallback products');
|
|
323
|
+
return resolveFallbackProducts({ siteName, fallbackProducts });
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
try {
|
|
327
|
+
log(logger, 'info', `[PerspectAPI] Loading products for site "${siteName}"`);
|
|
328
|
+
const queryParams: ProductQueryParams = {
|
|
329
|
+
isActive: true,
|
|
330
|
+
limit,
|
|
331
|
+
offset,
|
|
332
|
+
search
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
const normalizedCategory = normalizeQueryParamList(category as any);
|
|
336
|
+
if (normalizedCategory) {
|
|
337
|
+
queryParams.category = normalizedCategory;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const normalizedCategoryIds = normalizeQueryParamList(categoryIds);
|
|
341
|
+
if (normalizedCategoryIds) {
|
|
342
|
+
queryParams.category_id = normalizedCategoryIds;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const response: PaginatedResponse<Product> = await client.products.getProducts(siteName, queryParams);
|
|
346
|
+
|
|
347
|
+
if (!response.data) {
|
|
348
|
+
log(logger, 'warn', '[PerspectAPI] Products response missing data, returning fallback set');
|
|
349
|
+
return resolveFallbackProducts({ siteName, fallbackProducts });
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
log(logger, 'debug', `[PerspectAPI] Found ${response.data.length} products, transforming...`);
|
|
353
|
+
return response.data.map(product => transformProduct(product, logger));
|
|
354
|
+
} catch (error) {
|
|
355
|
+
log(logger, 'error', '[PerspectAPI] Error loading products', error);
|
|
356
|
+
return resolveFallbackProducts({ siteName, fallbackProducts });
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
export interface LoadProductBySlugOptions extends LoaderOptions {
|
|
361
|
+
slug: string;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Load single product by slug.
|
|
366
|
+
*/
|
|
367
|
+
export async function loadProductBySlug(
|
|
368
|
+
options: LoadProductBySlugOptions
|
|
369
|
+
): Promise<Product | null> {
|
|
370
|
+
const {
|
|
371
|
+
client,
|
|
372
|
+
siteName,
|
|
373
|
+
slug,
|
|
374
|
+
logger = noopLogger,
|
|
375
|
+
fallbackProducts
|
|
376
|
+
} = options;
|
|
377
|
+
|
|
378
|
+
if (!client) {
|
|
379
|
+
log(logger, 'warn', '[PerspectAPI] No client configured, searching fallback products');
|
|
380
|
+
const fallback = resolveFallbackProducts({ siteName, fallbackProducts });
|
|
381
|
+
return fallback.find(product => product.slug === slug) ?? null;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
try {
|
|
385
|
+
log(logger, 'info', `[PerspectAPI] Loading product "${slug}" for site "${siteName}"`);
|
|
386
|
+
const response: ApiResponse<Product & { variants?: unknown[] }> =
|
|
387
|
+
await client.products.getProductBySlug(siteName, slug);
|
|
388
|
+
|
|
389
|
+
if (!response.data) {
|
|
390
|
+
log(logger, 'warn', `[PerspectAPI] Product not found for slug "${slug}"`);
|
|
391
|
+
return null;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
return transformProduct(response.data, logger);
|
|
395
|
+
} catch (error) {
|
|
396
|
+
log(logger, 'error', `[PerspectAPI] Error loading product slug "${slug}"`, error);
|
|
397
|
+
return null;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
export interface LoadContentOptions extends LoaderOptions {
|
|
402
|
+
/**
|
|
403
|
+
* Additional query parameters for content filtering.
|
|
404
|
+
*/
|
|
405
|
+
params?: Partial<{
|
|
406
|
+
page_status: string;
|
|
407
|
+
page_type: string;
|
|
408
|
+
limit: number;
|
|
409
|
+
search: string;
|
|
410
|
+
}>;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Load published pages for a site.
|
|
415
|
+
*/
|
|
416
|
+
export async function loadPages(
|
|
417
|
+
options: LoadContentOptions
|
|
418
|
+
): Promise<BlogPost[]> {
|
|
419
|
+
const { client, siteName, logger = noopLogger, params, fallbackPosts } = options;
|
|
420
|
+
|
|
421
|
+
if (!client) {
|
|
422
|
+
log(logger, 'warn', '[PerspectAPI] No client configured, using fallback posts for pages');
|
|
423
|
+
return resolveFallbackPosts({ siteName, fallbackPosts }).filter(
|
|
424
|
+
post => post.page_type !== 'post'
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
try {
|
|
429
|
+
log(logger, 'info', `[PerspectAPI] Loading pages for site "${siteName}"`);
|
|
430
|
+
const response: PaginatedResponse<Content> = await client.content.getContent(
|
|
431
|
+
siteName,
|
|
432
|
+
{
|
|
433
|
+
...params,
|
|
434
|
+
page_status: (params?.page_status ?? 'publish') as ContentStatus,
|
|
435
|
+
page_type: (params?.page_type ?? 'page') as ContentType,
|
|
436
|
+
limit: options.limit ?? params?.limit ?? 100
|
|
437
|
+
}
|
|
438
|
+
);
|
|
439
|
+
|
|
440
|
+
if (!response.data) {
|
|
441
|
+
return [];
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
return response.data.map(content => transformContent(content));
|
|
445
|
+
} catch (error) {
|
|
446
|
+
log(logger, 'error', '[PerspectAPI] Error loading pages', error);
|
|
447
|
+
return [];
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Load published blog posts for a site.
|
|
453
|
+
*/
|
|
454
|
+
export async function loadPosts(
|
|
455
|
+
options: LoadContentOptions
|
|
456
|
+
): Promise<BlogPost[]> {
|
|
457
|
+
const { client, siteName, logger = noopLogger, params, fallbackPosts } = options;
|
|
458
|
+
|
|
459
|
+
if (!client) {
|
|
460
|
+
log(logger, 'warn', '[PerspectAPI] No client configured, using fallback posts');
|
|
461
|
+
return resolveFallbackPosts({ siteName, fallbackPosts });
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
try {
|
|
465
|
+
log(logger, 'info', `[PerspectAPI] Loading posts for site "${siteName}"`);
|
|
466
|
+
const response: PaginatedResponse<Content> = await client.content.getContent(
|
|
467
|
+
siteName,
|
|
468
|
+
{
|
|
469
|
+
...params,
|
|
470
|
+
page_status: (params?.page_status ?? 'publish') as ContentStatus,
|
|
471
|
+
page_type: (params?.page_type ?? 'post') as ContentType,
|
|
472
|
+
limit: options.limit ?? params?.limit ?? 100
|
|
473
|
+
}
|
|
474
|
+
);
|
|
475
|
+
|
|
476
|
+
if (!response.data) {
|
|
477
|
+
log(logger, 'warn', '[PerspectAPI] Posts response missing data');
|
|
478
|
+
return [];
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
return response.data.map(content => transformContent(content));
|
|
482
|
+
} catch (error) {
|
|
483
|
+
log(logger, 'error', '[PerspectAPI] Error loading posts', error);
|
|
484
|
+
return [];
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
export interface LoadContentBySlugOptions extends LoaderOptions {
|
|
489
|
+
slug: string;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Load a single content item (post or page) by slug.
|
|
494
|
+
*/
|
|
495
|
+
export async function loadContentBySlug(
|
|
496
|
+
options: LoadContentBySlugOptions
|
|
497
|
+
): Promise<BlogPost | null> {
|
|
498
|
+
const {
|
|
499
|
+
client,
|
|
500
|
+
siteName,
|
|
501
|
+
slug,
|
|
502
|
+
logger = noopLogger,
|
|
503
|
+
fallbackPosts
|
|
504
|
+
} = options;
|
|
505
|
+
|
|
506
|
+
if (!client) {
|
|
507
|
+
log(logger, 'warn', '[PerspectAPI] No client configured, searching fallback posts');
|
|
508
|
+
const fallback = resolveFallbackPosts({ siteName, fallbackPosts });
|
|
509
|
+
return fallback.find(post => post.slug === slug) ?? null;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
try {
|
|
513
|
+
log(logger, 'info', `[PerspectAPI] Loading content slug "${slug}" for site "${siteName}"`);
|
|
514
|
+
const response: ApiResponse<Content> =
|
|
515
|
+
await client.content.getContentBySlug(siteName, slug);
|
|
516
|
+
|
|
517
|
+
if (!response.data) {
|
|
518
|
+
log(logger, 'warn', `[PerspectAPI] Content not found for slug "${slug}"`);
|
|
519
|
+
return null;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
return transformContent(response.data);
|
|
523
|
+
} catch (error) {
|
|
524
|
+
log(logger, 'error', `[PerspectAPI] Error loading content slug "${slug}"`, error);
|
|
525
|
+
return null;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* Load all published content (pages + posts).
|
|
531
|
+
*/
|
|
532
|
+
export async function loadAllContent(
|
|
533
|
+
options: LoaderOptions
|
|
534
|
+
): Promise<BlogPost[]> {
|
|
535
|
+
const { logger = noopLogger } = options;
|
|
536
|
+
|
|
537
|
+
const [pages, posts] = await Promise.all([
|
|
538
|
+
loadPages(options),
|
|
539
|
+
loadPosts(options)
|
|
540
|
+
]);
|
|
541
|
+
|
|
542
|
+
log(logger, 'debug', `[PerspectAPI] Loaded ${pages.length + posts.length} total content items`);
|
|
543
|
+
return [...pages, ...posts];
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
export interface CheckoutSessionOptions {
|
|
547
|
+
client?: PerspectApiClient | null;
|
|
548
|
+
siteName: string;
|
|
549
|
+
items: Array<{ productId: string; quantity: number }>;
|
|
550
|
+
successUrl: string;
|
|
551
|
+
cancelUrl: string;
|
|
552
|
+
customerEmail?: string;
|
|
553
|
+
mode?: 'live' | 'test';
|
|
554
|
+
logger?: LoaderLogger;
|
|
555
|
+
fallbackProducts?: Product[];
|
|
556
|
+
metadata?: CheckoutMetadata;
|
|
557
|
+
/**
|
|
558
|
+
* Optional resolver to convert a product record into a Stripe price ID.
|
|
559
|
+
*/
|
|
560
|
+
priceIdResolver?: (product: Product, mode: 'live' | 'test') => string | undefined;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* Convenience helper that creates a checkout session by looking up Stripe price IDs
|
|
565
|
+
* from product metadata. Falls back to gateway price IDs if Stripe IDs are missing.
|
|
566
|
+
*/
|
|
567
|
+
export async function createCheckoutSession(
|
|
568
|
+
options: CheckoutSessionOptions
|
|
569
|
+
): Promise<ApiResponse<CheckoutSession> | { error: string }> {
|
|
570
|
+
const {
|
|
571
|
+
client,
|
|
572
|
+
siteName,
|
|
573
|
+
items,
|
|
574
|
+
successUrl,
|
|
575
|
+
cancelUrl,
|
|
576
|
+
customerEmail,
|
|
577
|
+
mode = 'live',
|
|
578
|
+
logger = noopLogger,
|
|
579
|
+
fallbackProducts,
|
|
580
|
+
metadata,
|
|
581
|
+
priceIdResolver
|
|
582
|
+
} = options;
|
|
583
|
+
|
|
584
|
+
if (!client) {
|
|
585
|
+
log(logger, 'error', '[PerspectAPI] Cannot create checkout session without SDK client');
|
|
586
|
+
return { error: 'PerspectAPI client not configured' };
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
try {
|
|
590
|
+
// Fetch active products to build price map
|
|
591
|
+
const products = await loadProducts({
|
|
592
|
+
client,
|
|
593
|
+
siteName,
|
|
594
|
+
logger,
|
|
595
|
+
limit: 200,
|
|
596
|
+
fallbackProducts
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
const productMap = new Map<string, Product>(
|
|
600
|
+
products.map(product => [String(product.id), product])
|
|
601
|
+
);
|
|
602
|
+
|
|
603
|
+
const line_items = items.map(item => {
|
|
604
|
+
const product = productMap.get(String(item.productId));
|
|
605
|
+
|
|
606
|
+
if (!product) {
|
|
607
|
+
throw new Error(`Product ${item.productId} not found while building checkout session`);
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
const resolvedPrice =
|
|
611
|
+
priceIdResolver?.(product, mode) ??
|
|
612
|
+
(mode === 'test'
|
|
613
|
+
? product.stripe_product_id_test ?? (product as any).gateway_product_id_test
|
|
614
|
+
: product.stripe_product_id_live ?? (product as any).gateway_product_id_live);
|
|
615
|
+
|
|
616
|
+
if (!resolvedPrice) {
|
|
617
|
+
throw new Error(`Missing Stripe price ID for product ${item.productId} (${product.product ?? product.name ?? 'unknown'})`);
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
return {
|
|
621
|
+
price: resolvedPrice,
|
|
622
|
+
quantity: item.quantity
|
|
623
|
+
};
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
const checkoutData: CreateCheckoutSessionRequest = {
|
|
627
|
+
line_items,
|
|
628
|
+
successUrl,
|
|
629
|
+
cancelUrl,
|
|
630
|
+
success_url: successUrl,
|
|
631
|
+
cancel_url: cancelUrl,
|
|
632
|
+
customerEmail,
|
|
633
|
+
customer_email: customerEmail,
|
|
634
|
+
mode: mode === 'test' ? 'payment' : 'payment',
|
|
635
|
+
metadata
|
|
636
|
+
};
|
|
637
|
+
|
|
638
|
+
log(logger, 'info', '[PerspectAPI] Creating checkout session');
|
|
639
|
+
return client.checkout.createCheckoutSession(siteName, checkoutData);
|
|
640
|
+
} catch (error) {
|
|
641
|
+
log(logger, 'error', '[PerspectAPI] Failed to create checkout session', error);
|
|
642
|
+
return { error: error instanceof Error ? error.message : 'Unknown error creating checkout session' };
|
|
643
|
+
}
|
|
644
|
+
}
|