@syra.fm/sdk 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -4
- package/dist/cjs/client.js +77 -4
- package/dist/cjs/index.js +3 -1
- package/dist/cjs/schema.js +20 -1
- package/dist/esm/client.js +77 -4
- package/dist/esm/index.js +2 -2
- package/dist/esm/schema.js +19 -0
- package/dist/types/client.d.ts +69 -6
- package/dist/types/index.d.ts +4 -4
- package/dist/types/schema.d.ts +281 -0
- package/package.json +1 -1
- package/src/client.test.ts +260 -9
- package/src/client.ts +178 -8
- package/src/index.ts +6 -0
- package/src/schema.ts +21 -0
package/dist/types/schema.d.ts
CHANGED
|
@@ -484,3 +484,284 @@ export declare const trackSummarySchema: z.ZodObject<{
|
|
|
484
484
|
previewAvailable?: boolean | undefined;
|
|
485
485
|
}>;
|
|
486
486
|
export type TrackSummary = z.infer<typeof trackSummarySchema>;
|
|
487
|
+
/**
|
|
488
|
+
* The summary view of a podcast SHOW returned by the public podcast endpoints
|
|
489
|
+
* (`GET /api/podcasts/search`, `GET /api/podcasts/:id`) — just enough to render
|
|
490
|
+
* a show card and deep-link into the Syra app.
|
|
491
|
+
*
|
|
492
|
+
* Artwork mirrors tracks: `image` is the re-hosted Syra image id (resolved via
|
|
493
|
+
* `/api/images/:id`); `imageSizes` is the multi-resolution variant set (each
|
|
494
|
+
* variant `url` is `/api/images/:id`); `imageSourceUrl` keeps the original
|
|
495
|
+
* external artwork URL as an absolute fallback when re-hosting has not run yet.
|
|
496
|
+
*/
|
|
497
|
+
export declare const podcastSummarySchema: z.ZodObject<{
|
|
498
|
+
id: z.ZodString;
|
|
499
|
+
title: z.ZodString;
|
|
500
|
+
author: z.ZodOptional<z.ZodString>;
|
|
501
|
+
description: z.ZodOptional<z.ZodString>;
|
|
502
|
+
image: z.ZodOptional<z.ZodString>;
|
|
503
|
+
imageSizes: z.ZodOptional<z.ZodObject<{
|
|
504
|
+
small: z.ZodOptional<z.ZodObject<{
|
|
505
|
+
id: z.ZodOptional<z.ZodString>;
|
|
506
|
+
url: z.ZodString;
|
|
507
|
+
width: z.ZodOptional<z.ZodNumber>;
|
|
508
|
+
height: z.ZodOptional<z.ZodNumber>;
|
|
509
|
+
}, "strip", z.ZodTypeAny, {
|
|
510
|
+
url: string;
|
|
511
|
+
id?: string | undefined;
|
|
512
|
+
width?: number | undefined;
|
|
513
|
+
height?: number | undefined;
|
|
514
|
+
}, {
|
|
515
|
+
url: string;
|
|
516
|
+
id?: string | undefined;
|
|
517
|
+
width?: number | undefined;
|
|
518
|
+
height?: number | undefined;
|
|
519
|
+
}>>;
|
|
520
|
+
medium: z.ZodOptional<z.ZodObject<{
|
|
521
|
+
id: z.ZodOptional<z.ZodString>;
|
|
522
|
+
url: z.ZodString;
|
|
523
|
+
width: z.ZodOptional<z.ZodNumber>;
|
|
524
|
+
height: z.ZodOptional<z.ZodNumber>;
|
|
525
|
+
}, "strip", z.ZodTypeAny, {
|
|
526
|
+
url: string;
|
|
527
|
+
id?: string | undefined;
|
|
528
|
+
width?: number | undefined;
|
|
529
|
+
height?: number | undefined;
|
|
530
|
+
}, {
|
|
531
|
+
url: string;
|
|
532
|
+
id?: string | undefined;
|
|
533
|
+
width?: number | undefined;
|
|
534
|
+
height?: number | undefined;
|
|
535
|
+
}>>;
|
|
536
|
+
large: z.ZodOptional<z.ZodObject<{
|
|
537
|
+
id: z.ZodOptional<z.ZodString>;
|
|
538
|
+
url: z.ZodString;
|
|
539
|
+
width: z.ZodOptional<z.ZodNumber>;
|
|
540
|
+
height: z.ZodOptional<z.ZodNumber>;
|
|
541
|
+
}, "strip", z.ZodTypeAny, {
|
|
542
|
+
url: string;
|
|
543
|
+
id?: string | undefined;
|
|
544
|
+
width?: number | undefined;
|
|
545
|
+
height?: number | undefined;
|
|
546
|
+
}, {
|
|
547
|
+
url: string;
|
|
548
|
+
id?: string | undefined;
|
|
549
|
+
width?: number | undefined;
|
|
550
|
+
height?: number | undefined;
|
|
551
|
+
}>>;
|
|
552
|
+
xlarge: z.ZodOptional<z.ZodObject<{
|
|
553
|
+
id: z.ZodOptional<z.ZodString>;
|
|
554
|
+
url: z.ZodString;
|
|
555
|
+
width: z.ZodOptional<z.ZodNumber>;
|
|
556
|
+
height: z.ZodOptional<z.ZodNumber>;
|
|
557
|
+
}, "strip", z.ZodTypeAny, {
|
|
558
|
+
url: string;
|
|
559
|
+
id?: string | undefined;
|
|
560
|
+
width?: number | undefined;
|
|
561
|
+
height?: number | undefined;
|
|
562
|
+
}, {
|
|
563
|
+
url: string;
|
|
564
|
+
id?: string | undefined;
|
|
565
|
+
width?: number | undefined;
|
|
566
|
+
height?: number | undefined;
|
|
567
|
+
}>>;
|
|
568
|
+
xxlarge: z.ZodOptional<z.ZodObject<{
|
|
569
|
+
id: z.ZodOptional<z.ZodString>;
|
|
570
|
+
url: z.ZodString;
|
|
571
|
+
width: z.ZodOptional<z.ZodNumber>;
|
|
572
|
+
height: z.ZodOptional<z.ZodNumber>;
|
|
573
|
+
}, "strip", z.ZodTypeAny, {
|
|
574
|
+
url: string;
|
|
575
|
+
id?: string | undefined;
|
|
576
|
+
width?: number | undefined;
|
|
577
|
+
height?: number | undefined;
|
|
578
|
+
}, {
|
|
579
|
+
url: string;
|
|
580
|
+
id?: string | undefined;
|
|
581
|
+
width?: number | undefined;
|
|
582
|
+
height?: number | undefined;
|
|
583
|
+
}>>;
|
|
584
|
+
original: z.ZodOptional<z.ZodObject<{
|
|
585
|
+
id: z.ZodOptional<z.ZodString>;
|
|
586
|
+
url: z.ZodString;
|
|
587
|
+
width: z.ZodOptional<z.ZodNumber>;
|
|
588
|
+
height: z.ZodOptional<z.ZodNumber>;
|
|
589
|
+
}, "strip", z.ZodTypeAny, {
|
|
590
|
+
url: string;
|
|
591
|
+
id?: string | undefined;
|
|
592
|
+
width?: number | undefined;
|
|
593
|
+
height?: number | undefined;
|
|
594
|
+
}, {
|
|
595
|
+
url: string;
|
|
596
|
+
id?: string | undefined;
|
|
597
|
+
width?: number | undefined;
|
|
598
|
+
height?: number | undefined;
|
|
599
|
+
}>>;
|
|
600
|
+
}, "strip", z.ZodTypeAny, {
|
|
601
|
+
small?: {
|
|
602
|
+
url: string;
|
|
603
|
+
id?: string | undefined;
|
|
604
|
+
width?: number | undefined;
|
|
605
|
+
height?: number | undefined;
|
|
606
|
+
} | undefined;
|
|
607
|
+
medium?: {
|
|
608
|
+
url: string;
|
|
609
|
+
id?: string | undefined;
|
|
610
|
+
width?: number | undefined;
|
|
611
|
+
height?: number | undefined;
|
|
612
|
+
} | undefined;
|
|
613
|
+
large?: {
|
|
614
|
+
url: string;
|
|
615
|
+
id?: string | undefined;
|
|
616
|
+
width?: number | undefined;
|
|
617
|
+
height?: number | undefined;
|
|
618
|
+
} | undefined;
|
|
619
|
+
xlarge?: {
|
|
620
|
+
url: string;
|
|
621
|
+
id?: string | undefined;
|
|
622
|
+
width?: number | undefined;
|
|
623
|
+
height?: number | undefined;
|
|
624
|
+
} | undefined;
|
|
625
|
+
xxlarge?: {
|
|
626
|
+
url: string;
|
|
627
|
+
id?: string | undefined;
|
|
628
|
+
width?: number | undefined;
|
|
629
|
+
height?: number | undefined;
|
|
630
|
+
} | undefined;
|
|
631
|
+
original?: {
|
|
632
|
+
url: string;
|
|
633
|
+
id?: string | undefined;
|
|
634
|
+
width?: number | undefined;
|
|
635
|
+
height?: number | undefined;
|
|
636
|
+
} | undefined;
|
|
637
|
+
}, {
|
|
638
|
+
small?: {
|
|
639
|
+
url: string;
|
|
640
|
+
id?: string | undefined;
|
|
641
|
+
width?: number | undefined;
|
|
642
|
+
height?: number | undefined;
|
|
643
|
+
} | undefined;
|
|
644
|
+
medium?: {
|
|
645
|
+
url: string;
|
|
646
|
+
id?: string | undefined;
|
|
647
|
+
width?: number | undefined;
|
|
648
|
+
height?: number | undefined;
|
|
649
|
+
} | undefined;
|
|
650
|
+
large?: {
|
|
651
|
+
url: string;
|
|
652
|
+
id?: string | undefined;
|
|
653
|
+
width?: number | undefined;
|
|
654
|
+
height?: number | undefined;
|
|
655
|
+
} | undefined;
|
|
656
|
+
xlarge?: {
|
|
657
|
+
url: string;
|
|
658
|
+
id?: string | undefined;
|
|
659
|
+
width?: number | undefined;
|
|
660
|
+
height?: number | undefined;
|
|
661
|
+
} | undefined;
|
|
662
|
+
xxlarge?: {
|
|
663
|
+
url: string;
|
|
664
|
+
id?: string | undefined;
|
|
665
|
+
width?: number | undefined;
|
|
666
|
+
height?: number | undefined;
|
|
667
|
+
} | undefined;
|
|
668
|
+
original?: {
|
|
669
|
+
url: string;
|
|
670
|
+
id?: string | undefined;
|
|
671
|
+
width?: number | undefined;
|
|
672
|
+
height?: number | undefined;
|
|
673
|
+
} | undefined;
|
|
674
|
+
}>>;
|
|
675
|
+
imageSourceUrl: z.ZodOptional<z.ZodString>;
|
|
676
|
+
}, "strip", z.ZodTypeAny, {
|
|
677
|
+
id: string;
|
|
678
|
+
title: string;
|
|
679
|
+
author?: string | undefined;
|
|
680
|
+
description?: string | undefined;
|
|
681
|
+
image?: string | undefined;
|
|
682
|
+
imageSizes?: {
|
|
683
|
+
small?: {
|
|
684
|
+
url: string;
|
|
685
|
+
id?: string | undefined;
|
|
686
|
+
width?: number | undefined;
|
|
687
|
+
height?: number | undefined;
|
|
688
|
+
} | undefined;
|
|
689
|
+
medium?: {
|
|
690
|
+
url: string;
|
|
691
|
+
id?: string | undefined;
|
|
692
|
+
width?: number | undefined;
|
|
693
|
+
height?: number | undefined;
|
|
694
|
+
} | undefined;
|
|
695
|
+
large?: {
|
|
696
|
+
url: string;
|
|
697
|
+
id?: string | undefined;
|
|
698
|
+
width?: number | undefined;
|
|
699
|
+
height?: number | undefined;
|
|
700
|
+
} | undefined;
|
|
701
|
+
xlarge?: {
|
|
702
|
+
url: string;
|
|
703
|
+
id?: string | undefined;
|
|
704
|
+
width?: number | undefined;
|
|
705
|
+
height?: number | undefined;
|
|
706
|
+
} | undefined;
|
|
707
|
+
xxlarge?: {
|
|
708
|
+
url: string;
|
|
709
|
+
id?: string | undefined;
|
|
710
|
+
width?: number | undefined;
|
|
711
|
+
height?: number | undefined;
|
|
712
|
+
} | undefined;
|
|
713
|
+
original?: {
|
|
714
|
+
url: string;
|
|
715
|
+
id?: string | undefined;
|
|
716
|
+
width?: number | undefined;
|
|
717
|
+
height?: number | undefined;
|
|
718
|
+
} | undefined;
|
|
719
|
+
} | undefined;
|
|
720
|
+
imageSourceUrl?: string | undefined;
|
|
721
|
+
}, {
|
|
722
|
+
id: string;
|
|
723
|
+
title: string;
|
|
724
|
+
author?: string | undefined;
|
|
725
|
+
description?: string | undefined;
|
|
726
|
+
image?: string | undefined;
|
|
727
|
+
imageSizes?: {
|
|
728
|
+
small?: {
|
|
729
|
+
url: string;
|
|
730
|
+
id?: string | undefined;
|
|
731
|
+
width?: number | undefined;
|
|
732
|
+
height?: number | undefined;
|
|
733
|
+
} | undefined;
|
|
734
|
+
medium?: {
|
|
735
|
+
url: string;
|
|
736
|
+
id?: string | undefined;
|
|
737
|
+
width?: number | undefined;
|
|
738
|
+
height?: number | undefined;
|
|
739
|
+
} | undefined;
|
|
740
|
+
large?: {
|
|
741
|
+
url: string;
|
|
742
|
+
id?: string | undefined;
|
|
743
|
+
width?: number | undefined;
|
|
744
|
+
height?: number | undefined;
|
|
745
|
+
} | undefined;
|
|
746
|
+
xlarge?: {
|
|
747
|
+
url: string;
|
|
748
|
+
id?: string | undefined;
|
|
749
|
+
width?: number | undefined;
|
|
750
|
+
height?: number | undefined;
|
|
751
|
+
} | undefined;
|
|
752
|
+
xxlarge?: {
|
|
753
|
+
url: string;
|
|
754
|
+
id?: string | undefined;
|
|
755
|
+
width?: number | undefined;
|
|
756
|
+
height?: number | undefined;
|
|
757
|
+
} | undefined;
|
|
758
|
+
original?: {
|
|
759
|
+
url: string;
|
|
760
|
+
id?: string | undefined;
|
|
761
|
+
width?: number | undefined;
|
|
762
|
+
height?: number | undefined;
|
|
763
|
+
} | undefined;
|
|
764
|
+
} | undefined;
|
|
765
|
+
imageSourceUrl?: string | undefined;
|
|
766
|
+
}>;
|
|
767
|
+
export type PodcastSummary = z.infer<typeof podcastSummarySchema>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@syra.fm/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Headless, isomorphic client for the Syra public API — catalog reads and 30s preview clips. No React/RN/DOM dependencies; public reads only.",
|
|
5
5
|
"main": "dist/cjs/index.js",
|
|
6
6
|
"module": "dist/esm/index.js",
|
package/src/client.test.ts
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { describe, it, expect } from 'bun:test';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
createSyraClient,
|
|
4
|
+
SyraApiError,
|
|
5
|
+
DEFAULT_SYRA_BASE_URL,
|
|
6
|
+
DEFAULT_SYRA_WEB_BASE_URL,
|
|
7
|
+
} from './index';
|
|
3
8
|
|
|
4
9
|
// ── Fixtures ──────────────────────────────────────────────────────────────────
|
|
5
10
|
|
|
@@ -21,6 +26,25 @@ function makeTrack(overrides: Record<string, unknown> = {}): Record<string, unkn
|
|
|
21
26
|
};
|
|
22
27
|
}
|
|
23
28
|
|
|
29
|
+
function makePodcast(overrides: Record<string, unknown> = {}): Record<string, unknown> {
|
|
30
|
+
return {
|
|
31
|
+
id: '507f1f77bcf86cd799439021',
|
|
32
|
+
title: 'Test Show',
|
|
33
|
+
author: 'Test Publisher',
|
|
34
|
+
description: 'A show about testing.',
|
|
35
|
+
image: '507f1f77bcf86cd799439022',
|
|
36
|
+
explicit: false,
|
|
37
|
+
type: 'episodic',
|
|
38
|
+
source: 'rss',
|
|
39
|
+
refreshIntervalMin: 60,
|
|
40
|
+
episodeCount: 12,
|
|
41
|
+
status: 'active',
|
|
42
|
+
createdAt: '2026-01-01T00:00:00.000Z',
|
|
43
|
+
updatedAt: '2026-01-01T00:00:00.000Z',
|
|
44
|
+
...overrides,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
24
48
|
interface FetchCall {
|
|
25
49
|
url: string;
|
|
26
50
|
}
|
|
@@ -46,7 +70,7 @@ function fakeFetch(
|
|
|
46
70
|
// ── searchTracks ──────────────────────────────────────────────────────────────
|
|
47
71
|
|
|
48
72
|
describe('createSyraClient.searchTracks', () => {
|
|
49
|
-
it('calls /api/search with category=tracks and the limit, returns preview-available tracks', async () => {
|
|
73
|
+
it('calls /api/search with category=tracks and the limit, returns a page of preview-available tracks', async () => {
|
|
50
74
|
const { fetch, calls } = fakeFetch(() => ({
|
|
51
75
|
body: {
|
|
52
76
|
results: {
|
|
@@ -55,14 +79,20 @@ describe('createSyraClient.searchTracks', () => {
|
|
|
55
79
|
makeTrack({ id: '507f1f77bcf86cd799439012', previewAvailable: false }),
|
|
56
80
|
],
|
|
57
81
|
},
|
|
82
|
+
hasMore: false,
|
|
83
|
+
limit: 10,
|
|
84
|
+
offset: 0,
|
|
58
85
|
},
|
|
59
86
|
}));
|
|
60
87
|
|
|
61
88
|
const client = createSyraClient({ baseURL: 'https://api.example.test', fetch });
|
|
62
|
-
const
|
|
89
|
+
const page = await client.searchTracks('hello', { limit: 10 });
|
|
63
90
|
|
|
64
|
-
expect(
|
|
65
|
-
expect(
|
|
91
|
+
expect(page.items).toHaveLength(1);
|
|
92
|
+
expect(page.items[0].id).toBe('507f1f77bcf86cd799439011');
|
|
93
|
+
expect(page.hasMore).toBe(false);
|
|
94
|
+
expect(page.limit).toBe(10);
|
|
95
|
+
expect(page.offset).toBe(0);
|
|
66
96
|
|
|
67
97
|
expect(calls).toHaveLength(1);
|
|
68
98
|
const url = new URL(calls[0].url);
|
|
@@ -70,6 +100,53 @@ describe('createSyraClient.searchTracks', () => {
|
|
|
70
100
|
expect(url.searchParams.get('q')).toBe('hello');
|
|
71
101
|
expect(url.searchParams.get('category')).toBe('tracks');
|
|
72
102
|
expect(url.searchParams.get('limit')).toBe('10');
|
|
103
|
+
expect(url.searchParams.has('offset')).toBe(false);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('sends the offset param and reports hasMore/offset/limit from the backend', async () => {
|
|
107
|
+
const { fetch, calls } = fakeFetch(() => ({
|
|
108
|
+
body: {
|
|
109
|
+
results: { tracks: [makeTrack()] },
|
|
110
|
+
hasMore: true,
|
|
111
|
+
limit: 20,
|
|
112
|
+
offset: 40,
|
|
113
|
+
},
|
|
114
|
+
}));
|
|
115
|
+
|
|
116
|
+
const client = createSyraClient({ baseURL: 'https://api.example.test', fetch });
|
|
117
|
+
const page = await client.searchTracks('hello', { limit: 20, offset: 40 });
|
|
118
|
+
|
|
119
|
+
expect(page.items).toHaveLength(1);
|
|
120
|
+
expect(page.hasMore).toBe(true);
|
|
121
|
+
expect(page.limit).toBe(20);
|
|
122
|
+
expect(page.offset).toBe(40);
|
|
123
|
+
|
|
124
|
+
const url = new URL(calls[0].url);
|
|
125
|
+
expect(url.searchParams.get('offset')).toBe('40');
|
|
126
|
+
expect(url.searchParams.get('limit')).toBe('20');
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('reports backend hasMore even when client-side preview filtering empties the page', async () => {
|
|
130
|
+
const { fetch } = fakeFetch(() => ({
|
|
131
|
+
body: {
|
|
132
|
+
results: {
|
|
133
|
+
tracks: [
|
|
134
|
+
makeTrack({ previewAvailable: false }),
|
|
135
|
+
makeTrack({ previewAvailable: false }),
|
|
136
|
+
],
|
|
137
|
+
},
|
|
138
|
+
hasMore: true,
|
|
139
|
+
limit: 2,
|
|
140
|
+
offset: 0,
|
|
141
|
+
},
|
|
142
|
+
}));
|
|
143
|
+
|
|
144
|
+
const client = createSyraClient({ fetch });
|
|
145
|
+
const page = await client.searchTracks('x', { limit: 2 });
|
|
146
|
+
|
|
147
|
+
// All rows were filtered out, but the page is NOT the last one.
|
|
148
|
+
expect(page.items).toHaveLength(0);
|
|
149
|
+
expect(page.hasMore).toBe(true);
|
|
73
150
|
});
|
|
74
151
|
|
|
75
152
|
it('drops malformed rows without throwing', async () => {
|
|
@@ -85,14 +162,17 @@ describe('createSyraClient.searchTracks', () => {
|
|
|
85
162
|
}));
|
|
86
163
|
|
|
87
164
|
const client = createSyraClient({ fetch });
|
|
88
|
-
const
|
|
89
|
-
expect(
|
|
165
|
+
const page = await client.searchTracks('x');
|
|
166
|
+
expect(page.items).toHaveLength(1);
|
|
90
167
|
});
|
|
91
168
|
|
|
92
|
-
it('returns an empty
|
|
169
|
+
it('returns an empty page with hasMore=false when results.tracks is absent', async () => {
|
|
93
170
|
const { fetch } = fakeFetch(() => ({ body: {} }));
|
|
94
171
|
const client = createSyraClient({ fetch });
|
|
95
|
-
|
|
172
|
+
const page = await client.searchTracks('x');
|
|
173
|
+
expect(page.items).toEqual([]);
|
|
174
|
+
expect(page.hasMore).toBe(false);
|
|
175
|
+
expect(page.offset).toBe(0);
|
|
96
176
|
});
|
|
97
177
|
});
|
|
98
178
|
|
|
@@ -194,3 +274,174 @@ describe('createSyraClient.artworkUrl', () => {
|
|
|
194
274
|
expect(client.artworkUrl('not-an-id')).toBeUndefined();
|
|
195
275
|
});
|
|
196
276
|
});
|
|
277
|
+
|
|
278
|
+
// ── searchPodcasts ──────────────────────────────────────────────────────────────
|
|
279
|
+
|
|
280
|
+
describe('createSyraClient.searchPodcasts', () => {
|
|
281
|
+
it('calls /api/podcasts/search with q and limit, returns a page of parsed shows', async () => {
|
|
282
|
+
const { fetch, calls } = fakeFetch(() => ({
|
|
283
|
+
body: {
|
|
284
|
+
data: [
|
|
285
|
+
makePodcast({ id: '507f1f77bcf86cd799439021' }),
|
|
286
|
+
makePodcast({ id: '507f1f77bcf86cd799439023', title: 'Second Show' }),
|
|
287
|
+
],
|
|
288
|
+
hasMore: false,
|
|
289
|
+
limit: 5,
|
|
290
|
+
offset: 0,
|
|
291
|
+
},
|
|
292
|
+
}));
|
|
293
|
+
|
|
294
|
+
const client = createSyraClient({ baseURL: 'https://api.example.test', fetch });
|
|
295
|
+
const page = await client.searchPodcasts('news', { limit: 5 });
|
|
296
|
+
|
|
297
|
+
expect(page.items).toHaveLength(2);
|
|
298
|
+
expect(page.items[0].id).toBe('507f1f77bcf86cd799439021');
|
|
299
|
+
expect(page.items[0].author).toBe('Test Publisher');
|
|
300
|
+
expect(page.hasMore).toBe(false);
|
|
301
|
+
expect(page.limit).toBe(5);
|
|
302
|
+
expect(page.offset).toBe(0);
|
|
303
|
+
|
|
304
|
+
expect(calls).toHaveLength(1);
|
|
305
|
+
const url = new URL(calls[0].url);
|
|
306
|
+
expect(url.pathname).toBe('/api/podcasts/search');
|
|
307
|
+
expect(url.searchParams.get('q')).toBe('news');
|
|
308
|
+
expect(url.searchParams.get('limit')).toBe('5');
|
|
309
|
+
expect(url.searchParams.has('offset')).toBe(false);
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
it('sends the offset param and reports hasMore/offset from the backend', async () => {
|
|
313
|
+
const { fetch, calls } = fakeFetch(() => ({
|
|
314
|
+
body: { data: [makePodcast()], hasMore: true, limit: 10, offset: 20 },
|
|
315
|
+
}));
|
|
316
|
+
|
|
317
|
+
const client = createSyraClient({ baseURL: 'https://api.example.test', fetch });
|
|
318
|
+
const page = await client.searchPodcasts('news', { limit: 10, offset: 20 });
|
|
319
|
+
|
|
320
|
+
expect(page.hasMore).toBe(true);
|
|
321
|
+
expect(page.limit).toBe(10);
|
|
322
|
+
expect(page.offset).toBe(20);
|
|
323
|
+
|
|
324
|
+
const url = new URL(calls[0].url);
|
|
325
|
+
expect(url.searchParams.get('offset')).toBe('20');
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
it('omits the limit and offset params when not provided', async () => {
|
|
329
|
+
const { fetch, calls } = fakeFetch(() => ({ body: { data: [makePodcast()] } }));
|
|
330
|
+
const client = createSyraClient({ fetch });
|
|
331
|
+
await client.searchPodcasts('news');
|
|
332
|
+
const url = new URL(calls[0].url);
|
|
333
|
+
expect(url.searchParams.has('limit')).toBe(false);
|
|
334
|
+
expect(url.searchParams.has('offset')).toBe(false);
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
it('drops malformed rows without throwing', async () => {
|
|
338
|
+
const { fetch } = fakeFetch(() => ({
|
|
339
|
+
body: { data: [{ id: 'broken' }, makePodcast()] },
|
|
340
|
+
}));
|
|
341
|
+
const client = createSyraClient({ fetch });
|
|
342
|
+
const page = await client.searchPodcasts('x');
|
|
343
|
+
expect(page.items).toHaveLength(1);
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
it('returns an empty page with hasMore=false when data is absent', async () => {
|
|
347
|
+
const { fetch } = fakeFetch(() => ({ body: {} }));
|
|
348
|
+
const client = createSyraClient({ fetch });
|
|
349
|
+
const page = await client.searchPodcasts('x');
|
|
350
|
+
expect(page.items).toEqual([]);
|
|
351
|
+
expect(page.hasMore).toBe(false);
|
|
352
|
+
expect(page.offset).toBe(0);
|
|
353
|
+
});
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
// ── getPodcast ──────────────────────────────────────────────────────────────────
|
|
357
|
+
|
|
358
|
+
describe('createSyraClient.getPodcast', () => {
|
|
359
|
+
it('fetches /api/podcasts/:id and validates data.podcast', async () => {
|
|
360
|
+
const { fetch, calls } = fakeFetch(() => ({
|
|
361
|
+
body: { data: { podcast: makePodcast(), episodes: [], persons: [] } },
|
|
362
|
+
}));
|
|
363
|
+
const client = createSyraClient({ baseURL: 'https://api.example.test', fetch });
|
|
364
|
+
|
|
365
|
+
const podcast = await client.getPodcast('507f1f77bcf86cd799439021');
|
|
366
|
+
expect(podcast.title).toBe('Test Show');
|
|
367
|
+
expect(calls[0].url).toBe('https://api.example.test/api/podcasts/507f1f77bcf86cd799439021');
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
it('throws SyraApiError on a non-2xx response', async () => {
|
|
371
|
+
const { fetch } = fakeFetch(() => ({ status: 404, body: { error: 'not found' } }));
|
|
372
|
+
const client = createSyraClient({ fetch });
|
|
373
|
+
await expect(client.getPodcast('507f1f77bcf86cd799439021')).rejects.toBeInstanceOf(SyraApiError);
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
it('throws when data.podcast fails schema validation', async () => {
|
|
377
|
+
const { fetch } = fakeFetch(() => ({ body: { data: { podcast: { id: 'x' } } } }));
|
|
378
|
+
const client = createSyraClient({ fetch });
|
|
379
|
+
await expect(client.getPodcast('x')).rejects.toThrow();
|
|
380
|
+
});
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
// ── podcastUrl ────────────────────────────────────────────────────────────────
|
|
384
|
+
|
|
385
|
+
describe('createSyraClient.podcastUrl', () => {
|
|
386
|
+
it('builds the web deep link from the web base URL, not the API host', () => {
|
|
387
|
+
const client = createSyraClient({
|
|
388
|
+
baseURL: 'https://api.example.test',
|
|
389
|
+
webBaseURL: 'https://web.example.test',
|
|
390
|
+
});
|
|
391
|
+
expect(client.podcastUrl('507f1f77bcf86cd799439021')).toBe(
|
|
392
|
+
'https://web.example.test/podcasts/507f1f77bcf86cd799439021',
|
|
393
|
+
);
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
it('defaults to the production web base URL', () => {
|
|
397
|
+
const client = createSyraClient();
|
|
398
|
+
expect(client.podcastUrl('abc')).toBe(`${DEFAULT_SYRA_WEB_BASE_URL}/podcasts/abc`);
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
it('does not use the API base URL for the deep link', () => {
|
|
402
|
+
const client = createSyraClient({ baseURL: 'https://api.example.test' });
|
|
403
|
+
expect(client.podcastUrl('abc')).toBe(`${DEFAULT_SYRA_WEB_BASE_URL}/podcasts/abc`);
|
|
404
|
+
expect(DEFAULT_SYRA_WEB_BASE_URL).not.toBe(DEFAULT_SYRA_BASE_URL);
|
|
405
|
+
});
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
// ── podcastArtworkUrl ────────────────────────────────────────────────────────────
|
|
409
|
+
|
|
410
|
+
describe('createSyraClient.podcastArtworkUrl', () => {
|
|
411
|
+
const client = createSyraClient({ baseURL: 'https://api.example.test' });
|
|
412
|
+
|
|
413
|
+
it('resolves the re-hosted image id to an absolute images URL', () => {
|
|
414
|
+
expect(client.podcastArtworkUrl({ image: '507f1f77bcf86cd799439022' })).toBe(
|
|
415
|
+
'https://api.example.test/api/images/507f1f77bcf86cd799439022',
|
|
416
|
+
);
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
it('prefers a named size from imageSizes', () => {
|
|
420
|
+
const url = client.podcastArtworkUrl(
|
|
421
|
+
{
|
|
422
|
+
image: '507f1f77bcf86cd799439022',
|
|
423
|
+
imageSizes: {
|
|
424
|
+
large: {
|
|
425
|
+
id: '507f1f77bcf86cd799439023',
|
|
426
|
+
url: '/api/images/507f1f77bcf86cd799439023',
|
|
427
|
+
width: 640,
|
|
428
|
+
height: 640,
|
|
429
|
+
},
|
|
430
|
+
},
|
|
431
|
+
},
|
|
432
|
+
'large',
|
|
433
|
+
);
|
|
434
|
+
expect(url).toBe('https://api.example.test/api/images/507f1f77bcf86cd799439023');
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
it('falls back to imageSourceUrl when no Syra image is present', () => {
|
|
438
|
+
expect(
|
|
439
|
+
client.podcastArtworkUrl({ imageSourceUrl: 'https://cdn.example.com/cover.jpg' }),
|
|
440
|
+
).toBe('https://cdn.example.com/cover.jpg');
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
it('returns undefined when nothing resolvable is present', () => {
|
|
444
|
+
expect(client.podcastArtworkUrl({})).toBeUndefined();
|
|
445
|
+
expect(client.podcastArtworkUrl({ image: 'not-an-id' })).toBeUndefined();
|
|
446
|
+
});
|
|
447
|
+
});
|