@youversion/platform-core 1.9.2 → 1.11.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/.turbo/turbo-build.log +8 -8
- package/CHANGELOG.md +12 -0
- package/dist/index.cjs +470 -228
- package/dist/index.d.cts +5 -3
- package/dist/index.d.ts +5 -3
- package/dist/index.js +470 -228
- package/package.json +1 -1
- package/src/__tests__/MockLanguages.ts +15 -0
- package/src/__tests__/handlers.ts +6 -1
- package/src/__tests__/languages.test.ts +149 -1
- package/src/languages.ts +43 -7
package/package.json
CHANGED
|
@@ -76,6 +76,21 @@ export const mockLanguages: Language[] = [
|
|
|
76
76
|
speaking_population: 8000000,
|
|
77
77
|
default_bible_id: null,
|
|
78
78
|
},
|
|
79
|
+
{
|
|
80
|
+
id: 'por',
|
|
81
|
+
language: 'por',
|
|
82
|
+
script: 'Latn',
|
|
83
|
+
script_name: 'Latin',
|
|
84
|
+
aliases: ['pt'],
|
|
85
|
+
display_names: { en: 'Portuguese', pt: 'português' },
|
|
86
|
+
scripts: ['Latn'],
|
|
87
|
+
variants: [],
|
|
88
|
+
countries: ['PT', 'BR'],
|
|
89
|
+
text_direction: 'ltr',
|
|
90
|
+
writing_population: 250000000,
|
|
91
|
+
speaking_population: 250000000,
|
|
92
|
+
default_bible_id: null,
|
|
93
|
+
},
|
|
79
94
|
// Add more languages to exceed the maximum page size and exercise pagination
|
|
80
95
|
...Array.from({ length: 120 }, (_, i) => ({
|
|
81
96
|
id: `lang${i + 1}`,
|
|
@@ -45,7 +45,12 @@ export const handlers = [
|
|
|
45
45
|
: mockLanguages;
|
|
46
46
|
|
|
47
47
|
const defaultPageSize = 25;
|
|
48
|
-
const size =
|
|
48
|
+
const size =
|
|
49
|
+
pageSize === '*'
|
|
50
|
+
? filteredLanguages.length
|
|
51
|
+
: pageSize
|
|
52
|
+
? parseInt(pageSize, 10)
|
|
53
|
+
: defaultPageSize;
|
|
49
54
|
let start = 0;
|
|
50
55
|
|
|
51
56
|
if (pageToken) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach } from 'vitest';
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
2
2
|
import { ApiClient } from '../client';
|
|
3
3
|
import { LanguagesClient } from '../languages';
|
|
4
4
|
import { LanguageSchema } from '../schemas';
|
|
@@ -60,6 +60,116 @@ describe('LanguagesClient', () => {
|
|
|
60
60
|
'Country code must be a 2-character ISO 3166-1 alpha-2 code',
|
|
61
61
|
);
|
|
62
62
|
});
|
|
63
|
+
|
|
64
|
+
it('should normalize lowercase country codes to uppercase', async () => {
|
|
65
|
+
const getSpy = vi.spyOn(apiClient, 'get');
|
|
66
|
+
|
|
67
|
+
await languagesClient.getLanguages({ country: 'us', page_size: 5 });
|
|
68
|
+
|
|
69
|
+
expect(getSpy).toHaveBeenCalledWith(
|
|
70
|
+
'/v1/languages',
|
|
71
|
+
expect.objectContaining({ country: 'US' }),
|
|
72
|
+
);
|
|
73
|
+
getSpy.mockRestore();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('should fetch languages with valid fields filter', async () => {
|
|
77
|
+
const languages = await languagesClient.getLanguages({
|
|
78
|
+
fields: ['id', 'language', 'script'],
|
|
79
|
+
page_size: 10,
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
expect(languages.data).toHaveLength(10);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('should throw an error for invalid field name', async () => {
|
|
86
|
+
await expect(
|
|
87
|
+
languagesClient.getLanguages({
|
|
88
|
+
// @ts-expect-error - testing invalid field name
|
|
89
|
+
fields: ['id', 'invalid_field'],
|
|
90
|
+
}),
|
|
91
|
+
).rejects.toThrow();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('should return all languages with no pagination when page_size="*" and fields are valid', async () => {
|
|
95
|
+
const languages = await languagesClient.getLanguages({
|
|
96
|
+
fields: ['id', 'language'],
|
|
97
|
+
page_size: '*',
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// page_size="*" should return all results with no next page token
|
|
101
|
+
expect(languages.next_page_token).toBeNull();
|
|
102
|
+
// Should return more than the default page size (25)
|
|
103
|
+
expect(languages.data.length).toBeGreaterThan(25);
|
|
104
|
+
// Should equal total_size since we're fetching all
|
|
105
|
+
expect(languages.data.length).toBe(languages.total_size);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('should throw an error for page_size="*" without fields', async () => {
|
|
109
|
+
await expect(
|
|
110
|
+
languagesClient.getLanguages({
|
|
111
|
+
page_size: '*',
|
|
112
|
+
}),
|
|
113
|
+
).rejects.toThrow('page_size="*" requires 1-3 fields to be specified');
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('should throw an error for page_size="*" with empty fields array', async () => {
|
|
117
|
+
await expect(
|
|
118
|
+
languagesClient.getLanguages({
|
|
119
|
+
fields: [],
|
|
120
|
+
page_size: '*',
|
|
121
|
+
}),
|
|
122
|
+
).rejects.toThrow('page_size="*" requires 1-3 fields to be specified');
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('should throw an error for page_size="*" with more than 3 fields', async () => {
|
|
126
|
+
await expect(
|
|
127
|
+
languagesClient.getLanguages({
|
|
128
|
+
fields: ['id', 'language', 'script', 'script_name'],
|
|
129
|
+
page_size: '*',
|
|
130
|
+
}),
|
|
131
|
+
).rejects.toThrow('page_size="*" requires 1-3 fields to be specified');
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('should allow page_size="*" with exactly 1 field', async () => {
|
|
135
|
+
const languages = await languagesClient.getLanguages({
|
|
136
|
+
fields: ['id'],
|
|
137
|
+
page_size: '*',
|
|
138
|
+
});
|
|
139
|
+
expect(languages.data.length).toBeGreaterThan(0);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('should allow page_size="*" with exactly 3 fields', async () => {
|
|
143
|
+
const languages = await languagesClient.getLanguages({
|
|
144
|
+
fields: ['id', 'language', 'script'],
|
|
145
|
+
page_size: '*',
|
|
146
|
+
});
|
|
147
|
+
expect(languages.data.length).toBeGreaterThan(0);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('should throw an error for invalid page_size - negative number', async () => {
|
|
151
|
+
await expect(
|
|
152
|
+
languagesClient.getLanguages({
|
|
153
|
+
page_size: -1,
|
|
154
|
+
}),
|
|
155
|
+
).rejects.toThrow();
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('should throw an error for invalid page_size - zero', async () => {
|
|
159
|
+
await expect(
|
|
160
|
+
languagesClient.getLanguages({
|
|
161
|
+
page_size: 0,
|
|
162
|
+
}),
|
|
163
|
+
).rejects.toThrow();
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('should throw an error for invalid page_size - decimal', async () => {
|
|
167
|
+
await expect(
|
|
168
|
+
languagesClient.getLanguages({
|
|
169
|
+
page_size: 10.5,
|
|
170
|
+
}),
|
|
171
|
+
).rejects.toThrow();
|
|
172
|
+
});
|
|
63
173
|
});
|
|
64
174
|
|
|
65
175
|
describe('getLanguage', () => {
|
|
@@ -106,5 +216,43 @@ describe('LanguagesClient', () => {
|
|
|
106
216
|
'Language ID must match BCP 47 format (language or language+script)',
|
|
107
217
|
);
|
|
108
218
|
});
|
|
219
|
+
|
|
220
|
+
it('should throw an error for invalid language ID - single character', async () => {
|
|
221
|
+
await expect(languagesClient.getLanguage('e')).rejects.toThrow(
|
|
222
|
+
'Language ID must match BCP 47 format (language or language+script)',
|
|
223
|
+
);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it('should throw an error for invalid language ID - too long without script', async () => {
|
|
227
|
+
await expect(languagesClient.getLanguage('engl')).rejects.toThrow(
|
|
228
|
+
'Language ID must match BCP 47 format (language or language+script)',
|
|
229
|
+
);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('should throw an error for invalid language ID - invalid script format (lowercase)', async () => {
|
|
233
|
+
await expect(languagesClient.getLanguage('sr-latn')).rejects.toThrow(
|
|
234
|
+
'Language ID must match BCP 47 format (language or language+script)',
|
|
235
|
+
);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('should throw an error for invalid language ID - script too short', async () => {
|
|
239
|
+
await expect(languagesClient.getLanguage('sr-Lat')).rejects.toThrow(
|
|
240
|
+
'Language ID must match BCP 47 format (language or language+script)',
|
|
241
|
+
);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it('should throw an error for invalid language ID - script too long', async () => {
|
|
245
|
+
await expect(languagesClient.getLanguage('sr-Latin')).rejects.toThrow(
|
|
246
|
+
'Language ID must match BCP 47 format (language or language+script)',
|
|
247
|
+
);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it('should accept valid 3-letter language code', async () => {
|
|
251
|
+
const language = await languagesClient.getLanguage('por');
|
|
252
|
+
|
|
253
|
+
const { success } = LanguageSchema.safeParse(language);
|
|
254
|
+
expect(success).toBe(true);
|
|
255
|
+
expect(language.id).toBe('por');
|
|
256
|
+
});
|
|
109
257
|
});
|
|
110
258
|
});
|
package/src/languages.ts
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import type { ApiClient } from './client';
|
|
3
3
|
import type { Collection, Language } from './types';
|
|
4
|
+
import { LanguageSchema } from './schemas';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Options for getting languages collection.
|
|
7
8
|
*/
|
|
8
9
|
export type GetLanguagesOptions = {
|
|
9
|
-
page_size?: number;
|
|
10
|
+
page_size?: number | '*';
|
|
11
|
+
fields?: (keyof Language)[];
|
|
10
12
|
page_token?: string;
|
|
11
13
|
country?: string; // ISO 3166-1 alpha-2 country code
|
|
12
14
|
};
|
|
@@ -17,7 +19,7 @@ export type GetLanguagesOptions = {
|
|
|
17
19
|
export class LanguagesClient {
|
|
18
20
|
private client: ApiClient;
|
|
19
21
|
|
|
20
|
-
private languageIdSchema = z
|
|
22
|
+
private static readonly languageIdSchema = z
|
|
21
23
|
.string()
|
|
22
24
|
.trim()
|
|
23
25
|
.min(1, 'Language ID must be a non-empty string')
|
|
@@ -25,12 +27,32 @@ export class LanguagesClient {
|
|
|
25
27
|
/^[a-z]{2,3}(?:-[A-Z][a-z]{3})?$/,
|
|
26
28
|
'Language ID must match BCP 47 format (language or language+script)',
|
|
27
29
|
);
|
|
28
|
-
private countrySchema = z
|
|
30
|
+
private static readonly countrySchema = z
|
|
29
31
|
.string()
|
|
30
32
|
.trim()
|
|
31
33
|
.length(2, 'Country code must be a 2-character ISO 3166-1 alpha-2 code')
|
|
32
34
|
.toUpperCase();
|
|
33
35
|
|
|
36
|
+
private static readonly GetLanguagesOptionsSchema = z
|
|
37
|
+
.object({
|
|
38
|
+
page_size: z.union([z.number(), z.literal('*')]).optional(),
|
|
39
|
+
fields: z.array(LanguageSchema.keyof()).optional(),
|
|
40
|
+
page_token: z.string().optional(),
|
|
41
|
+
country: LanguagesClient.countrySchema,
|
|
42
|
+
})
|
|
43
|
+
.refine(
|
|
44
|
+
(data) => {
|
|
45
|
+
if (data.page_size === '*') {
|
|
46
|
+
return data.fields && data.fields?.length >= 1 && data.fields?.length <= 3;
|
|
47
|
+
}
|
|
48
|
+
return true;
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
message: 'page_size="*" required 1-3 fields to be specified',
|
|
52
|
+
path: ['page_size', 'fields'],
|
|
53
|
+
},
|
|
54
|
+
);
|
|
55
|
+
|
|
34
56
|
/**
|
|
35
57
|
* Creates a new LanguagesClient instance.
|
|
36
58
|
* @param client The API client to use for requests.
|
|
@@ -45,16 +67,30 @@ export class LanguagesClient {
|
|
|
45
67
|
* @returns A collection of Language objects.
|
|
46
68
|
*/
|
|
47
69
|
async getLanguages(options: GetLanguagesOptions = {}): Promise<Collection<Language>> {
|
|
48
|
-
const params: Record<string, string | number> = {};
|
|
70
|
+
const params: Record<string, string | number | (keyof Language)[]> = {};
|
|
49
71
|
|
|
50
72
|
if (options.country !== undefined) {
|
|
51
|
-
const country =
|
|
73
|
+
const country = LanguagesClient.countrySchema.parse(options.country);
|
|
52
74
|
params.country = country;
|
|
53
75
|
}
|
|
54
76
|
|
|
77
|
+
if (options.fields !== undefined) {
|
|
78
|
+
const fieldsSchema = z.array(LanguageSchema.keyof());
|
|
79
|
+
fieldsSchema.parse(options.fields);
|
|
80
|
+
params['fields[]'] = options.fields;
|
|
81
|
+
}
|
|
82
|
+
|
|
55
83
|
if (options.page_size !== undefined) {
|
|
56
|
-
const pageSizeSchema = z.number().int().positive();
|
|
84
|
+
const pageSizeSchema = z.union([z.number().int().positive(), z.literal('*')]);
|
|
57
85
|
pageSizeSchema.parse(options.page_size);
|
|
86
|
+
|
|
87
|
+
if (options.page_size === '*') {
|
|
88
|
+
const fieldsCount = options.fields?.length ?? 0;
|
|
89
|
+
if (fieldsCount < 1 || fieldsCount > 3) {
|
|
90
|
+
throw new Error('page_size="*" requires 1-3 fields to be specified');
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
58
94
|
params.page_size = options.page_size;
|
|
59
95
|
}
|
|
60
96
|
|
|
@@ -71,7 +107,7 @@ export class LanguagesClient {
|
|
|
71
107
|
* @returns The requested Language object.
|
|
72
108
|
*/
|
|
73
109
|
async getLanguage(languageId: string): Promise<Language> {
|
|
74
|
-
|
|
110
|
+
LanguagesClient.languageIdSchema.parse(languageId);
|
|
75
111
|
return this.client.get<Language>(`/v1/languages/${languageId}`);
|
|
76
112
|
}
|
|
77
113
|
}
|