@youversion/platform-core 0.4.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.
Files changed (59) hide show
  1. package/.env.example +7 -0
  2. package/.env.local +10 -0
  3. package/.turbo/turbo-build.log +18 -0
  4. package/CHANGELOG.md +7 -0
  5. package/LICENSE +201 -0
  6. package/README.md +369 -0
  7. package/dist/index.cjs +1330 -0
  8. package/dist/index.d.cts +737 -0
  9. package/dist/index.d.ts +737 -0
  10. package/dist/index.js +1286 -0
  11. package/package.json +46 -0
  12. package/src/AuthenticationStrategy.ts +78 -0
  13. package/src/SignInWithYouVersionResult.ts +53 -0
  14. package/src/StorageStrategy.ts +81 -0
  15. package/src/URLBuilder.ts +71 -0
  16. package/src/Users.ts +137 -0
  17. package/src/WebAuthenticationStrategy.ts +127 -0
  18. package/src/YouVersionAPI.ts +27 -0
  19. package/src/YouVersionPlatformConfiguration.ts +80 -0
  20. package/src/YouVersionUserInfo.ts +49 -0
  21. package/src/__tests__/StorageStrategy.test.ts +404 -0
  22. package/src/__tests__/URLBuilder.test.ts +289 -0
  23. package/src/__tests__/YouVersionPlatformConfiguration.test.ts +150 -0
  24. package/src/__tests__/authentication.test.ts +174 -0
  25. package/src/__tests__/bible.test.ts +356 -0
  26. package/src/__tests__/client.test.ts +109 -0
  27. package/src/__tests__/handlers.ts +41 -0
  28. package/src/__tests__/highlights.test.ts +485 -0
  29. package/src/__tests__/languages.test.ts +139 -0
  30. package/src/__tests__/setup.ts +17 -0
  31. package/src/authentication.ts +27 -0
  32. package/src/bible.ts +272 -0
  33. package/src/client.ts +162 -0
  34. package/src/highlight.ts +16 -0
  35. package/src/highlights.ts +173 -0
  36. package/src/index.ts +20 -0
  37. package/src/languages.ts +80 -0
  38. package/src/schemas/bible-index.ts +48 -0
  39. package/src/schemas/book.ts +34 -0
  40. package/src/schemas/chapter.ts +24 -0
  41. package/src/schemas/collection.ts +28 -0
  42. package/src/schemas/highlight.ts +23 -0
  43. package/src/schemas/index.ts +11 -0
  44. package/src/schemas/language.ts +38 -0
  45. package/src/schemas/passage.ts +14 -0
  46. package/src/schemas/user.ts +10 -0
  47. package/src/schemas/verse.ts +17 -0
  48. package/src/schemas/version.ts +31 -0
  49. package/src/schemas/votd.ts +10 -0
  50. package/src/types/api-config.ts +9 -0
  51. package/src/types/auth.ts +15 -0
  52. package/src/types/book.ts +116 -0
  53. package/src/types/chapter.ts +5 -0
  54. package/src/types/highlight.ts +9 -0
  55. package/src/types/index.ts +22 -0
  56. package/src/utils/constants.ts +219 -0
  57. package/tsconfig.build.json +11 -0
  58. package/tsconfig.json +12 -0
  59. package/vitest.config.ts +9 -0
@@ -0,0 +1,356 @@
1
+ import { describe, it, expect, beforeEach } from 'vitest';
2
+ import { ApiClient } from '../client';
3
+ import { BibleClient } from '../bible';
4
+ import {
5
+ BibleBookSchema,
6
+ BibleChapterSchema,
7
+ BiblePassageSchema,
8
+ BibleVerseSchema,
9
+ BibleVersionSchema,
10
+ VOTDSchema,
11
+ } from '../schemas';
12
+
13
+ describe('BibleClient', () => {
14
+ let apiClient: ApiClient;
15
+ let bibleClient: BibleClient;
16
+
17
+ beforeEach(() => {
18
+ apiClient = new ApiClient({
19
+ baseUrl: 'https://api-dev.youversion.com',
20
+ appId: process.env.YVP_APP_ID || '',
21
+ version: 'v1',
22
+ installationId: 'test-installation',
23
+ });
24
+ bibleClient = new BibleClient(apiClient);
25
+ });
26
+
27
+ describe('getVersions', () => {
28
+ it('should fetch Bible versions with language ranges', async () => {
29
+ const versions = await bibleClient.getVersions('en*');
30
+
31
+ const { success } = BibleVersionSchema.safeParse(versions.data[0]);
32
+ expect(success).toBe(true);
33
+
34
+ const hasNIV = versions.data.some((version) => version.id === 111);
35
+ expect(hasNIV).toBe(true);
36
+ });
37
+
38
+ it('should throw an error for invalid language ranges', async () => {
39
+ await expect(bibleClient.getVersions('')).rejects.toThrow(
40
+ 'Language ranges must be a non-empty string',
41
+ );
42
+ await expect(bibleClient.getVersions(' ')).rejects.toThrow(
43
+ 'Language ranges must be a non-empty string',
44
+ );
45
+ });
46
+ });
47
+
48
+ describe('getVersion', () => {
49
+ it('should fetch a Bible version by ID', async () => {
50
+ const version = await bibleClient.getVersion(1);
51
+
52
+ const { success } = BibleVersionSchema.safeParse(version);
53
+ expect(success).toBe(true);
54
+ expect(version).toHaveProperty('id', 1);
55
+ });
56
+
57
+ it('should throw an error for invalid version ID', async () => {
58
+ await expect(bibleClient.getVersion(0)).rejects.toThrow(
59
+ 'Version ID must be a positive integer',
60
+ );
61
+ await expect(bibleClient.getVersion(-1)).rejects.toThrow(
62
+ 'Version ID must be a positive integer',
63
+ );
64
+ await expect(bibleClient.getVersion(1.5)).rejects.toThrow();
65
+ await expect(bibleClient.getVersion(NaN)).rejects.toThrow();
66
+ });
67
+ });
68
+
69
+ describe('getBooks', () => {
70
+ it('should fetch all books for a version', async () => {
71
+ const books = await bibleClient.getBooks(1);
72
+
73
+ const { success } = BibleBookSchema.safeParse(books.data[0]);
74
+ expect(success).toBe(true);
75
+
76
+ expect(books.data).toHaveLength(66);
77
+ expect(books.data[0]).toHaveProperty('id', 'GEN');
78
+ expect(books.data[0]).toHaveProperty('title', 'Genesis');
79
+ expect(books.data[0]).toHaveProperty('abbreviation', 'Gen');
80
+ expect(books.data[0]).toHaveProperty('canon', 'ot');
81
+ });
82
+ });
83
+
84
+ describe('getBook', () => {
85
+ it('should fetch a specific book', async () => {
86
+ const book = await bibleClient.getBook(1, 'GEN');
87
+
88
+ const { success } = BibleBookSchema.safeParse(book);
89
+ expect(success).toBe(true);
90
+
91
+ expect(book.chapters).toHaveLength(50);
92
+ expect(book).toHaveProperty('id', 'GEN');
93
+ expect(book).toHaveProperty('title', 'Genesis');
94
+ expect(book).toHaveProperty('abbreviation', 'Gen');
95
+ expect(book).toHaveProperty('canon', 'ot');
96
+ });
97
+
98
+ it('should throw an error for invalid inputs', async () => {
99
+ await expect(bibleClient.getBook(0, 'GEN')).rejects.toThrow(
100
+ 'Version ID must be a positive integer',
101
+ );
102
+ await expect(bibleClient.getBook(1, 'AB')).rejects.toThrow(
103
+ 'Book ID must be exactly 3 characters',
104
+ );
105
+ await expect(bibleClient.getBook(1, 'ABCD')).rejects.toThrow(
106
+ 'Book ID must be exactly 3 characters',
107
+ );
108
+ });
109
+ });
110
+
111
+ describe('getChapters', () => {
112
+ it('should fetch all chapters for a book', async () => {
113
+ const chapters = await bibleClient.getChapters(1, 'GEN');
114
+
115
+ const { success } = BibleChapterSchema.safeParse(chapters.data[0]);
116
+ expect(success).toBe(true);
117
+
118
+ expect(chapters.data).toHaveLength(50);
119
+ expect(chapters.data[0]).toHaveProperty('id', '1');
120
+ expect(chapters.data[0]).toHaveProperty('book_id', 'GEN');
121
+ expect(chapters.data[0]).toHaveProperty('passage_id', 'GEN.1');
122
+ expect(chapters.data[0]).toHaveProperty('title', '1');
123
+ expect(chapters.data[0]?.verses).toHaveLength(31);
124
+ });
125
+ });
126
+
127
+ describe('getChapter', () => {
128
+ it('should fetch a specific chapter', async () => {
129
+ const chapter = await bibleClient.getChapter(1, 'GEN', 1);
130
+
131
+ const { success } = BibleChapterSchema.safeParse(chapter);
132
+ expect(success).toBe(true);
133
+
134
+ expect(chapter).toHaveProperty('id', '1');
135
+ expect(chapter).toHaveProperty('book_id', 'GEN');
136
+ expect(chapter).toHaveProperty('passage_id', 'GEN.1');
137
+ expect(chapter).toHaveProperty('title', '1');
138
+ expect(chapter.verses).toHaveLength(31);
139
+ });
140
+
141
+ it('should reject invalid chapter numbers', async () => {
142
+ await expect(bibleClient.getChapter(1, 'GEN', 0)).rejects.toThrow(
143
+ 'Chapter must be a positive integer',
144
+ );
145
+ await expect(bibleClient.getChapter(1, 'GEN', -1)).rejects.toThrow(
146
+ 'Chapter must be a positive integer',
147
+ );
148
+ });
149
+ });
150
+
151
+ describe('getVerses', () => {
152
+ it('should fetch all verses for a chapter', async () => {
153
+ const verses = await bibleClient.getVerses(1, 'GEN', 1);
154
+
155
+ const { success } = BibleVerseSchema.safeParse(verses.data[0]);
156
+ expect(success).toBe(true);
157
+
158
+ expect(verses.data).toHaveLength(24);
159
+ expect(verses.data[0]).toHaveProperty('id', '1');
160
+ expect(verses.data[0]).toHaveProperty('reference', 'Genesis 1:1');
161
+ expect(verses.data[0]).toHaveProperty('book_id', 'GEN');
162
+ expect(verses.data[0]).toHaveProperty('chapter_id', '1');
163
+ expect(verses.data[0]).toHaveProperty('passage_id', 'GEN.1.1');
164
+ });
165
+ });
166
+
167
+ describe('getVerse', () => {
168
+ it('should fetch a specific verse', async () => {
169
+ const verse = await bibleClient.getVerse(1, 'GEN', 1, 1);
170
+
171
+ const { success } = BibleVerseSchema.safeParse(verse);
172
+ expect(success).toBe(true);
173
+
174
+ expect(verse).toHaveProperty('id', '1');
175
+ expect(verse).toHaveProperty('reference', 'Genesis 1:1');
176
+ expect(verse).toHaveProperty('book_id', 'GEN');
177
+ expect(verse).toHaveProperty('chapter_id', '1');
178
+ expect(verse).toHaveProperty('passage_id', 'GEN.1.1');
179
+ });
180
+
181
+ it('should throw an error for invalid inputs', async () => {
182
+ await expect(bibleClient.getVerse(0, 'GEN', 1, 1)).rejects.toThrow(
183
+ 'Version ID must be a positive integer',
184
+ );
185
+ await expect(bibleClient.getVerse(1, 'AB', 1, 1)).rejects.toThrow(
186
+ 'Book ID must be exactly 3 characters',
187
+ );
188
+ await expect(bibleClient.getVerse(1, 'GEN', 0, 1)).rejects.toThrow(
189
+ 'Chapter must be a positive integer',
190
+ );
191
+ await expect(bibleClient.getVerse(1, 'GEN', 1, 0)).rejects.toThrow(
192
+ 'Verse must be a positive integer',
193
+ );
194
+ await expect(bibleClient.getVerse(1, 'GEN', -1, 1)).rejects.toThrow(
195
+ 'Chapter must be a positive integer',
196
+ );
197
+ await expect(bibleClient.getVerse(1, 'GEN', 1, -1)).rejects.toThrow(
198
+ 'Verse must be a positive integer',
199
+ );
200
+ });
201
+ });
202
+
203
+ describe('getPassage', () => {
204
+ it('should fetch a passage for a verse', async () => {
205
+ const passage = await bibleClient.getPassage(111, 'GEN.1.1');
206
+
207
+ const { success } = BiblePassageSchema.safeParse(passage);
208
+ expect(success).toBe(true);
209
+
210
+ expect(passage).toEqual({
211
+ id: 'GEN.1.1',
212
+ content:
213
+ '<div><div class="pi"><span class="yv-v" v="1"></span><span class="yv-vlbl">1</span>In the beginning God created the heavens and the earth. </div></div>',
214
+ bible_id: 111,
215
+ human_reference: 'Genesis 1:1',
216
+ });
217
+ });
218
+
219
+ it('should fetch a passage for a chapter', async () => {
220
+ const passage = await bibleClient.getPassage(111, 'GEN.1');
221
+
222
+ const { success } = BiblePassageSchema.safeParse(passage);
223
+ expect(success).toBe(true);
224
+
225
+ expect(passage).toHaveProperty('id', 'GEN.1');
226
+ expect(passage).toHaveProperty('bible_id', 111);
227
+ expect(passage).toHaveProperty('human_reference', 'Genesis 1');
228
+ });
229
+
230
+ it('should fetch a passage with html format by default', async () => {
231
+ const passage = await bibleClient.getPassage(111, 'GEN.1.1');
232
+
233
+ expect(passage.content).toContain('<div>');
234
+ });
235
+
236
+ it('should fetch a passage with text format', async () => {
237
+ const passage = await bibleClient.getPassage(111, 'GEN.1.1', 'text');
238
+
239
+ expect(passage.content).not.toContain('<div>');
240
+ });
241
+
242
+ it('should fetch a passage with include_headings', async () => {
243
+ const passage = await bibleClient.getPassage(111, 'ROM.1', 'html', true);
244
+
245
+ expect(passage.id).toBe('ROM.1');
246
+ expect(passage.bible_id).toBe(111);
247
+ expect(passage.content).toContain('yv-h');
248
+ expect(passage.content).not.toContain('yv-n');
249
+ });
250
+
251
+ it('should fetch a passage with include_notes', async () => {
252
+ const passage = await bibleClient.getPassage(111, 'ROM.1', 'html', undefined, true);
253
+
254
+ expect(passage.id).toBe('ROM.1');
255
+ expect(passage.bible_id).toBe(111);
256
+ expect(passage.content).toContain('yv-n');
257
+ expect(passage.content).not.toContain('yv-h');
258
+ });
259
+
260
+ it('should fetch a passage with both include_headings and include_notes', async () => {
261
+ const passage = await bibleClient.getPassage(111, 'ROM.1', 'html', true, true);
262
+
263
+ expect(passage.id).toBe('ROM.1');
264
+ expect(passage.bible_id).toBe(111);
265
+ expect(passage.content).toContain('yv-n');
266
+ expect(passage.content).toContain('yv-h');
267
+ });
268
+
269
+ it('should throw an error for invalid version ID', async () => {
270
+ await expect(bibleClient.getPassage(0, 'GEN.1.1')).rejects.toThrow(
271
+ 'Version ID must be a positive integer',
272
+ );
273
+ await expect(bibleClient.getPassage(-1, 'GEN.1.1')).rejects.toThrow(
274
+ 'Version ID must be a positive integer',
275
+ );
276
+ });
277
+
278
+ it('should throw an error for invalid include_headings', async () => {
279
+ // @ts-expect-error - we want to test the error case
280
+ await expect(bibleClient.getPassage(1, 'GEN.1.1', 'html', 'true')).rejects.toThrow();
281
+ });
282
+
283
+ it('should throw an error for invalid include_notes', async () => {
284
+ await expect(
285
+ // @ts-expect-error - we want to test the error case
286
+ bibleClient.getPassage(1, 'GEN.1.1', 'html', undefined, 'true'),
287
+ ).rejects.toThrow();
288
+ });
289
+ });
290
+
291
+ describe('getVOTD', () => {
292
+ it('should fetch VOTD for day 1', async () => {
293
+ const votd = await bibleClient.getVOTD(1);
294
+
295
+ const { success } = VOTDSchema.safeParse(votd);
296
+ expect(success).toBe(true);
297
+
298
+ expect(votd).toEqual({
299
+ day: 1,
300
+ passage_id: 'ISA.43.19',
301
+ });
302
+ });
303
+
304
+ it('should fetch VOTD for day 100', async () => {
305
+ const votd = await bibleClient.getVOTD(100);
306
+
307
+ const { success } = VOTDSchema.safeParse(votd);
308
+ expect(success).toBe(true);
309
+
310
+ expect(votd.day).toBe(100);
311
+ expect(votd.passage_id).toBeDefined();
312
+ });
313
+
314
+ it('should fetch VOTD for day 366', async () => {
315
+ const votd = await bibleClient.getVOTD(366);
316
+
317
+ const { success } = VOTDSchema.safeParse(votd);
318
+ expect(success).toBe(true);
319
+
320
+ expect(votd.day).toBe(366);
321
+ expect(votd.passage_id).toBeDefined();
322
+ });
323
+
324
+ it('should throw an error for day less than 1', async () => {
325
+ await expect(bibleClient.getVOTD(0)).rejects.toThrow();
326
+ await expect(bibleClient.getVOTD(-1)).rejects.toThrow();
327
+ });
328
+
329
+ it('should throw an error for day greater than 366', async () => {
330
+ await expect(bibleClient.getVOTD(367)).rejects.toThrow();
331
+ });
332
+
333
+ it('should throw an error for non-integer day', async () => {
334
+ await expect(bibleClient.getVOTD(1.5)).rejects.toThrow();
335
+ });
336
+
337
+ it('should throw an error for NaN', async () => {
338
+ await expect(bibleClient.getVOTD(NaN)).rejects.toThrow();
339
+ });
340
+ });
341
+
342
+ describe('getAllVOTDs', () => {
343
+ it('should fetch all VOTDs', async () => {
344
+ const votds = await bibleClient.getAllVOTDs();
345
+
346
+ const { success } = VOTDSchema.safeParse(votds.data[0]);
347
+ expect(success).toBe(true);
348
+
349
+ expect(votds.data).toHaveLength(366);
350
+ expect(votds.data[0]).toEqual({
351
+ day: 1,
352
+ passage_id: 'ISA.43.19',
353
+ });
354
+ });
355
+ });
356
+ });
@@ -0,0 +1,109 @@
1
+ import { describe, it, expect, beforeEach } from 'vitest';
2
+ import { ApiClient } from '../client';
3
+ import { http, HttpResponse } from 'msw';
4
+ import { server } from './setup';
5
+
6
+ describe('ApiClient', () => {
7
+ let apiClient: ApiClient;
8
+
9
+ beforeEach(() => {
10
+ apiClient = new ApiClient({
11
+ baseUrl: 'https://api-dev.youversion.com',
12
+ appId: 'test-app',
13
+ version: 'v1',
14
+ installationId: 'test-installation',
15
+ });
16
+ });
17
+
18
+ describe('constructor', () => {
19
+ it('should set default version to v1', () => {
20
+ const client = new ApiClient({
21
+ baseUrl: 'https://api-dev.youversion.com',
22
+ appId: 'test-app',
23
+ installationId: 'test-installation',
24
+ });
25
+
26
+ expect(client.config.version).toBe('v1');
27
+ });
28
+
29
+ it('should use provided version', () => {
30
+ const client = new ApiClient({
31
+ baseUrl: 'https://api-dev.youversion.com',
32
+ appId: 'test-app',
33
+ version: 'v2',
34
+ installationId: 'test-installation',
35
+ });
36
+
37
+ expect(client.config.version).toBe('v2');
38
+ });
39
+ });
40
+
41
+ describe('get', () => {
42
+ it('should make GET request and return data', async () => {
43
+ server.use(
44
+ http.get('https://api-dev.youversion.com/test', () => {
45
+ return HttpResponse.json({ message: 'success' });
46
+ }),
47
+ );
48
+
49
+ const result = await apiClient.get<{ message: string }>('/test');
50
+
51
+ expect(result).toEqual({ message: 'success' });
52
+ });
53
+
54
+ it('should include query parameters', async () => {
55
+ server.use(
56
+ http.get('https://api-dev.youversion.com/test', ({ request }) => {
57
+ const url = new URL(request.url);
58
+ const param = url.searchParams.get('param');
59
+ return HttpResponse.json({ param });
60
+ }),
61
+ );
62
+
63
+ const result = await apiClient.get<{ param: string }>('/test', {
64
+ param: 'value',
65
+ });
66
+
67
+ expect(result).toEqual({ param: 'value' });
68
+ });
69
+ });
70
+
71
+ describe('post', () => {
72
+ it('should make POST request and return data', async () => {
73
+ server.use(
74
+ http.post('https://api-dev.youversion.com/test', async ({ request }) => {
75
+ const body = await request.json();
76
+ return HttpResponse.json({ received: body });
77
+ }),
78
+ );
79
+
80
+ const result = await apiClient.post<{ received: unknown }>('/test', {
81
+ data: 'test',
82
+ });
83
+
84
+ expect(result).toEqual({ received: { data: 'test' } });
85
+ });
86
+
87
+ it('should include query parameters in POST request', async () => {
88
+ server.use(
89
+ http.post('https://api-dev.youversion.com/test', async ({ request }) => {
90
+ const url = new URL(request.url);
91
+ const param = url.searchParams.get('param');
92
+ const body = await request.json();
93
+ return HttpResponse.json({ param, body });
94
+ }),
95
+ );
96
+
97
+ const result = await apiClient.post<{ param: string; body: unknown }>(
98
+ '/test',
99
+ { data: 'test' },
100
+ { param: 'value' },
101
+ );
102
+
103
+ expect(result).toEqual({
104
+ param: 'value',
105
+ body: { data: 'test' },
106
+ });
107
+ });
108
+ });
109
+ });
@@ -0,0 +1,41 @@
1
+ import { http, HttpResponse } from 'msw';
2
+ import type { Collection, Highlight } from '../types';
3
+
4
+ const baseUrl = 'https://api-dev.youversion.com';
5
+
6
+ export const handlers = [
7
+ // Highlights endpoints
8
+ http.get(`${baseUrl}/v1/highlights`, ({ request }) => {
9
+ const url = new URL(request.url);
10
+ const bibleId = url.searchParams.get('version_id');
11
+ const passageId = url.searchParams.get('passage_id');
12
+
13
+ const highlights: Collection<Highlight> = {
14
+ data: [
15
+ {
16
+ version_id: bibleId ? Number(bibleId) : 111,
17
+ passage_id: passageId || 'MAT.1.1',
18
+ color: 'fffe00',
19
+ },
20
+ {
21
+ version_id: bibleId ? Number(bibleId) : 111,
22
+ passage_id: passageId || 'MAT.1.2',
23
+ color: '5dff79',
24
+ },
25
+ ],
26
+ next_page_token: null,
27
+ };
28
+
29
+ return HttpResponse.json(highlights);
30
+ }),
31
+
32
+ http.post(`${baseUrl}/v1/highlights`, async ({ request }) => {
33
+ const body = (await request.json()) as Highlight;
34
+
35
+ return HttpResponse.json(body, { status: 201 });
36
+ }),
37
+
38
+ http.delete(`${baseUrl}/v1/highlights/:passageId`, () => {
39
+ return new HttpResponse(null, { status: 204 });
40
+ }),
41
+ ];