@youversion/platform-core 1.2.1 → 1.4.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.
@@ -1,5 +1,5 @@
1
1
 
2
- > @youversion/platform-core@1.2.1 build /home/runner/work/platform-sdk-react/platform-sdk-react/packages/core
2
+ > @youversion/platform-core@1.4.0 build /home/runner/work/platform-sdk-react/platform-sdk-react/packages/core
3
3
  > tsup src/index.ts --format cjs,esm --dts
4
4
 
5
5
  CLI Building entry: src/index.ts
@@ -8,11 +8,11 @@
8
8
  CLI Target: es2022
9
9
  CJS Build start
10
10
  ESM Build start
11
- ESM dist/index.js 41.09 KB
12
- ESM ⚡️ Build success in 34ms
13
- CJS dist/index.cjs 42.90 KB
14
- CJS ⚡️ Build success in 34ms
11
+ ESM dist/index.js 41.47 KB
12
+ ESM ⚡️ Build success in 36ms
13
+ CJS dist/index.cjs 43.29 KB
14
+ CJS ⚡️ Build success in 36ms
15
15
  DTS Build start
16
- DTS ⚡️ Build success in 2020ms
17
- DTS dist/index.d.cts 32.78 KB
18
- DTS dist/index.d.ts 32.78 KB
16
+ DTS ⚡️ Build success in 1771ms
17
+ DTS dist/index.d.cts 32.84 KB
18
+ DTS dist/index.d.ts 32.84 KB
package/CHANGELOG.md CHANGED
@@ -1,5 +1,26 @@
1
1
  # @youversion/platform-core
2
2
 
3
+ ## 1.4.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 8275a27: feat(ui): add bible reader settings
8
+ - refactor popover component to have consistent styling across
9
+ multiple components and reduce duplication in code.
10
+ - add bible reader settings and save the users settings to localStorage.
11
+
12
+ ## 1.3.0
13
+
14
+ ### Minor Changes
15
+
16
+ - b2b86c2: Add support for array query parameters in API client and improve language range handling
17
+ - **API Client**: Enhanced query string serialization to support array parameters, properly formatting them as repeated keys (e.g., `?param=one&param=two`)
18
+ - **Bible Client**: Updated `getVersions()` method to accept either a single language range string or an array of language ranges, providing more flexibility for filtering Bible versions
19
+ - **Schema**: Renamed language range schema to use plural naming convention for consistency
20
+ - **Testing**: Added comprehensive test coverage for query string building with both scalar and array parameters
21
+
22
+ This change maintains backward compatibility while providing more flexible API parameter handling.
23
+
3
24
  ## 1.2.1
4
25
 
5
26
  ### Patch Changes
package/dist/index.cjs CHANGED
@@ -69,9 +69,15 @@ var ApiClient = class {
69
69
  */
70
70
  buildQueryString(params) {
71
71
  if (!params) return "";
72
- const queryString = new URLSearchParams(
73
- Object.entries(params).map(([key, value]) => [key, String(value)])
74
- ).toString();
72
+ const searchParams = new URLSearchParams();
73
+ Object.entries(params).forEach(([key, value]) => {
74
+ if (Array.isArray(value)) {
75
+ value.forEach((item) => searchParams.append(key, String(item)));
76
+ } else {
77
+ searchParams.append(key, String(value));
78
+ }
79
+ });
80
+ const queryString = searchParams.toString();
75
81
  return queryString ? `?${queryString}` : "";
76
82
  }
77
83
  /**
@@ -205,14 +211,15 @@ var BibleClient = class {
205
211
  /**
206
212
  * Fetches a collection of Bible versions filtered by language ranges.
207
213
  *
208
- * @param language_ranges - A comma-separated list of language codes or ranges to filter the versions (required).
214
+ * @param language_ranges - One or more language codes or ranges to filter the versions (required).
209
215
  * @param license_id - Optional license ID to filter versions by license.
210
216
  * @returns A promise that resolves to a collection of BibleVersion objects.
211
217
  */
212
218
  async getVersions(language_ranges, license_id) {
213
- this.languageRangesSchema.parse(language_ranges);
219
+ const languageRangeArray = Array.isArray(language_ranges) ? language_ranges : [language_ranges];
220
+ const parsedLanguageRanges = import_zod.z.array(this.languageRangesSchema).nonempty("At least one language range is required").parse(languageRangeArray);
214
221
  const params = {
215
- "language_ranges[]": language_ranges
222
+ "language_ranges[]": parsedLanguageRanges
216
223
  };
217
224
  if (license_id !== void 0) {
218
225
  params.license_id = license_id;
package/dist/index.d.cts CHANGED
@@ -258,7 +258,8 @@ interface HighlightColor {
258
258
  label: string;
259
259
  }
260
260
 
261
- type QueryParams = Record<string, string | number | boolean>;
261
+ type PrimitiveQueryParam = string | number | boolean;
262
+ type QueryParams = Record<string, PrimitiveQueryParam | PrimitiveQueryParam[]>;
262
263
  type RequestData = Record<string, string | number | boolean | object>;
263
264
  type RequestHeaders = Record<string, string>;
264
265
  /**
@@ -333,11 +334,11 @@ declare class BibleClient {
333
334
  /**
334
335
  * Fetches a collection of Bible versions filtered by language ranges.
335
336
  *
336
- * @param language_ranges - A comma-separated list of language codes or ranges to filter the versions (required).
337
+ * @param language_ranges - One or more language codes or ranges to filter the versions (required).
337
338
  * @param license_id - Optional license ID to filter versions by license.
338
339
  * @returns A promise that resolves to a collection of BibleVersion objects.
339
340
  */
340
- getVersions(language_ranges: string, license_id?: string | number): Promise<Collection<BibleVersion>>;
341
+ getVersions(language_ranges: string | string[], license_id?: string | number): Promise<Collection<BibleVersion>>;
341
342
  /**
342
343
  * Fetches a Bible version by its ID.
343
344
  * @param id The version ID.
package/dist/index.d.ts CHANGED
@@ -258,7 +258,8 @@ interface HighlightColor {
258
258
  label: string;
259
259
  }
260
260
 
261
- type QueryParams = Record<string, string | number | boolean>;
261
+ type PrimitiveQueryParam = string | number | boolean;
262
+ type QueryParams = Record<string, PrimitiveQueryParam | PrimitiveQueryParam[]>;
262
263
  type RequestData = Record<string, string | number | boolean | object>;
263
264
  type RequestHeaders = Record<string, string>;
264
265
  /**
@@ -333,11 +334,11 @@ declare class BibleClient {
333
334
  /**
334
335
  * Fetches a collection of Bible versions filtered by language ranges.
335
336
  *
336
- * @param language_ranges - A comma-separated list of language codes or ranges to filter the versions (required).
337
+ * @param language_ranges - One or more language codes or ranges to filter the versions (required).
337
338
  * @param license_id - Optional license ID to filter versions by license.
338
339
  * @returns A promise that resolves to a collection of BibleVersion objects.
339
340
  */
340
- getVersions(language_ranges: string, license_id?: string | number): Promise<Collection<BibleVersion>>;
341
+ getVersions(language_ranges: string | string[], license_id?: string | number): Promise<Collection<BibleVersion>>;
341
342
  /**
342
343
  * Fetches a Bible version by its ID.
343
344
  * @param id The version ID.
package/dist/index.js CHANGED
@@ -30,9 +30,15 @@ var ApiClient = class {
30
30
  */
31
31
  buildQueryString(params) {
32
32
  if (!params) return "";
33
- const queryString = new URLSearchParams(
34
- Object.entries(params).map(([key, value]) => [key, String(value)])
35
- ).toString();
33
+ const searchParams = new URLSearchParams();
34
+ Object.entries(params).forEach(([key, value]) => {
35
+ if (Array.isArray(value)) {
36
+ value.forEach((item) => searchParams.append(key, String(item)));
37
+ } else {
38
+ searchParams.append(key, String(value));
39
+ }
40
+ });
41
+ const queryString = searchParams.toString();
36
42
  return queryString ? `?${queryString}` : "";
37
43
  }
38
44
  /**
@@ -166,14 +172,15 @@ var BibleClient = class {
166
172
  /**
167
173
  * Fetches a collection of Bible versions filtered by language ranges.
168
174
  *
169
- * @param language_ranges - A comma-separated list of language codes or ranges to filter the versions (required).
175
+ * @param language_ranges - One or more language codes or ranges to filter the versions (required).
170
176
  * @param license_id - Optional license ID to filter versions by license.
171
177
  * @returns A promise that resolves to a collection of BibleVersion objects.
172
178
  */
173
179
  async getVersions(language_ranges, license_id) {
174
- this.languageRangesSchema.parse(language_ranges);
180
+ const languageRangeArray = Array.isArray(language_ranges) ? language_ranges : [language_ranges];
181
+ const parsedLanguageRanges = z.array(this.languageRangesSchema).nonempty("At least one language range is required").parse(languageRangeArray);
175
182
  const params = {
176
- "language_ranges[]": language_ranges
183
+ "language_ranges[]": parsedLanguageRanges
177
184
  };
178
185
  if (license_id !== void 0) {
179
186
  params.license_id = license_id;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@youversion/platform-core",
3
- "version": "1.2.1",
3
+ "version": "1.4.0",
4
4
  "type": "module",
5
5
  "publishConfig": {
6
6
  "access": "public",
@@ -1,4 +1,5 @@
1
1
  import { describe, it, expect, beforeEach } from 'vitest';
2
+ import { http, HttpResponse } from 'msw';
2
3
  import { ApiClient } from '../client';
3
4
  import { BibleClient } from '../bible';
4
5
  import {
@@ -9,6 +10,8 @@ import {
9
10
  BibleVersionSchema,
10
11
  VOTDSchema,
11
12
  } from '../schemas';
13
+ import { server } from './setup';
14
+ import { mockVersions } from './MockVersions';
12
15
 
13
16
  describe('BibleClient', () => {
14
17
  let apiClient: ApiClient;
@@ -34,6 +37,50 @@ describe('BibleClient', () => {
34
37
  expect(hasNIV).toBe(true);
35
38
  });
36
39
 
40
+ it('should send multiple language ranges when provided', async () => {
41
+ server.use(
42
+ http.get('https://api.youversion.com/v1/bibles', ({ request }) => {
43
+ const url = new URL(request.url);
44
+ const languageRanges = url.searchParams.getAll('language_ranges[]');
45
+
46
+ expect(languageRanges).toEqual(['en*', 'es*']);
47
+
48
+ return HttpResponse.json({
49
+ data: mockVersions,
50
+ next_page_token: null,
51
+ total_size: mockVersions.length,
52
+ });
53
+ }),
54
+ );
55
+
56
+ const versions = await bibleClient.getVersions(['en*', 'es*']);
57
+
58
+ const { success } = BibleVersionSchema.safeParse(versions.data[0]);
59
+ expect(success).toBe(true);
60
+ });
61
+
62
+ it('should accept a wildcard language range', async () => {
63
+ server.use(
64
+ http.get('https://api.youversion.com/v1/bibles', ({ request }) => {
65
+ const url = new URL(request.url);
66
+ const languageRanges = url.searchParams.getAll('language_ranges[]');
67
+
68
+ expect(languageRanges).toEqual(['*']);
69
+
70
+ return HttpResponse.json({
71
+ data: mockVersions,
72
+ next_page_token: null,
73
+ total_size: mockVersions.length,
74
+ });
75
+ }),
76
+ );
77
+
78
+ const versions = await bibleClient.getVersions('*');
79
+
80
+ const { success } = BibleVersionSchema.safeParse(versions.data[0]);
81
+ expect(success).toBe(true);
82
+ });
83
+
37
84
  it('should throw an error for invalid language ranges', async () => {
38
85
  await expect(bibleClient.getVersions('')).rejects.toThrow(
39
86
  'Language ranges must be a non-empty string',
@@ -41,6 +88,9 @@ describe('BibleClient', () => {
41
88
  await expect(bibleClient.getVersions(' ')).rejects.toThrow(
42
89
  'Language ranges must be a non-empty string',
43
90
  );
91
+ await expect(bibleClient.getVersions([])).rejects.toThrow(
92
+ 'At least one language range is required',
93
+ );
44
94
  });
45
95
  });
46
96
 
@@ -43,6 +43,45 @@ describe('ApiClient', () => {
43
43
  });
44
44
  });
45
45
 
46
+ describe('buildQueryString', () => {
47
+ const buildQueryString = (params?: Parameters<ApiClient['get']>[1]) =>
48
+ (
49
+ apiClient as unknown as {
50
+ buildQueryString: (params?: Parameters<ApiClient['get']>[1]) => string;
51
+ }
52
+ ).buildQueryString(params);
53
+
54
+ it('should serialize single scalar parameter', () => {
55
+ const query = buildQueryString({ param: 'value' });
56
+
57
+ expect(query).toBe('?param=value');
58
+ });
59
+
60
+ it('should serialize an array of length 1 as repeated key', () => {
61
+ const query = buildQueryString({ param: ['only'] });
62
+
63
+ expect(query).toBe('?param=only');
64
+ });
65
+
66
+ it('should serialize an array of length 2 as repeated keys', () => {
67
+ const query = buildQueryString({ param: ['one', 'two'] });
68
+
69
+ expect(query).toBe('?param=one&param=two');
70
+ });
71
+
72
+ it('should serialize an array of length 3 as repeated keys', () => {
73
+ const query = buildQueryString({ param: ['one', 'two', 'three'] });
74
+
75
+ expect(query).toBe('?param=one&param=two&param=three');
76
+ });
77
+
78
+ it('should handle both scalar and array parameters together', () => {
79
+ const query = buildQueryString({ param: 'value', list: ['one', 'two'] });
80
+
81
+ expect(query).toBe('?param=value&list=one&list=two');
82
+ });
83
+ });
84
+
46
85
  describe('get', () => {
47
86
  it('should make GET request and return data', async () => {
48
87
  server.use(
@@ -71,6 +110,22 @@ describe('ApiClient', () => {
71
110
 
72
111
  expect(result).toEqual({ param: 'value' });
73
112
  });
113
+
114
+ it('should include array query parameters as repeated keys', async () => {
115
+ server.use(
116
+ http.get('https://test_placeholder.youversion.com/test', ({ request }) => {
117
+ const url = new URL(request.url);
118
+ const params = url.searchParams.getAll('param');
119
+ return HttpResponse.json({ params });
120
+ }),
121
+ );
122
+
123
+ const result = await apiClient.get<{ params: string[] }>('/test', {
124
+ param: ['one', 'two'],
125
+ });
126
+
127
+ expect(result).toEqual({ params: ['one', 'two'] });
128
+ });
74
129
  });
75
130
 
76
131
  describe('post', () => {
@@ -0,0 +1,29 @@
1
+ class LocalStorageMock {
2
+ private store: Record<string, string> = {};
3
+
4
+ getItem(key: string): string | null {
5
+ return this.store[key] ?? null;
6
+ }
7
+
8
+ setItem(key: string, value: string): void {
9
+ this.store[key] = value;
10
+ }
11
+
12
+ removeItem(key: string): void {
13
+ delete this.store[key];
14
+ }
15
+
16
+ clear(): void {
17
+ this.store = {};
18
+ }
19
+
20
+ get length(): number {
21
+ return Object.keys(this.store).length;
22
+ }
23
+
24
+ key(index: number): string | null {
25
+ return Object.keys(this.store)[index] ?? null;
26
+ }
27
+ }
28
+
29
+ globalThis.localStorage = new LocalStorageMock() as Storage;
package/src/bible.ts CHANGED
@@ -50,17 +50,23 @@ export class BibleClient {
50
50
  /**
51
51
  * Fetches a collection of Bible versions filtered by language ranges.
52
52
  *
53
- * @param language_ranges - A comma-separated list of language codes or ranges to filter the versions (required).
53
+ * @param language_ranges - One or more language codes or ranges to filter the versions (required).
54
54
  * @param license_id - Optional license ID to filter versions by license.
55
55
  * @returns A promise that resolves to a collection of BibleVersion objects.
56
56
  */
57
57
  async getVersions(
58
- language_ranges: string,
58
+ language_ranges: string | string[],
59
59
  license_id?: string | number,
60
60
  ): Promise<Collection<BibleVersion>> {
61
- this.languageRangesSchema.parse(language_ranges);
62
- const params: Record<string, string | number> = {
63
- 'language_ranges[]': language_ranges,
61
+ const languageRangeArray = Array.isArray(language_ranges) ? language_ranges : [language_ranges];
62
+
63
+ const parsedLanguageRanges = z
64
+ .array(this.languageRangesSchema)
65
+ .nonempty('At least one language range is required')
66
+ .parse(languageRangeArray);
67
+
68
+ const params: Record<string, string | number | string[]> = {
69
+ 'language_ranges[]': parsedLanguageRanges,
64
70
  };
65
71
  if (license_id !== undefined) {
66
72
  params.license_id = license_id;
package/src/client.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import type { ApiConfig } from './types';
2
2
 
3
- type QueryParams = Record<string, string | number | boolean>;
3
+ type PrimitiveQueryParam = string | number | boolean;
4
+ type QueryParams = Record<string, PrimitiveQueryParam | PrimitiveQueryParam[]>;
4
5
  type RequestData = Record<string, string | number | boolean | object>;
5
6
  type RequestHeaders = Record<string, string>;
6
7
 
@@ -41,9 +42,18 @@ export class ApiClient {
41
42
  */
42
43
  private buildQueryString(params?: QueryParams): string {
43
44
  if (!params) return '';
44
- const queryString = new URLSearchParams(
45
- Object.entries(params).map(([key, value]) => [key, String(value)]),
46
- ).toString();
45
+
46
+ const searchParams = new URLSearchParams();
47
+
48
+ Object.entries(params).forEach(([key, value]) => {
49
+ if (Array.isArray(value)) {
50
+ value.forEach((item) => searchParams.append(key, String(item)));
51
+ } else {
52
+ searchParams.append(key, String(value));
53
+ }
54
+ });
55
+
56
+ const queryString = searchParams.toString();
47
57
  return queryString ? `?${queryString}` : '';
48
58
  }
49
59
 
package/vitest.config.ts CHANGED
@@ -3,7 +3,7 @@ import { defineConfig } from 'vitest/config';
3
3
  export default defineConfig({
4
4
  test: {
5
5
  environment: 'node',
6
- setupFiles: ['./src/__tests__/setup.ts'],
6
+ setupFiles: ['./src/__tests__/polyfills.ts', './src/__tests__/setup.ts'],
7
7
  testTimeout: 10_000,
8
8
  coverage: {
9
9
  provider: 'v8',