perspectapi-ts-sdk 2.0.0 → 2.2.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/README.md +82 -0
- package/dist/index.d.mts +34 -1
- package/dist/index.d.ts +34 -1
- package/dist/index.js +125 -10
- package/dist/index.mjs +125 -10
- package/package.json +1 -1
- package/src/cache/cache-manager.ts +17 -0
- package/src/client/content-client.ts +28 -2
- package/src/client/products-client.ts +11 -0
- package/src/loaders.ts +65 -10
- package/src/types/index.ts +40 -0
- package/src/utils/validators.ts +53 -0
package/README.md
CHANGED
|
@@ -148,6 +148,88 @@ export default {
|
|
|
148
148
|
```
|
|
149
149
|
|
|
150
150
|
When PerspectAPI sends a webhook—or when your Worker mutates data directly—call `perspect.cache.invalidate({ tags: [...] })` using the tags emitted by the SDK (`products:site:<site>`, `content:slug:<site>:<slug>`, etc.) so stale entries are purged immediately.
|
|
151
|
+
|
|
152
|
+
### Webhook-driven cache invalidation
|
|
153
|
+
|
|
154
|
+
Most deployments rely on PerspectAPI webhooks to bust cache entries whenever content changes. Your app needs an HTTP endpoint that:
|
|
155
|
+
|
|
156
|
+
1. Verifies the webhook signature (or shared secret).
|
|
157
|
+
2. Maps the webhook payload to the cache tags that were used when reading data.
|
|
158
|
+
3. Calls `client.cache.invalidate({ tags })`.
|
|
159
|
+
|
|
160
|
+
Below is a minimal Cloudflare Worker handler; the same flow works in Express, Fastify, etc.—just swap the request parsing.
|
|
161
|
+
|
|
162
|
+
```ts
|
|
163
|
+
// worker.ts
|
|
164
|
+
import { PerspectApiClient } from 'perspectapi-ts-sdk';
|
|
165
|
+
|
|
166
|
+
const kvAdapter = (kv: KVNamespace) => ({
|
|
167
|
+
get: (key: string) => kv.get(key),
|
|
168
|
+
set: (key: string, value: string, options?: { ttlSeconds?: number }) =>
|
|
169
|
+
options?.ttlSeconds
|
|
170
|
+
? kv.put(key, value, { expirationTtl: options.ttlSeconds })
|
|
171
|
+
: kv.put(key, value),
|
|
172
|
+
delete: (key: string) => kv.delete(key),
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
const perspect = new PerspectApiClient({
|
|
176
|
+
baseUrl: env.PERSPECT_API_URL,
|
|
177
|
+
apiKey: env.PERSPECT_API_KEY,
|
|
178
|
+
cache: { adapter: kvAdapter(env.PERSPECT_CACHE), defaultTtlSeconds: 300 },
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
type WebhookEvent =
|
|
182
|
+
| { type: 'product.updated'; site: string; slug?: string; id?: number }
|
|
183
|
+
| { type: 'content.published'; site: string; slug: string; id: number }
|
|
184
|
+
| { type: 'category.updated'; site: string; slug: string };
|
|
185
|
+
|
|
186
|
+
const tagMap: Record<string, (e: WebhookEvent) => string[]> = {
|
|
187
|
+
'product.updated': event => [
|
|
188
|
+
`products`,
|
|
189
|
+
`products:site:${event.site}`,
|
|
190
|
+
event.slug ? `products:slug:${event.site}:${event.slug}` : '',
|
|
191
|
+
event.id ? `products:id:${event.id}` : '',
|
|
192
|
+
],
|
|
193
|
+
'content.published': event => [
|
|
194
|
+
`content`,
|
|
195
|
+
`content:site:${event.site}`,
|
|
196
|
+
`content:slug:${event.site}:${event.slug}`,
|
|
197
|
+
`content:id:${event.id}`,
|
|
198
|
+
],
|
|
199
|
+
'category.updated': event => [
|
|
200
|
+
`categories`,
|
|
201
|
+
`categories:site:${event.site}`,
|
|
202
|
+
`categories:product:${event.site}:${event.slug}`,
|
|
203
|
+
],
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
export default {
|
|
207
|
+
async fetch(request: Request, env: Env) {
|
|
208
|
+
const url = new URL(request.url);
|
|
209
|
+
|
|
210
|
+
if (url.pathname === '/webhooks/perspect' && request.method === 'POST') {
|
|
211
|
+
const event = (await request.json()) as WebhookEvent;
|
|
212
|
+
|
|
213
|
+
// TODO: validate shared secret / signature before proceeding
|
|
214
|
+
|
|
215
|
+
const buildTags = tagMap[event.type];
|
|
216
|
+
if (buildTags) {
|
|
217
|
+
const tags = buildTags(event).filter(Boolean);
|
|
218
|
+
if (tags.length) {
|
|
219
|
+
await perspect.cache.invalidate({ tags });
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return new Response(null, { status: 202 });
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// ...rest of your app
|
|
227
|
+
return new Response('ok');
|
|
228
|
+
},
|
|
229
|
+
};
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
> 🔁 Adjust the `WebhookEvent` union and `tagMap` to match the actual payloads you receive. PerspectAPI webhooks also carry version IDs and environment metadata that you can use for more granular targeting if needed.
|
|
151
233
|
```
|
|
152
234
|
|
|
153
235
|
## Image Transformations
|
package/dist/index.d.mts
CHANGED
|
@@ -424,6 +424,34 @@ type CheckoutMetadataValue = string | number | boolean | null | CheckoutMetadata
|
|
|
424
424
|
[key: string]: CheckoutMetadataValue;
|
|
425
425
|
};
|
|
426
426
|
type CheckoutMetadata = Record<string, CheckoutMetadataValue>;
|
|
427
|
+
type CheckoutTaxStrategy = 'disabled' | 'gateway_auto' | 'manual_rates' | 'external_service';
|
|
428
|
+
type CheckoutTaxExemptionStatus = 'none' | 'exempt' | 'reverse_charge';
|
|
429
|
+
interface CheckoutTaxCustomerExemptionRequest {
|
|
430
|
+
status?: CheckoutTaxExemptionStatus;
|
|
431
|
+
reason?: string;
|
|
432
|
+
tax_id?: string;
|
|
433
|
+
tax_id_type?: string;
|
|
434
|
+
certificate_url?: string;
|
|
435
|
+
metadata?: Record<string, any>;
|
|
436
|
+
expires_at?: string;
|
|
437
|
+
}
|
|
438
|
+
interface CheckoutTaxRequest {
|
|
439
|
+
strategy?: CheckoutTaxStrategy;
|
|
440
|
+
customer_identifier?: string;
|
|
441
|
+
customer_profile_id?: string;
|
|
442
|
+
customer_display_name?: string;
|
|
443
|
+
allow_exemption?: boolean;
|
|
444
|
+
require_tax_id?: boolean;
|
|
445
|
+
save_profile?: boolean;
|
|
446
|
+
customer_exemption?: CheckoutTaxCustomerExemptionRequest;
|
|
447
|
+
manual_rate_percent?: number;
|
|
448
|
+
manual_rate_map?: Record<string, number>;
|
|
449
|
+
external_service?: {
|
|
450
|
+
provider: string;
|
|
451
|
+
config?: Record<string, any>;
|
|
452
|
+
};
|
|
453
|
+
metadata?: Record<string, any>;
|
|
454
|
+
}
|
|
427
455
|
interface CreateCheckoutSessionRequest {
|
|
428
456
|
priceId?: string;
|
|
429
457
|
quantity?: number;
|
|
@@ -455,6 +483,7 @@ interface CreateCheckoutSessionRequest {
|
|
|
455
483
|
allowed_countries: string[];
|
|
456
484
|
};
|
|
457
485
|
billing_address_collection?: 'auto' | 'required';
|
|
486
|
+
tax?: CheckoutTaxRequest;
|
|
458
487
|
}
|
|
459
488
|
interface CheckoutSession {
|
|
460
489
|
id: string;
|
|
@@ -2322,6 +2351,10 @@ interface CheckoutSessionOptions {
|
|
|
2322
2351
|
* Optional resolver to convert a product record into a Stripe price ID.
|
|
2323
2352
|
*/
|
|
2324
2353
|
priceIdResolver?: (product: Product, mode: 'live' | 'test') => string | undefined;
|
|
2354
|
+
/**
|
|
2355
|
+
* Optional tax configuration that will be forwarded to the checkout API.
|
|
2356
|
+
*/
|
|
2357
|
+
tax?: CheckoutTaxRequest;
|
|
2325
2358
|
}
|
|
2326
2359
|
/**
|
|
2327
2360
|
* Convenience helper that creates a checkout session by looking up Stripe price IDs
|
|
@@ -2331,4 +2364,4 @@ declare function createCheckoutSession(options: CheckoutSessionOptions): Promise
|
|
|
2331
2364
|
error: string;
|
|
2332
2365
|
}>;
|
|
2333
2366
|
|
|
2334
|
-
export { type ApiError, type ApiKey, ApiKeysClient, type ApiResponse, AuthClient, type AuthResponse, BaseClient, type BlogPost, type CacheConfig, CacheManager, CategoriesClient, type Category, CheckoutClient, type CheckoutMetadata, type CheckoutMetadataValue, type CheckoutSession, type CheckoutSessionOptions, ContactClient, type ContactStatusResponse, type ContactSubmission, type ContactSubmitResponse, type Content, ContentClient, type ContentQueryParams, type ContentStatus, type ContentType, type CreateApiKeyRequest, type CreateCategoryRequest, type CreateCheckoutSessionRequest, type CreateContactRequest, type CreateContentRequest, type CreateNewsletterSubscriptionRequest, type CreateOrganizationRequest, type CreatePaymentGatewayRequest, type CreateProductRequest, type CreateSiteRequest, type CreateWebhookRequest, DEFAULT_IMAGE_SIZES, HttpClient, type HttpMethod, type ImageTransformOptions, InMemoryCacheAdapter, type LoadContentBySlugOptions, type LoadContentOptions, type LoadProductBySlugOptions, type LoadProductsOptions, type LoaderLogger, type LoaderOptions, type MediaItem, NewsletterClient, type NewsletterConfirmResponse, type NewsletterList, type NewsletterPreferences, type NewsletterStatusResponse, type NewsletterSubscribeResponse, type NewsletterSubscription, type NewsletterUnsubscribeRequest, type NewsletterUnsubscribeResponse, NoopCacheAdapter, type Organization, OrganizationsClient, type PaginatedResponse, type PaginationParams, type PaymentGateway, PerspectApiClient, type PerspectApiConfig, type Product, type ProductQueryParams, ProductsClient, type RequestOptions, type ResponsiveImageSizes, type SignInRequest, type SignUpRequest, type Site, SitesClient, type UpdateApiKeyRequest, type UpdateContentRequest, type User, type Webhook, WebhooksClient, buildImageUrl, createApiError, createCheckoutSession, createPerspectApiClient, PerspectApiClient as default, generateResponsiveImageHtml, generateResponsiveUrls, generateSizesAttribute, generateSrcSet, loadAllContent, loadContentBySlug, loadPages, loadPosts, loadProductBySlug, loadProducts, transformContent, transformMediaItem, transformProduct };
|
|
2367
|
+
export { type ApiError, type ApiKey, ApiKeysClient, type ApiResponse, AuthClient, type AuthResponse, BaseClient, type BlogPost, type CacheConfig, CacheManager, CategoriesClient, type Category, CheckoutClient, type CheckoutMetadata, type CheckoutMetadataValue, type CheckoutSession, type CheckoutSessionOptions, type CheckoutTaxCustomerExemptionRequest, type CheckoutTaxExemptionStatus, type CheckoutTaxRequest, type CheckoutTaxStrategy, ContactClient, type ContactStatusResponse, type ContactSubmission, type ContactSubmitResponse, type Content, ContentClient, type ContentQueryParams, type ContentStatus, type ContentType, type CreateApiKeyRequest, type CreateCategoryRequest, type CreateCheckoutSessionRequest, type CreateContactRequest, type CreateContentRequest, type CreateNewsletterSubscriptionRequest, type CreateOrganizationRequest, type CreatePaymentGatewayRequest, type CreateProductRequest, type CreateSiteRequest, type CreateWebhookRequest, DEFAULT_IMAGE_SIZES, HttpClient, type HttpMethod, type ImageTransformOptions, InMemoryCacheAdapter, type LoadContentBySlugOptions, type LoadContentOptions, type LoadProductBySlugOptions, type LoadProductsOptions, type LoaderLogger, type LoaderOptions, type MediaItem, NewsletterClient, type NewsletterConfirmResponse, type NewsletterList, type NewsletterPreferences, type NewsletterStatusResponse, type NewsletterSubscribeResponse, type NewsletterSubscription, type NewsletterUnsubscribeRequest, type NewsletterUnsubscribeResponse, NoopCacheAdapter, type Organization, OrganizationsClient, type PaginatedResponse, type PaginationParams, type PaymentGateway, PerspectApiClient, type PerspectApiConfig, type Product, type ProductQueryParams, ProductsClient, type RequestOptions, type ResponsiveImageSizes, type SignInRequest, type SignUpRequest, type Site, SitesClient, type UpdateApiKeyRequest, type UpdateContentRequest, type User, type Webhook, WebhooksClient, buildImageUrl, createApiError, createCheckoutSession, createPerspectApiClient, PerspectApiClient as default, generateResponsiveImageHtml, generateResponsiveUrls, generateSizesAttribute, generateSrcSet, loadAllContent, loadContentBySlug, loadPages, loadPosts, loadProductBySlug, loadProducts, transformContent, transformMediaItem, transformProduct };
|
package/dist/index.d.ts
CHANGED
|
@@ -424,6 +424,34 @@ type CheckoutMetadataValue = string | number | boolean | null | CheckoutMetadata
|
|
|
424
424
|
[key: string]: CheckoutMetadataValue;
|
|
425
425
|
};
|
|
426
426
|
type CheckoutMetadata = Record<string, CheckoutMetadataValue>;
|
|
427
|
+
type CheckoutTaxStrategy = 'disabled' | 'gateway_auto' | 'manual_rates' | 'external_service';
|
|
428
|
+
type CheckoutTaxExemptionStatus = 'none' | 'exempt' | 'reverse_charge';
|
|
429
|
+
interface CheckoutTaxCustomerExemptionRequest {
|
|
430
|
+
status?: CheckoutTaxExemptionStatus;
|
|
431
|
+
reason?: string;
|
|
432
|
+
tax_id?: string;
|
|
433
|
+
tax_id_type?: string;
|
|
434
|
+
certificate_url?: string;
|
|
435
|
+
metadata?: Record<string, any>;
|
|
436
|
+
expires_at?: string;
|
|
437
|
+
}
|
|
438
|
+
interface CheckoutTaxRequest {
|
|
439
|
+
strategy?: CheckoutTaxStrategy;
|
|
440
|
+
customer_identifier?: string;
|
|
441
|
+
customer_profile_id?: string;
|
|
442
|
+
customer_display_name?: string;
|
|
443
|
+
allow_exemption?: boolean;
|
|
444
|
+
require_tax_id?: boolean;
|
|
445
|
+
save_profile?: boolean;
|
|
446
|
+
customer_exemption?: CheckoutTaxCustomerExemptionRequest;
|
|
447
|
+
manual_rate_percent?: number;
|
|
448
|
+
manual_rate_map?: Record<string, number>;
|
|
449
|
+
external_service?: {
|
|
450
|
+
provider: string;
|
|
451
|
+
config?: Record<string, any>;
|
|
452
|
+
};
|
|
453
|
+
metadata?: Record<string, any>;
|
|
454
|
+
}
|
|
427
455
|
interface CreateCheckoutSessionRequest {
|
|
428
456
|
priceId?: string;
|
|
429
457
|
quantity?: number;
|
|
@@ -455,6 +483,7 @@ interface CreateCheckoutSessionRequest {
|
|
|
455
483
|
allowed_countries: string[];
|
|
456
484
|
};
|
|
457
485
|
billing_address_collection?: 'auto' | 'required';
|
|
486
|
+
tax?: CheckoutTaxRequest;
|
|
458
487
|
}
|
|
459
488
|
interface CheckoutSession {
|
|
460
489
|
id: string;
|
|
@@ -2322,6 +2351,10 @@ interface CheckoutSessionOptions {
|
|
|
2322
2351
|
* Optional resolver to convert a product record into a Stripe price ID.
|
|
2323
2352
|
*/
|
|
2324
2353
|
priceIdResolver?: (product: Product, mode: 'live' | 'test') => string | undefined;
|
|
2354
|
+
/**
|
|
2355
|
+
* Optional tax configuration that will be forwarded to the checkout API.
|
|
2356
|
+
*/
|
|
2357
|
+
tax?: CheckoutTaxRequest;
|
|
2325
2358
|
}
|
|
2326
2359
|
/**
|
|
2327
2360
|
* Convenience helper that creates a checkout session by looking up Stripe price IDs
|
|
@@ -2331,4 +2364,4 @@ declare function createCheckoutSession(options: CheckoutSessionOptions): Promise
|
|
|
2331
2364
|
error: string;
|
|
2332
2365
|
}>;
|
|
2333
2366
|
|
|
2334
|
-
export { type ApiError, type ApiKey, ApiKeysClient, type ApiResponse, AuthClient, type AuthResponse, BaseClient, type BlogPost, type CacheConfig, CacheManager, CategoriesClient, type Category, CheckoutClient, type CheckoutMetadata, type CheckoutMetadataValue, type CheckoutSession, type CheckoutSessionOptions, ContactClient, type ContactStatusResponse, type ContactSubmission, type ContactSubmitResponse, type Content, ContentClient, type ContentQueryParams, type ContentStatus, type ContentType, type CreateApiKeyRequest, type CreateCategoryRequest, type CreateCheckoutSessionRequest, type CreateContactRequest, type CreateContentRequest, type CreateNewsletterSubscriptionRequest, type CreateOrganizationRequest, type CreatePaymentGatewayRequest, type CreateProductRequest, type CreateSiteRequest, type CreateWebhookRequest, DEFAULT_IMAGE_SIZES, HttpClient, type HttpMethod, type ImageTransformOptions, InMemoryCacheAdapter, type LoadContentBySlugOptions, type LoadContentOptions, type LoadProductBySlugOptions, type LoadProductsOptions, type LoaderLogger, type LoaderOptions, type MediaItem, NewsletterClient, type NewsletterConfirmResponse, type NewsletterList, type NewsletterPreferences, type NewsletterStatusResponse, type NewsletterSubscribeResponse, type NewsletterSubscription, type NewsletterUnsubscribeRequest, type NewsletterUnsubscribeResponse, NoopCacheAdapter, type Organization, OrganizationsClient, type PaginatedResponse, type PaginationParams, type PaymentGateway, PerspectApiClient, type PerspectApiConfig, type Product, type ProductQueryParams, ProductsClient, type RequestOptions, type ResponsiveImageSizes, type SignInRequest, type SignUpRequest, type Site, SitesClient, type UpdateApiKeyRequest, type UpdateContentRequest, type User, type Webhook, WebhooksClient, buildImageUrl, createApiError, createCheckoutSession, createPerspectApiClient, PerspectApiClient as default, generateResponsiveImageHtml, generateResponsiveUrls, generateSizesAttribute, generateSrcSet, loadAllContent, loadContentBySlug, loadPages, loadPosts, loadProductBySlug, loadProducts, transformContent, transformMediaItem, transformProduct };
|
|
2367
|
+
export { type ApiError, type ApiKey, ApiKeysClient, type ApiResponse, AuthClient, type AuthResponse, BaseClient, type BlogPost, type CacheConfig, CacheManager, CategoriesClient, type Category, CheckoutClient, type CheckoutMetadata, type CheckoutMetadataValue, type CheckoutSession, type CheckoutSessionOptions, type CheckoutTaxCustomerExemptionRequest, type CheckoutTaxExemptionStatus, type CheckoutTaxRequest, type CheckoutTaxStrategy, ContactClient, type ContactStatusResponse, type ContactSubmission, type ContactSubmitResponse, type Content, ContentClient, type ContentQueryParams, type ContentStatus, type ContentType, type CreateApiKeyRequest, type CreateCategoryRequest, type CreateCheckoutSessionRequest, type CreateContactRequest, type CreateContentRequest, type CreateNewsletterSubscriptionRequest, type CreateOrganizationRequest, type CreatePaymentGatewayRequest, type CreateProductRequest, type CreateSiteRequest, type CreateWebhookRequest, DEFAULT_IMAGE_SIZES, HttpClient, type HttpMethod, type ImageTransformOptions, InMemoryCacheAdapter, type LoadContentBySlugOptions, type LoadContentOptions, type LoadProductBySlugOptions, type LoadProductsOptions, type LoaderLogger, type LoaderOptions, type MediaItem, NewsletterClient, type NewsletterConfirmResponse, type NewsletterList, type NewsletterPreferences, type NewsletterStatusResponse, type NewsletterSubscribeResponse, type NewsletterSubscription, type NewsletterUnsubscribeRequest, type NewsletterUnsubscribeResponse, NoopCacheAdapter, type Organization, OrganizationsClient, type PaginatedResponse, type PaginationParams, type PaymentGateway, PerspectApiClient, type PerspectApiConfig, type Product, type ProductQueryParams, ProductsClient, type RequestOptions, type ResponsiveImageSizes, type SignInRequest, type SignUpRequest, type Site, SitesClient, type UpdateApiKeyRequest, type UpdateContentRequest, type User, type Webhook, WebhooksClient, buildImageUrl, createApiError, createCheckoutSession, createPerspectApiClient, PerspectApiClient as default, generateResponsiveImageHtml, generateResponsiveUrls, generateSizesAttribute, generateSrcSet, loadAllContent, loadContentBySlug, loadPages, loadPosts, loadProductBySlug, loadProducts, transformContent, transformMediaItem, transformProduct };
|
package/dist/index.js
CHANGED
|
@@ -354,6 +354,7 @@ var CacheManager = class {
|
|
|
354
354
|
}
|
|
355
355
|
async getOrSet(key, resolveValue, policy) {
|
|
356
356
|
if (!this.enabled || policy?.skipCache) {
|
|
357
|
+
console.log("[Cache] Cache disabled or skipped", { key, enabled: this.enabled, skipCache: policy?.skipCache });
|
|
357
358
|
const value2 = await resolveValue();
|
|
358
359
|
if (this.enabled && !policy?.skipCache && policy?.ttlSeconds !== 0) {
|
|
359
360
|
await this.set(key, value2, policy);
|
|
@@ -365,12 +366,16 @@ var CacheManager = class {
|
|
|
365
366
|
if (cachedRaw) {
|
|
366
367
|
const entry = this.deserialize(cachedRaw);
|
|
367
368
|
if (!entry.expiresAt || entry.expiresAt > Date.now()) {
|
|
369
|
+
console.log("[Cache] \u2713 HIT", { key, tags: entry.tags });
|
|
368
370
|
return entry.value;
|
|
369
371
|
}
|
|
372
|
+
console.log("[Cache] \u2717 EXPIRED", { key, expiresAt: new Date(entry.expiresAt) });
|
|
370
373
|
await this.adapter.delete(namespacedKey);
|
|
371
374
|
if (entry.tags?.length) {
|
|
372
375
|
await this.removeKeyFromTags(namespacedKey, entry.tags);
|
|
373
376
|
}
|
|
377
|
+
} else {
|
|
378
|
+
console.log("[Cache] \u2717 MISS", { key });
|
|
374
379
|
}
|
|
375
380
|
const value = await resolveValue();
|
|
376
381
|
await this.set(key, value, policy);
|
|
@@ -388,6 +393,7 @@ var CacheManager = class {
|
|
|
388
393
|
tags: options?.tags,
|
|
389
394
|
metadata: options?.metadata
|
|
390
395
|
};
|
|
396
|
+
console.log("[Cache] SET", { key, ttlSeconds, tags: options?.tags });
|
|
391
397
|
await this.adapter.set(namespacedKey, this.serialize(entry), {
|
|
392
398
|
ttlSeconds: ttlSeconds > 0 ? ttlSeconds : void 0
|
|
393
399
|
});
|
|
@@ -406,34 +412,42 @@ var CacheManager = class {
|
|
|
406
412
|
if (!this.enabled) {
|
|
407
413
|
return;
|
|
408
414
|
}
|
|
415
|
+
let totalInvalidated = 0;
|
|
409
416
|
if (options.keys?.length) {
|
|
410
417
|
const namespacedKeys = options.keys.map((key) => this.namespacedKey(key));
|
|
418
|
+
console.log("[Cache] INVALIDATE by keys", { count: options.keys.length, keys: options.keys });
|
|
411
419
|
if (this.adapter.deleteMany) {
|
|
412
420
|
await this.adapter.deleteMany(namespacedKeys);
|
|
413
421
|
} else {
|
|
414
422
|
await Promise.all(namespacedKeys.map((key) => this.adapter.delete(key)));
|
|
415
423
|
}
|
|
424
|
+
totalInvalidated += options.keys.length;
|
|
416
425
|
}
|
|
417
426
|
if (options.tags?.length) {
|
|
427
|
+
console.log("[Cache] INVALIDATE by tags", { tags: options.tags });
|
|
418
428
|
await Promise.all(
|
|
419
429
|
options.tags.map(async (tag) => {
|
|
420
430
|
const tagKey = this.tagKey(tag);
|
|
421
431
|
const payload = await this.adapter.get(tagKey);
|
|
422
432
|
if (!payload) {
|
|
433
|
+
console.log("[Cache] No entries for tag", { tag });
|
|
423
434
|
return;
|
|
424
435
|
}
|
|
425
436
|
const keys = this.deserializeTagSet(payload);
|
|
426
437
|
if (keys.length) {
|
|
438
|
+
console.log("[Cache] Invalidating entries for tag", { tag, count: keys.length });
|
|
427
439
|
if (this.adapter.deleteMany) {
|
|
428
440
|
await this.adapter.deleteMany(keys);
|
|
429
441
|
} else {
|
|
430
442
|
await Promise.all(keys.map((key) => this.adapter.delete(key)));
|
|
431
443
|
}
|
|
444
|
+
totalInvalidated += keys.length;
|
|
432
445
|
}
|
|
433
446
|
await this.adapter.delete(tagKey);
|
|
434
447
|
})
|
|
435
448
|
);
|
|
436
449
|
}
|
|
450
|
+
console.log("[Cache] \u2713 INVALIDATED", { totalEntries: totalInvalidated, keys: options.keys?.length || 0, tags: options.tags?.length || 0 });
|
|
437
451
|
}
|
|
438
452
|
namespacedKey(key) {
|
|
439
453
|
return `${this.keyPrefix}:${key}`;
|
|
@@ -715,6 +729,43 @@ var AuthClient = class extends BaseClient {
|
|
|
715
729
|
}
|
|
716
730
|
};
|
|
717
731
|
|
|
732
|
+
// src/utils/validators.ts
|
|
733
|
+
var MAX_API_QUERY_LIMIT = 100;
|
|
734
|
+
var ALLOWED_CONTENT_TYPES = ["post", "page"];
|
|
735
|
+
function validateLimit(limit, context) {
|
|
736
|
+
if (typeof limit !== "number" || Number.isNaN(limit) || !Number.isFinite(limit)) {
|
|
737
|
+
throw new Error(`[PerspectAPI] ${context} limit must be a finite number.`);
|
|
738
|
+
}
|
|
739
|
+
if (limit < 1) {
|
|
740
|
+
throw new Error(`[PerspectAPI] ${context} limit must be at least 1.`);
|
|
741
|
+
}
|
|
742
|
+
if (limit > MAX_API_QUERY_LIMIT) {
|
|
743
|
+
throw new Error(
|
|
744
|
+
`[PerspectAPI] ${context} limit ${limit} exceeds the maximum allowed value of ${MAX_API_QUERY_LIMIT}.`
|
|
745
|
+
);
|
|
746
|
+
}
|
|
747
|
+
return limit;
|
|
748
|
+
}
|
|
749
|
+
function validateOptionalLimit(limit, context) {
|
|
750
|
+
if (limit === void 0 || limit === null) {
|
|
751
|
+
return void 0;
|
|
752
|
+
}
|
|
753
|
+
return validateLimit(limit, context);
|
|
754
|
+
}
|
|
755
|
+
function validateOptionalContentType(pageType, context) {
|
|
756
|
+
if (pageType === void 0 || pageType === null) {
|
|
757
|
+
return void 0;
|
|
758
|
+
}
|
|
759
|
+
if (ALLOWED_CONTENT_TYPES.includes(pageType)) {
|
|
760
|
+
return pageType;
|
|
761
|
+
}
|
|
762
|
+
throw new Error(
|
|
763
|
+
`[PerspectAPI] ${context} received unsupported page_type "${pageType}". Allowed values are ${ALLOWED_CONTENT_TYPES.join(
|
|
764
|
+
", "
|
|
765
|
+
)}.`
|
|
766
|
+
);
|
|
767
|
+
}
|
|
768
|
+
|
|
718
769
|
// src/client/content-client.ts
|
|
719
770
|
var ContentClient = class extends BaseClient {
|
|
720
771
|
constructor(http, cache) {
|
|
@@ -726,12 +777,33 @@ var ContentClient = class extends BaseClient {
|
|
|
726
777
|
async getContent(siteName, params, cachePolicy) {
|
|
727
778
|
const endpoint = this.siteScopedEndpoint(siteName);
|
|
728
779
|
const path = this.buildPath(endpoint);
|
|
780
|
+
const normalizedParams = params ? { ...params } : void 0;
|
|
781
|
+
if (normalizedParams) {
|
|
782
|
+
const validatedLimit = validateOptionalLimit(
|
|
783
|
+
normalizedParams.limit,
|
|
784
|
+
"content query"
|
|
785
|
+
);
|
|
786
|
+
if (validatedLimit !== void 0) {
|
|
787
|
+
normalizedParams.limit = validatedLimit;
|
|
788
|
+
} else {
|
|
789
|
+
delete normalizedParams.limit;
|
|
790
|
+
}
|
|
791
|
+
const validatedPageType = validateOptionalContentType(
|
|
792
|
+
normalizedParams.page_type,
|
|
793
|
+
"content query"
|
|
794
|
+
);
|
|
795
|
+
if (validatedPageType !== void 0) {
|
|
796
|
+
normalizedParams.page_type = validatedPageType;
|
|
797
|
+
} else {
|
|
798
|
+
delete normalizedParams.page_type;
|
|
799
|
+
}
|
|
800
|
+
}
|
|
729
801
|
return this.fetchWithCache(
|
|
730
802
|
endpoint,
|
|
731
|
-
|
|
803
|
+
normalizedParams,
|
|
732
804
|
this.buildContentTags(siteName),
|
|
733
805
|
cachePolicy,
|
|
734
|
-
() => this.http.get(path,
|
|
806
|
+
() => this.http.get(path, normalizedParams)
|
|
735
807
|
);
|
|
736
808
|
}
|
|
737
809
|
/**
|
|
@@ -1093,6 +1165,15 @@ var ProductsClient = class extends BaseClient {
|
|
|
1093
1165
|
};
|
|
1094
1166
|
const normalizedParams = params ? { ...params } : void 0;
|
|
1095
1167
|
if (normalizedParams) {
|
|
1168
|
+
const validatedLimit = validateOptionalLimit(
|
|
1169
|
+
normalizedParams.limit,
|
|
1170
|
+
"products query"
|
|
1171
|
+
);
|
|
1172
|
+
if (validatedLimit !== void 0) {
|
|
1173
|
+
normalizedParams.limit = validatedLimit;
|
|
1174
|
+
} else {
|
|
1175
|
+
delete normalizedParams.limit;
|
|
1176
|
+
}
|
|
1096
1177
|
const normalizedCategories = normalizeList(normalizedParams.category);
|
|
1097
1178
|
if (normalizedCategories !== void 0) {
|
|
1098
1179
|
normalizedParams.category = normalizedCategories;
|
|
@@ -2308,12 +2389,16 @@ async function loadProducts(options) {
|
|
|
2308
2389
|
siteName,
|
|
2309
2390
|
logger = noopLogger,
|
|
2310
2391
|
fallbackProducts,
|
|
2311
|
-
limit
|
|
2392
|
+
limit: requestedLimit,
|
|
2312
2393
|
offset,
|
|
2313
2394
|
search,
|
|
2314
2395
|
category,
|
|
2315
2396
|
categoryIds
|
|
2316
2397
|
} = options;
|
|
2398
|
+
const resolvedLimit = validateLimit(
|
|
2399
|
+
requestedLimit ?? MAX_API_QUERY_LIMIT,
|
|
2400
|
+
"products query"
|
|
2401
|
+
);
|
|
2317
2402
|
if (!client) {
|
|
2318
2403
|
log(logger, "warn", "[PerspectAPI] No client configured, using fallback products");
|
|
2319
2404
|
return resolveFallbackProducts({ siteName, fallbackProducts });
|
|
@@ -2322,7 +2407,7 @@ async function loadProducts(options) {
|
|
|
2322
2407
|
log(logger, "info", `[PerspectAPI] Loading products for site "${siteName}"`);
|
|
2323
2408
|
const queryParams = {
|
|
2324
2409
|
isActive: true,
|
|
2325
|
-
limit,
|
|
2410
|
+
limit: resolvedLimit,
|
|
2326
2411
|
offset,
|
|
2327
2412
|
search
|
|
2328
2413
|
};
|
|
@@ -2382,13 +2467,20 @@ async function loadPages(options) {
|
|
|
2382
2467
|
}
|
|
2383
2468
|
try {
|
|
2384
2469
|
log(logger, "info", `[PerspectAPI] Loading pages for site "${siteName}"`);
|
|
2470
|
+
const requestedLimit = options.limit !== void 0 ? options.limit : params?.limit;
|
|
2471
|
+
const resolvedLimit = validateLimit(
|
|
2472
|
+
requestedLimit ?? MAX_API_QUERY_LIMIT,
|
|
2473
|
+
"pages query"
|
|
2474
|
+
);
|
|
2475
|
+
const requestedPageType = params?.page_type;
|
|
2476
|
+
const validatedPageType = validateOptionalContentType(requestedPageType, "pages query") ?? "page";
|
|
2385
2477
|
const response = await client.content.getContent(
|
|
2386
2478
|
siteName,
|
|
2387
2479
|
{
|
|
2388
2480
|
...params,
|
|
2389
2481
|
page_status: params?.page_status ?? "publish",
|
|
2390
|
-
page_type:
|
|
2391
|
-
limit:
|
|
2482
|
+
page_type: validatedPageType,
|
|
2483
|
+
limit: resolvedLimit
|
|
2392
2484
|
}
|
|
2393
2485
|
);
|
|
2394
2486
|
if (!response.data) {
|
|
@@ -2408,13 +2500,20 @@ async function loadPosts(options) {
|
|
|
2408
2500
|
}
|
|
2409
2501
|
try {
|
|
2410
2502
|
log(logger, "info", `[PerspectAPI] Loading posts for site "${siteName}"`);
|
|
2503
|
+
const requestedLimit = options.limit !== void 0 ? options.limit : params?.limit;
|
|
2504
|
+
const resolvedLimit = validateLimit(
|
|
2505
|
+
requestedLimit ?? MAX_API_QUERY_LIMIT,
|
|
2506
|
+
"posts query"
|
|
2507
|
+
);
|
|
2508
|
+
const requestedPageType = params?.page_type;
|
|
2509
|
+
const validatedPageType = validateOptionalContentType(requestedPageType, "posts query") ?? "post";
|
|
2411
2510
|
const response = await client.content.getContent(
|
|
2412
2511
|
siteName,
|
|
2413
2512
|
{
|
|
2414
2513
|
...params,
|
|
2415
2514
|
page_status: params?.page_status ?? "publish",
|
|
2416
|
-
page_type:
|
|
2417
|
-
limit:
|
|
2515
|
+
page_type: validatedPageType,
|
|
2516
|
+
limit: resolvedLimit
|
|
2418
2517
|
}
|
|
2419
2518
|
);
|
|
2420
2519
|
if (!response.data) {
|
|
@@ -2474,7 +2573,8 @@ async function createCheckoutSession(options) {
|
|
|
2474
2573
|
logger = noopLogger,
|
|
2475
2574
|
fallbackProducts,
|
|
2476
2575
|
metadata,
|
|
2477
|
-
priceIdResolver
|
|
2576
|
+
priceIdResolver,
|
|
2577
|
+
tax
|
|
2478
2578
|
} = options;
|
|
2479
2579
|
if (!client) {
|
|
2480
2580
|
log(logger, "error", "[PerspectAPI] Cannot create checkout session without SDK client");
|
|
@@ -2485,7 +2585,7 @@ async function createCheckoutSession(options) {
|
|
|
2485
2585
|
client,
|
|
2486
2586
|
siteName,
|
|
2487
2587
|
logger,
|
|
2488
|
-
limit:
|
|
2588
|
+
limit: Math.min(Math.max(items.length, 1), MAX_API_QUERY_LIMIT),
|
|
2489
2589
|
fallbackProducts
|
|
2490
2590
|
});
|
|
2491
2591
|
const productMap = new Map(
|
|
@@ -2505,6 +2605,18 @@ async function createCheckoutSession(options) {
|
|
|
2505
2605
|
quantity: item.quantity
|
|
2506
2606
|
};
|
|
2507
2607
|
});
|
|
2608
|
+
const resolvedTax = (() => {
|
|
2609
|
+
if (!tax && !customerEmail) {
|
|
2610
|
+
return void 0;
|
|
2611
|
+
}
|
|
2612
|
+
if (!tax) {
|
|
2613
|
+
return { customer_identifier: customerEmail };
|
|
2614
|
+
}
|
|
2615
|
+
return {
|
|
2616
|
+
...tax,
|
|
2617
|
+
customer_identifier: tax.customer_identifier ?? customerEmail ?? void 0
|
|
2618
|
+
};
|
|
2619
|
+
})();
|
|
2508
2620
|
const checkoutData = {
|
|
2509
2621
|
line_items,
|
|
2510
2622
|
successUrl,
|
|
@@ -2516,6 +2628,9 @@ async function createCheckoutSession(options) {
|
|
|
2516
2628
|
mode: mode === "test" ? "payment" : "payment",
|
|
2517
2629
|
metadata
|
|
2518
2630
|
};
|
|
2631
|
+
if (resolvedTax) {
|
|
2632
|
+
checkoutData.tax = resolvedTax;
|
|
2633
|
+
}
|
|
2519
2634
|
log(logger, "info", "[PerspectAPI] Creating checkout session");
|
|
2520
2635
|
return client.checkout.createCheckoutSession(siteName, checkoutData);
|
|
2521
2636
|
} catch (error) {
|
package/dist/index.mjs
CHANGED
|
@@ -293,6 +293,7 @@ var CacheManager = class {
|
|
|
293
293
|
}
|
|
294
294
|
async getOrSet(key, resolveValue, policy) {
|
|
295
295
|
if (!this.enabled || policy?.skipCache) {
|
|
296
|
+
console.log("[Cache] Cache disabled or skipped", { key, enabled: this.enabled, skipCache: policy?.skipCache });
|
|
296
297
|
const value2 = await resolveValue();
|
|
297
298
|
if (this.enabled && !policy?.skipCache && policy?.ttlSeconds !== 0) {
|
|
298
299
|
await this.set(key, value2, policy);
|
|
@@ -304,12 +305,16 @@ var CacheManager = class {
|
|
|
304
305
|
if (cachedRaw) {
|
|
305
306
|
const entry = this.deserialize(cachedRaw);
|
|
306
307
|
if (!entry.expiresAt || entry.expiresAt > Date.now()) {
|
|
308
|
+
console.log("[Cache] \u2713 HIT", { key, tags: entry.tags });
|
|
307
309
|
return entry.value;
|
|
308
310
|
}
|
|
311
|
+
console.log("[Cache] \u2717 EXPIRED", { key, expiresAt: new Date(entry.expiresAt) });
|
|
309
312
|
await this.adapter.delete(namespacedKey);
|
|
310
313
|
if (entry.tags?.length) {
|
|
311
314
|
await this.removeKeyFromTags(namespacedKey, entry.tags);
|
|
312
315
|
}
|
|
316
|
+
} else {
|
|
317
|
+
console.log("[Cache] \u2717 MISS", { key });
|
|
313
318
|
}
|
|
314
319
|
const value = await resolveValue();
|
|
315
320
|
await this.set(key, value, policy);
|
|
@@ -327,6 +332,7 @@ var CacheManager = class {
|
|
|
327
332
|
tags: options?.tags,
|
|
328
333
|
metadata: options?.metadata
|
|
329
334
|
};
|
|
335
|
+
console.log("[Cache] SET", { key, ttlSeconds, tags: options?.tags });
|
|
330
336
|
await this.adapter.set(namespacedKey, this.serialize(entry), {
|
|
331
337
|
ttlSeconds: ttlSeconds > 0 ? ttlSeconds : void 0
|
|
332
338
|
});
|
|
@@ -345,34 +351,42 @@ var CacheManager = class {
|
|
|
345
351
|
if (!this.enabled) {
|
|
346
352
|
return;
|
|
347
353
|
}
|
|
354
|
+
let totalInvalidated = 0;
|
|
348
355
|
if (options.keys?.length) {
|
|
349
356
|
const namespacedKeys = options.keys.map((key) => this.namespacedKey(key));
|
|
357
|
+
console.log("[Cache] INVALIDATE by keys", { count: options.keys.length, keys: options.keys });
|
|
350
358
|
if (this.adapter.deleteMany) {
|
|
351
359
|
await this.adapter.deleteMany(namespacedKeys);
|
|
352
360
|
} else {
|
|
353
361
|
await Promise.all(namespacedKeys.map((key) => this.adapter.delete(key)));
|
|
354
362
|
}
|
|
363
|
+
totalInvalidated += options.keys.length;
|
|
355
364
|
}
|
|
356
365
|
if (options.tags?.length) {
|
|
366
|
+
console.log("[Cache] INVALIDATE by tags", { tags: options.tags });
|
|
357
367
|
await Promise.all(
|
|
358
368
|
options.tags.map(async (tag) => {
|
|
359
369
|
const tagKey = this.tagKey(tag);
|
|
360
370
|
const payload = await this.adapter.get(tagKey);
|
|
361
371
|
if (!payload) {
|
|
372
|
+
console.log("[Cache] No entries for tag", { tag });
|
|
362
373
|
return;
|
|
363
374
|
}
|
|
364
375
|
const keys = this.deserializeTagSet(payload);
|
|
365
376
|
if (keys.length) {
|
|
377
|
+
console.log("[Cache] Invalidating entries for tag", { tag, count: keys.length });
|
|
366
378
|
if (this.adapter.deleteMany) {
|
|
367
379
|
await this.adapter.deleteMany(keys);
|
|
368
380
|
} else {
|
|
369
381
|
await Promise.all(keys.map((key) => this.adapter.delete(key)));
|
|
370
382
|
}
|
|
383
|
+
totalInvalidated += keys.length;
|
|
371
384
|
}
|
|
372
385
|
await this.adapter.delete(tagKey);
|
|
373
386
|
})
|
|
374
387
|
);
|
|
375
388
|
}
|
|
389
|
+
console.log("[Cache] \u2713 INVALIDATED", { totalEntries: totalInvalidated, keys: options.keys?.length || 0, tags: options.tags?.length || 0 });
|
|
376
390
|
}
|
|
377
391
|
namespacedKey(key) {
|
|
378
392
|
return `${this.keyPrefix}:${key}`;
|
|
@@ -654,6 +668,43 @@ var AuthClient = class extends BaseClient {
|
|
|
654
668
|
}
|
|
655
669
|
};
|
|
656
670
|
|
|
671
|
+
// src/utils/validators.ts
|
|
672
|
+
var MAX_API_QUERY_LIMIT = 100;
|
|
673
|
+
var ALLOWED_CONTENT_TYPES = ["post", "page"];
|
|
674
|
+
function validateLimit(limit, context) {
|
|
675
|
+
if (typeof limit !== "number" || Number.isNaN(limit) || !Number.isFinite(limit)) {
|
|
676
|
+
throw new Error(`[PerspectAPI] ${context} limit must be a finite number.`);
|
|
677
|
+
}
|
|
678
|
+
if (limit < 1) {
|
|
679
|
+
throw new Error(`[PerspectAPI] ${context} limit must be at least 1.`);
|
|
680
|
+
}
|
|
681
|
+
if (limit > MAX_API_QUERY_LIMIT) {
|
|
682
|
+
throw new Error(
|
|
683
|
+
`[PerspectAPI] ${context} limit ${limit} exceeds the maximum allowed value of ${MAX_API_QUERY_LIMIT}.`
|
|
684
|
+
);
|
|
685
|
+
}
|
|
686
|
+
return limit;
|
|
687
|
+
}
|
|
688
|
+
function validateOptionalLimit(limit, context) {
|
|
689
|
+
if (limit === void 0 || limit === null) {
|
|
690
|
+
return void 0;
|
|
691
|
+
}
|
|
692
|
+
return validateLimit(limit, context);
|
|
693
|
+
}
|
|
694
|
+
function validateOptionalContentType(pageType, context) {
|
|
695
|
+
if (pageType === void 0 || pageType === null) {
|
|
696
|
+
return void 0;
|
|
697
|
+
}
|
|
698
|
+
if (ALLOWED_CONTENT_TYPES.includes(pageType)) {
|
|
699
|
+
return pageType;
|
|
700
|
+
}
|
|
701
|
+
throw new Error(
|
|
702
|
+
`[PerspectAPI] ${context} received unsupported page_type "${pageType}". Allowed values are ${ALLOWED_CONTENT_TYPES.join(
|
|
703
|
+
", "
|
|
704
|
+
)}.`
|
|
705
|
+
);
|
|
706
|
+
}
|
|
707
|
+
|
|
657
708
|
// src/client/content-client.ts
|
|
658
709
|
var ContentClient = class extends BaseClient {
|
|
659
710
|
constructor(http, cache) {
|
|
@@ -665,12 +716,33 @@ var ContentClient = class extends BaseClient {
|
|
|
665
716
|
async getContent(siteName, params, cachePolicy) {
|
|
666
717
|
const endpoint = this.siteScopedEndpoint(siteName);
|
|
667
718
|
const path = this.buildPath(endpoint);
|
|
719
|
+
const normalizedParams = params ? { ...params } : void 0;
|
|
720
|
+
if (normalizedParams) {
|
|
721
|
+
const validatedLimit = validateOptionalLimit(
|
|
722
|
+
normalizedParams.limit,
|
|
723
|
+
"content query"
|
|
724
|
+
);
|
|
725
|
+
if (validatedLimit !== void 0) {
|
|
726
|
+
normalizedParams.limit = validatedLimit;
|
|
727
|
+
} else {
|
|
728
|
+
delete normalizedParams.limit;
|
|
729
|
+
}
|
|
730
|
+
const validatedPageType = validateOptionalContentType(
|
|
731
|
+
normalizedParams.page_type,
|
|
732
|
+
"content query"
|
|
733
|
+
);
|
|
734
|
+
if (validatedPageType !== void 0) {
|
|
735
|
+
normalizedParams.page_type = validatedPageType;
|
|
736
|
+
} else {
|
|
737
|
+
delete normalizedParams.page_type;
|
|
738
|
+
}
|
|
739
|
+
}
|
|
668
740
|
return this.fetchWithCache(
|
|
669
741
|
endpoint,
|
|
670
|
-
|
|
742
|
+
normalizedParams,
|
|
671
743
|
this.buildContentTags(siteName),
|
|
672
744
|
cachePolicy,
|
|
673
|
-
() => this.http.get(path,
|
|
745
|
+
() => this.http.get(path, normalizedParams)
|
|
674
746
|
);
|
|
675
747
|
}
|
|
676
748
|
/**
|
|
@@ -1032,6 +1104,15 @@ var ProductsClient = class extends BaseClient {
|
|
|
1032
1104
|
};
|
|
1033
1105
|
const normalizedParams = params ? { ...params } : void 0;
|
|
1034
1106
|
if (normalizedParams) {
|
|
1107
|
+
const validatedLimit = validateOptionalLimit(
|
|
1108
|
+
normalizedParams.limit,
|
|
1109
|
+
"products query"
|
|
1110
|
+
);
|
|
1111
|
+
if (validatedLimit !== void 0) {
|
|
1112
|
+
normalizedParams.limit = validatedLimit;
|
|
1113
|
+
} else {
|
|
1114
|
+
delete normalizedParams.limit;
|
|
1115
|
+
}
|
|
1035
1116
|
const normalizedCategories = normalizeList(normalizedParams.category);
|
|
1036
1117
|
if (normalizedCategories !== void 0) {
|
|
1037
1118
|
normalizedParams.category = normalizedCategories;
|
|
@@ -2247,12 +2328,16 @@ async function loadProducts(options) {
|
|
|
2247
2328
|
siteName,
|
|
2248
2329
|
logger = noopLogger,
|
|
2249
2330
|
fallbackProducts,
|
|
2250
|
-
limit
|
|
2331
|
+
limit: requestedLimit,
|
|
2251
2332
|
offset,
|
|
2252
2333
|
search,
|
|
2253
2334
|
category,
|
|
2254
2335
|
categoryIds
|
|
2255
2336
|
} = options;
|
|
2337
|
+
const resolvedLimit = validateLimit(
|
|
2338
|
+
requestedLimit ?? MAX_API_QUERY_LIMIT,
|
|
2339
|
+
"products query"
|
|
2340
|
+
);
|
|
2256
2341
|
if (!client) {
|
|
2257
2342
|
log(logger, "warn", "[PerspectAPI] No client configured, using fallback products");
|
|
2258
2343
|
return resolveFallbackProducts({ siteName, fallbackProducts });
|
|
@@ -2261,7 +2346,7 @@ async function loadProducts(options) {
|
|
|
2261
2346
|
log(logger, "info", `[PerspectAPI] Loading products for site "${siteName}"`);
|
|
2262
2347
|
const queryParams = {
|
|
2263
2348
|
isActive: true,
|
|
2264
|
-
limit,
|
|
2349
|
+
limit: resolvedLimit,
|
|
2265
2350
|
offset,
|
|
2266
2351
|
search
|
|
2267
2352
|
};
|
|
@@ -2321,13 +2406,20 @@ async function loadPages(options) {
|
|
|
2321
2406
|
}
|
|
2322
2407
|
try {
|
|
2323
2408
|
log(logger, "info", `[PerspectAPI] Loading pages for site "${siteName}"`);
|
|
2409
|
+
const requestedLimit = options.limit !== void 0 ? options.limit : params?.limit;
|
|
2410
|
+
const resolvedLimit = validateLimit(
|
|
2411
|
+
requestedLimit ?? MAX_API_QUERY_LIMIT,
|
|
2412
|
+
"pages query"
|
|
2413
|
+
);
|
|
2414
|
+
const requestedPageType = params?.page_type;
|
|
2415
|
+
const validatedPageType = validateOptionalContentType(requestedPageType, "pages query") ?? "page";
|
|
2324
2416
|
const response = await client.content.getContent(
|
|
2325
2417
|
siteName,
|
|
2326
2418
|
{
|
|
2327
2419
|
...params,
|
|
2328
2420
|
page_status: params?.page_status ?? "publish",
|
|
2329
|
-
page_type:
|
|
2330
|
-
limit:
|
|
2421
|
+
page_type: validatedPageType,
|
|
2422
|
+
limit: resolvedLimit
|
|
2331
2423
|
}
|
|
2332
2424
|
);
|
|
2333
2425
|
if (!response.data) {
|
|
@@ -2347,13 +2439,20 @@ async function loadPosts(options) {
|
|
|
2347
2439
|
}
|
|
2348
2440
|
try {
|
|
2349
2441
|
log(logger, "info", `[PerspectAPI] Loading posts for site "${siteName}"`);
|
|
2442
|
+
const requestedLimit = options.limit !== void 0 ? options.limit : params?.limit;
|
|
2443
|
+
const resolvedLimit = validateLimit(
|
|
2444
|
+
requestedLimit ?? MAX_API_QUERY_LIMIT,
|
|
2445
|
+
"posts query"
|
|
2446
|
+
);
|
|
2447
|
+
const requestedPageType = params?.page_type;
|
|
2448
|
+
const validatedPageType = validateOptionalContentType(requestedPageType, "posts query") ?? "post";
|
|
2350
2449
|
const response = await client.content.getContent(
|
|
2351
2450
|
siteName,
|
|
2352
2451
|
{
|
|
2353
2452
|
...params,
|
|
2354
2453
|
page_status: params?.page_status ?? "publish",
|
|
2355
|
-
page_type:
|
|
2356
|
-
limit:
|
|
2454
|
+
page_type: validatedPageType,
|
|
2455
|
+
limit: resolvedLimit
|
|
2357
2456
|
}
|
|
2358
2457
|
);
|
|
2359
2458
|
if (!response.data) {
|
|
@@ -2413,7 +2512,8 @@ async function createCheckoutSession(options) {
|
|
|
2413
2512
|
logger = noopLogger,
|
|
2414
2513
|
fallbackProducts,
|
|
2415
2514
|
metadata,
|
|
2416
|
-
priceIdResolver
|
|
2515
|
+
priceIdResolver,
|
|
2516
|
+
tax
|
|
2417
2517
|
} = options;
|
|
2418
2518
|
if (!client) {
|
|
2419
2519
|
log(logger, "error", "[PerspectAPI] Cannot create checkout session without SDK client");
|
|
@@ -2424,7 +2524,7 @@ async function createCheckoutSession(options) {
|
|
|
2424
2524
|
client,
|
|
2425
2525
|
siteName,
|
|
2426
2526
|
logger,
|
|
2427
|
-
limit:
|
|
2527
|
+
limit: Math.min(Math.max(items.length, 1), MAX_API_QUERY_LIMIT),
|
|
2428
2528
|
fallbackProducts
|
|
2429
2529
|
});
|
|
2430
2530
|
const productMap = new Map(
|
|
@@ -2444,6 +2544,18 @@ async function createCheckoutSession(options) {
|
|
|
2444
2544
|
quantity: item.quantity
|
|
2445
2545
|
};
|
|
2446
2546
|
});
|
|
2547
|
+
const resolvedTax = (() => {
|
|
2548
|
+
if (!tax && !customerEmail) {
|
|
2549
|
+
return void 0;
|
|
2550
|
+
}
|
|
2551
|
+
if (!tax) {
|
|
2552
|
+
return { customer_identifier: customerEmail };
|
|
2553
|
+
}
|
|
2554
|
+
return {
|
|
2555
|
+
...tax,
|
|
2556
|
+
customer_identifier: tax.customer_identifier ?? customerEmail ?? void 0
|
|
2557
|
+
};
|
|
2558
|
+
})();
|
|
2447
2559
|
const checkoutData = {
|
|
2448
2560
|
line_items,
|
|
2449
2561
|
successUrl,
|
|
@@ -2455,6 +2567,9 @@ async function createCheckoutSession(options) {
|
|
|
2455
2567
|
mode: mode === "test" ? "payment" : "payment",
|
|
2456
2568
|
metadata
|
|
2457
2569
|
};
|
|
2570
|
+
if (resolvedTax) {
|
|
2571
|
+
checkoutData.tax = resolvedTax;
|
|
2572
|
+
}
|
|
2458
2573
|
log(logger, "info", "[PerspectAPI] Creating checkout session");
|
|
2459
2574
|
return client.checkout.createCheckoutSession(siteName, checkoutData);
|
|
2460
2575
|
} catch (error) {
|
package/package.json
CHANGED
|
@@ -75,6 +75,7 @@ export class CacheManager {
|
|
|
75
75
|
policy?: CachePolicy
|
|
76
76
|
): Promise<T> {
|
|
77
77
|
if (!this.enabled || policy?.skipCache) {
|
|
78
|
+
console.log('[Cache] Cache disabled or skipped', { key, enabled: this.enabled, skipCache: policy?.skipCache });
|
|
78
79
|
const value = await resolveValue();
|
|
79
80
|
|
|
80
81
|
if (this.enabled && !policy?.skipCache && policy?.ttlSeconds !== 0) {
|
|
@@ -91,14 +92,18 @@ export class CacheManager {
|
|
|
91
92
|
const entry = this.deserialize<T>(cachedRaw);
|
|
92
93
|
|
|
93
94
|
if (!entry.expiresAt || entry.expiresAt > Date.now()) {
|
|
95
|
+
console.log('[Cache] ✓ HIT', { key, tags: entry.tags });
|
|
94
96
|
return entry.value;
|
|
95
97
|
}
|
|
96
98
|
|
|
97
99
|
// Expired entry
|
|
100
|
+
console.log('[Cache] ✗ EXPIRED', { key, expiresAt: new Date(entry.expiresAt) });
|
|
98
101
|
await this.adapter.delete(namespacedKey);
|
|
99
102
|
if (entry.tags?.length) {
|
|
100
103
|
await this.removeKeyFromTags(namespacedKey, entry.tags);
|
|
101
104
|
}
|
|
105
|
+
} else {
|
|
106
|
+
console.log('[Cache] ✗ MISS', { key });
|
|
102
107
|
}
|
|
103
108
|
|
|
104
109
|
const value = await resolveValue();
|
|
@@ -122,6 +127,8 @@ export class CacheManager {
|
|
|
122
127
|
metadata: options?.metadata,
|
|
123
128
|
};
|
|
124
129
|
|
|
130
|
+
console.log('[Cache] SET', { key, ttlSeconds, tags: options?.tags });
|
|
131
|
+
|
|
125
132
|
await this.adapter.set(namespacedKey, this.serialize(entry), {
|
|
126
133
|
ttlSeconds: ttlSeconds > 0 ? ttlSeconds : undefined,
|
|
127
134
|
});
|
|
@@ -144,37 +151,47 @@ export class CacheManager {
|
|
|
144
151
|
return;
|
|
145
152
|
}
|
|
146
153
|
|
|
154
|
+
let totalInvalidated = 0;
|
|
155
|
+
|
|
147
156
|
if (options.keys?.length) {
|
|
148
157
|
const namespacedKeys = options.keys.map(key => this.namespacedKey(key));
|
|
158
|
+
console.log('[Cache] INVALIDATE by keys', { count: options.keys.length, keys: options.keys });
|
|
149
159
|
if (this.adapter.deleteMany) {
|
|
150
160
|
await this.adapter.deleteMany(namespacedKeys);
|
|
151
161
|
} else {
|
|
152
162
|
await Promise.all(namespacedKeys.map(key => this.adapter.delete(key)));
|
|
153
163
|
}
|
|
164
|
+
totalInvalidated += options.keys.length;
|
|
154
165
|
}
|
|
155
166
|
|
|
156
167
|
if (options.tags?.length) {
|
|
168
|
+
console.log('[Cache] INVALIDATE by tags', { tags: options.tags });
|
|
157
169
|
await Promise.all(
|
|
158
170
|
options.tags.map(async tag => {
|
|
159
171
|
const tagKey = this.tagKey(tag);
|
|
160
172
|
const payload = await this.adapter.get(tagKey);
|
|
161
173
|
if (!payload) {
|
|
174
|
+
console.log('[Cache] No entries for tag', { tag });
|
|
162
175
|
return;
|
|
163
176
|
}
|
|
164
177
|
|
|
165
178
|
const keys = this.deserializeTagSet(payload);
|
|
166
179
|
if (keys.length) {
|
|
180
|
+
console.log('[Cache] Invalidating entries for tag', { tag, count: keys.length });
|
|
167
181
|
if (this.adapter.deleteMany) {
|
|
168
182
|
await this.adapter.deleteMany(keys);
|
|
169
183
|
} else {
|
|
170
184
|
await Promise.all(keys.map(key => this.adapter.delete(key)));
|
|
171
185
|
}
|
|
186
|
+
totalInvalidated += keys.length;
|
|
172
187
|
}
|
|
173
188
|
|
|
174
189
|
await this.adapter.delete(tagKey);
|
|
175
190
|
})
|
|
176
191
|
);
|
|
177
192
|
}
|
|
193
|
+
|
|
194
|
+
console.log('[Cache] ✓ INVALIDATED', { totalEntries: totalInvalidated, keys: options.keys?.length || 0, tags: options.tags?.length || 0 });
|
|
178
195
|
}
|
|
179
196
|
|
|
180
197
|
private namespacedKey(key: string): string {
|
|
@@ -13,6 +13,7 @@ import type {
|
|
|
13
13
|
PaginatedResponse,
|
|
14
14
|
ApiResponse,
|
|
15
15
|
} from '../types';
|
|
16
|
+
import { validateOptionalContentType, validateOptionalLimit } from '../utils/validators';
|
|
16
17
|
|
|
17
18
|
export class ContentClient extends BaseClient {
|
|
18
19
|
constructor(http: any, cache?: CacheManager) {
|
|
@@ -29,13 +30,38 @@ export class ContentClient extends BaseClient {
|
|
|
29
30
|
): Promise<PaginatedResponse<Content>> {
|
|
30
31
|
const endpoint = this.siteScopedEndpoint(siteName);
|
|
31
32
|
const path = this.buildPath(endpoint);
|
|
33
|
+
const normalizedParams: ContentQueryParams | undefined = params
|
|
34
|
+
? { ...params }
|
|
35
|
+
: undefined;
|
|
36
|
+
|
|
37
|
+
if (normalizedParams) {
|
|
38
|
+
const validatedLimit = validateOptionalLimit(
|
|
39
|
+
normalizedParams.limit,
|
|
40
|
+
'content query',
|
|
41
|
+
);
|
|
42
|
+
if (validatedLimit !== undefined) {
|
|
43
|
+
normalizedParams.limit = validatedLimit;
|
|
44
|
+
} else {
|
|
45
|
+
delete normalizedParams.limit;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const validatedPageType = validateOptionalContentType(
|
|
49
|
+
normalizedParams.page_type,
|
|
50
|
+
'content query',
|
|
51
|
+
);
|
|
52
|
+
if (validatedPageType !== undefined) {
|
|
53
|
+
normalizedParams.page_type = validatedPageType;
|
|
54
|
+
} else {
|
|
55
|
+
delete normalizedParams.page_type;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
32
58
|
|
|
33
59
|
return this.fetchWithCache<PaginatedResponse<Content>>(
|
|
34
60
|
endpoint,
|
|
35
|
-
|
|
61
|
+
normalizedParams,
|
|
36
62
|
this.buildContentTags(siteName),
|
|
37
63
|
cachePolicy,
|
|
38
|
-
() => this.http.get(path,
|
|
64
|
+
() => this.http.get(path, normalizedParams) as Promise<PaginatedResponse<Content>>
|
|
39
65
|
);
|
|
40
66
|
}
|
|
41
67
|
|
|
@@ -12,6 +12,7 @@ import type {
|
|
|
12
12
|
ApiResponse,
|
|
13
13
|
ProductQueryParams,
|
|
14
14
|
} from '../types';
|
|
15
|
+
import { validateOptionalLimit } from '../utils/validators';
|
|
15
16
|
|
|
16
17
|
export class ProductsClient extends BaseClient {
|
|
17
18
|
constructor(http: any, cache?: CacheManager) {
|
|
@@ -41,6 +42,16 @@ export class ProductsClient extends BaseClient {
|
|
|
41
42
|
const normalizedParams: ProductQueryParams | undefined = params ? { ...params } : undefined;
|
|
42
43
|
|
|
43
44
|
if (normalizedParams) {
|
|
45
|
+
const validatedLimit = validateOptionalLimit(
|
|
46
|
+
normalizedParams.limit,
|
|
47
|
+
'products query',
|
|
48
|
+
);
|
|
49
|
+
if (validatedLimit !== undefined) {
|
|
50
|
+
normalizedParams.limit = validatedLimit;
|
|
51
|
+
} else {
|
|
52
|
+
delete normalizedParams.limit;
|
|
53
|
+
}
|
|
54
|
+
|
|
44
55
|
const normalizedCategories = normalizeList(normalizedParams.category as any);
|
|
45
56
|
if (normalizedCategories !== undefined) {
|
|
46
57
|
normalizedParams.category = normalizedCategories;
|
package/src/loaders.ts
CHANGED
|
@@ -17,8 +17,14 @@ import type {
|
|
|
17
17
|
BlogPost,
|
|
18
18
|
CreateCheckoutSessionRequest,
|
|
19
19
|
CheckoutSession,
|
|
20
|
-
CheckoutMetadata
|
|
20
|
+
CheckoutMetadata,
|
|
21
|
+
CheckoutTaxRequest
|
|
21
22
|
} from './types';
|
|
23
|
+
import {
|
|
24
|
+
MAX_API_QUERY_LIMIT,
|
|
25
|
+
validateLimit,
|
|
26
|
+
validateOptionalContentType
|
|
27
|
+
} from './utils/validators';
|
|
22
28
|
|
|
23
29
|
/**
|
|
24
30
|
* Logger interface so consumers can supply custom logging behaviour (or noop).
|
|
@@ -311,13 +317,18 @@ export async function loadProducts(options: LoadProductsOptions): Promise<Produc
|
|
|
311
317
|
siteName,
|
|
312
318
|
logger = noopLogger,
|
|
313
319
|
fallbackProducts,
|
|
314
|
-
limit
|
|
320
|
+
limit: requestedLimit,
|
|
315
321
|
offset,
|
|
316
322
|
search,
|
|
317
323
|
category,
|
|
318
324
|
categoryIds
|
|
319
325
|
} = options;
|
|
320
326
|
|
|
327
|
+
const resolvedLimit = validateLimit(
|
|
328
|
+
requestedLimit ?? MAX_API_QUERY_LIMIT,
|
|
329
|
+
'products query',
|
|
330
|
+
);
|
|
331
|
+
|
|
321
332
|
if (!client) {
|
|
322
333
|
log(logger, 'warn', '[PerspectAPI] No client configured, using fallback products');
|
|
323
334
|
return resolveFallbackProducts({ siteName, fallbackProducts });
|
|
@@ -327,7 +338,7 @@ export async function loadProducts(options: LoadProductsOptions): Promise<Produc
|
|
|
327
338
|
log(logger, 'info', `[PerspectAPI] Loading products for site "${siteName}"`);
|
|
328
339
|
const queryParams: ProductQueryParams = {
|
|
329
340
|
isActive: true,
|
|
330
|
-
limit,
|
|
341
|
+
limit: resolvedLimit,
|
|
331
342
|
offset,
|
|
332
343
|
search
|
|
333
344
|
};
|
|
@@ -427,13 +438,23 @@ export async function loadPages(
|
|
|
427
438
|
|
|
428
439
|
try {
|
|
429
440
|
log(logger, 'info', `[PerspectAPI] Loading pages for site "${siteName}"`);
|
|
441
|
+
const requestedLimit =
|
|
442
|
+
options.limit !== undefined ? options.limit : params?.limit;
|
|
443
|
+
const resolvedLimit = validateLimit(
|
|
444
|
+
requestedLimit ?? MAX_API_QUERY_LIMIT,
|
|
445
|
+
'pages query',
|
|
446
|
+
);
|
|
447
|
+
const requestedPageType = params?.page_type;
|
|
448
|
+
const validatedPageType =
|
|
449
|
+
validateOptionalContentType(requestedPageType, 'pages query') ?? 'page';
|
|
450
|
+
|
|
430
451
|
const response: PaginatedResponse<Content> = await client.content.getContent(
|
|
431
452
|
siteName,
|
|
432
453
|
{
|
|
433
454
|
...params,
|
|
434
455
|
page_status: (params?.page_status ?? 'publish') as ContentStatus,
|
|
435
|
-
page_type:
|
|
436
|
-
limit:
|
|
456
|
+
page_type: validatedPageType as ContentType,
|
|
457
|
+
limit: resolvedLimit
|
|
437
458
|
}
|
|
438
459
|
);
|
|
439
460
|
|
|
@@ -463,13 +484,23 @@ export async function loadPosts(
|
|
|
463
484
|
|
|
464
485
|
try {
|
|
465
486
|
log(logger, 'info', `[PerspectAPI] Loading posts for site "${siteName}"`);
|
|
487
|
+
const requestedLimit =
|
|
488
|
+
options.limit !== undefined ? options.limit : params?.limit;
|
|
489
|
+
const resolvedLimit = validateLimit(
|
|
490
|
+
requestedLimit ?? MAX_API_QUERY_LIMIT,
|
|
491
|
+
'posts query',
|
|
492
|
+
);
|
|
493
|
+
const requestedPageType = params?.page_type;
|
|
494
|
+
const validatedPageType =
|
|
495
|
+
validateOptionalContentType(requestedPageType, 'posts query') ?? 'post';
|
|
496
|
+
|
|
466
497
|
const response: PaginatedResponse<Content> = await client.content.getContent(
|
|
467
498
|
siteName,
|
|
468
499
|
{
|
|
469
500
|
...params,
|
|
470
501
|
page_status: (params?.page_status ?? 'publish') as ContentStatus,
|
|
471
|
-
page_type:
|
|
472
|
-
limit:
|
|
502
|
+
page_type: validatedPageType as ContentType,
|
|
503
|
+
limit: resolvedLimit
|
|
473
504
|
}
|
|
474
505
|
);
|
|
475
506
|
|
|
@@ -558,6 +589,10 @@ export interface CheckoutSessionOptions {
|
|
|
558
589
|
* Optional resolver to convert a product record into a Stripe price ID.
|
|
559
590
|
*/
|
|
560
591
|
priceIdResolver?: (product: Product, mode: 'live' | 'test') => string | undefined;
|
|
592
|
+
/**
|
|
593
|
+
* Optional tax configuration that will be forwarded to the checkout API.
|
|
594
|
+
*/
|
|
595
|
+
tax?: CheckoutTaxRequest;
|
|
561
596
|
}
|
|
562
597
|
|
|
563
598
|
/**
|
|
@@ -578,7 +613,8 @@ export async function createCheckoutSession(
|
|
|
578
613
|
logger = noopLogger,
|
|
579
614
|
fallbackProducts,
|
|
580
615
|
metadata,
|
|
581
|
-
priceIdResolver
|
|
616
|
+
priceIdResolver,
|
|
617
|
+
tax
|
|
582
618
|
} = options;
|
|
583
619
|
|
|
584
620
|
if (!client) {
|
|
@@ -592,7 +628,7 @@ export async function createCheckoutSession(
|
|
|
592
628
|
client,
|
|
593
629
|
siteName,
|
|
594
630
|
logger,
|
|
595
|
-
limit:
|
|
631
|
+
limit: Math.min(Math.max(items.length, 1), MAX_API_QUERY_LIMIT),
|
|
596
632
|
fallbackProducts
|
|
597
633
|
});
|
|
598
634
|
|
|
@@ -623,6 +659,21 @@ export async function createCheckoutSession(
|
|
|
623
659
|
};
|
|
624
660
|
});
|
|
625
661
|
|
|
662
|
+
const resolvedTax: CheckoutTaxRequest | undefined = (() => {
|
|
663
|
+
if (!tax && !customerEmail) {
|
|
664
|
+
return undefined;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
if (!tax) {
|
|
668
|
+
return { customer_identifier: customerEmail };
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
return {
|
|
672
|
+
...tax,
|
|
673
|
+
customer_identifier: tax.customer_identifier ?? customerEmail ?? undefined,
|
|
674
|
+
};
|
|
675
|
+
})();
|
|
676
|
+
|
|
626
677
|
const checkoutData: CreateCheckoutSessionRequest = {
|
|
627
678
|
line_items,
|
|
628
679
|
successUrl,
|
|
@@ -632,9 +683,13 @@ export async function createCheckoutSession(
|
|
|
632
683
|
customerEmail,
|
|
633
684
|
customer_email: customerEmail,
|
|
634
685
|
mode: mode === 'test' ? 'payment' : 'payment',
|
|
635
|
-
metadata
|
|
686
|
+
metadata,
|
|
636
687
|
};
|
|
637
688
|
|
|
689
|
+
if (resolvedTax) {
|
|
690
|
+
checkoutData.tax = resolvedTax;
|
|
691
|
+
}
|
|
692
|
+
|
|
638
693
|
log(logger, 'info', '[PerspectAPI] Creating checkout session');
|
|
639
694
|
return client.checkout.createCheckoutSession(siteName, checkoutData);
|
|
640
695
|
} catch (error) {
|
package/src/types/index.ts
CHANGED
|
@@ -399,6 +399,45 @@ export type CheckoutMetadataValue =
|
|
|
399
399
|
|
|
400
400
|
export type CheckoutMetadata = Record<string, CheckoutMetadataValue>;
|
|
401
401
|
|
|
402
|
+
export type CheckoutTaxStrategy =
|
|
403
|
+
| 'disabled'
|
|
404
|
+
| 'gateway_auto'
|
|
405
|
+
| 'manual_rates'
|
|
406
|
+
| 'external_service';
|
|
407
|
+
|
|
408
|
+
export type CheckoutTaxExemptionStatus =
|
|
409
|
+
| 'none'
|
|
410
|
+
| 'exempt'
|
|
411
|
+
| 'reverse_charge';
|
|
412
|
+
|
|
413
|
+
export interface CheckoutTaxCustomerExemptionRequest {
|
|
414
|
+
status?: CheckoutTaxExemptionStatus;
|
|
415
|
+
reason?: string;
|
|
416
|
+
tax_id?: string;
|
|
417
|
+
tax_id_type?: string;
|
|
418
|
+
certificate_url?: string;
|
|
419
|
+
metadata?: Record<string, any>;
|
|
420
|
+
expires_at?: string;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
export interface CheckoutTaxRequest {
|
|
424
|
+
strategy?: CheckoutTaxStrategy;
|
|
425
|
+
customer_identifier?: string;
|
|
426
|
+
customer_profile_id?: string;
|
|
427
|
+
customer_display_name?: string;
|
|
428
|
+
allow_exemption?: boolean;
|
|
429
|
+
require_tax_id?: boolean;
|
|
430
|
+
save_profile?: boolean;
|
|
431
|
+
customer_exemption?: CheckoutTaxCustomerExemptionRequest;
|
|
432
|
+
manual_rate_percent?: number;
|
|
433
|
+
manual_rate_map?: Record<string, number>;
|
|
434
|
+
external_service?: {
|
|
435
|
+
provider: string;
|
|
436
|
+
config?: Record<string, any>;
|
|
437
|
+
};
|
|
438
|
+
metadata?: Record<string, any>;
|
|
439
|
+
}
|
|
440
|
+
|
|
402
441
|
export interface CreateCheckoutSessionRequest {
|
|
403
442
|
// Single item checkout (legacy/simple)
|
|
404
443
|
priceId?: string;
|
|
@@ -437,6 +476,7 @@ export interface CreateCheckoutSessionRequest {
|
|
|
437
476
|
allowed_countries: string[];
|
|
438
477
|
};
|
|
439
478
|
billing_address_collection?: 'auto' | 'required';
|
|
479
|
+
tax?: CheckoutTaxRequest;
|
|
440
480
|
}
|
|
441
481
|
|
|
442
482
|
export interface CheckoutSession {
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { ContentType } from '../types';
|
|
2
|
+
|
|
3
|
+
export const MAX_API_QUERY_LIMIT = 100;
|
|
4
|
+
const ALLOWED_CONTENT_TYPES: ReadonlyArray<ContentType> = ['post', 'page'];
|
|
5
|
+
|
|
6
|
+
export function validateLimit(limit: number, context: string): number {
|
|
7
|
+
if (typeof limit !== 'number' || Number.isNaN(limit) || !Number.isFinite(limit)) {
|
|
8
|
+
throw new Error(`[PerspectAPI] ${context} limit must be a finite number.`);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
if (limit < 1) {
|
|
12
|
+
throw new Error(`[PerspectAPI] ${context} limit must be at least 1.`);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (limit > MAX_API_QUERY_LIMIT) {
|
|
16
|
+
throw new Error(
|
|
17
|
+
`[PerspectAPI] ${context} limit ${limit} exceeds the maximum allowed value of ${MAX_API_QUERY_LIMIT}.`,
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return limit;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function validateOptionalLimit(
|
|
25
|
+
limit: number | undefined,
|
|
26
|
+
context: string,
|
|
27
|
+
): number | undefined {
|
|
28
|
+
if (limit === undefined || limit === null) {
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return validateLimit(limit, context);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function validateOptionalContentType(
|
|
36
|
+
pageType: string | undefined,
|
|
37
|
+
context: string,
|
|
38
|
+
): ContentType | undefined {
|
|
39
|
+
if (pageType === undefined || pageType === null) {
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (ALLOWED_CONTENT_TYPES.includes(pageType as ContentType)) {
|
|
44
|
+
return pageType as ContentType;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
throw new Error(
|
|
48
|
+
`[PerspectAPI] ${context} received unsupported page_type "${pageType}". Allowed values are ${ALLOWED_CONTENT_TYPES.join(
|
|
49
|
+
', ',
|
|
50
|
+
)}.`,
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|