@raketa-cloud/admin 1.7.4 → 1.7.6

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 (38) hide show
  1. package/dist/application.css +23 -0
  2. package/package.json +3 -3
  3. package/src/HonoAdapter.js +7 -3
  4. package/src/cloud/Articles.js +10 -3
  5. package/src/cloud/Events.js +9 -2
  6. package/src/cloud/Pages.js +13 -1
  7. package/src/cloud/documents/articlesDocuments.js +4 -2
  8. package/src/cloud/documents/eventsDocuments.js +4 -2
  9. package/src/cloud/documents/pagesDocuments.js +4 -2
  10. package/src/handlers/_shared/SettingsHandler.js +4 -0
  11. package/src/handlers/articles/ArticlesHandler.js +10 -4
  12. package/src/handlers/articles/ExportsHandler.js +35 -0
  13. package/src/handlers/events/EventsHandler.js +10 -4
  14. package/src/handlers/events/ExportsHandler.js +35 -0
  15. package/src/handlers/pages/ExportsHandler.js +35 -0
  16. package/src/handlers/pages/PagesHandler.js +10 -4
  17. package/src/helpers/CSVExportHelper.js +103 -0
  18. package/src/routeArticlesModule.js +13 -0
  19. package/src/routeEventsModule.js +13 -0
  20. package/src/routePagesModule.js +13 -0
  21. package/src/routeSettings.js +4 -0
  22. package/src/tests/RaketaClient.test.js +636 -0
  23. package/src/tests/handlers/articles/ArticlesHandler.test/index.test.js +18 -2
  24. package/src/tests/handlers/articles/ExportsHandler.test/index.test.js +217 -0
  25. package/src/tests/handlers/events/EventsHandler.test/index.test.js +17 -1
  26. package/src/tests/handlers/events/ExportsHandler.test/index.test.js +212 -0
  27. package/src/tests/handlers/pages/ExportsHandler.test/index.test.js +212 -0
  28. package/src/tests/handlers/pages/PagesHandler.test/index.test.js +17 -1
  29. package/src/tests/helpers/CSVExportHelper.test.js +364 -0
  30. package/src/views/articles/_shared/_export_button.ejs +9 -0
  31. package/src/views/articles/index.ejs +37 -1
  32. package/src/views/events/_shared/_export_button.ejs +9 -0
  33. package/src/views/events/index.ejs +37 -1
  34. package/src/views/layouts/application.ejs +2 -0
  35. package/src/views/pages/_shared/_export_button.ejs +9 -0
  36. package/src/views/pages/index.ejs +37 -1
  37. package/src/views/settings/_nav.ejs +7 -1
  38. package/src/views/settings/variables/edit_form.ejs +178 -0
@@ -15,6 +15,7 @@ import ClonesHandler from "./handlers/events/ClonesHandler";
15
15
  import DeletesHandler from "./handlers/events/DeletesHandler";
16
16
  import EventsAPIHandler from "./handlers/events/EventsAPIHandler";
17
17
  import EventsHandler from "./handlers/events/EventsHandler";
18
+ import ExportsHandler from "./handlers/events/ExportsHandler";
18
19
  import HistoryHandler from "./handlers/events/HistoryHandler";
19
20
  import TranslationsHandler from "./handlers/events/TranslationsHandler";
20
21
  import URLsHandler from "./handlers/events/URLsHandler";
@@ -35,6 +36,18 @@ function routeEventsModule(
35
36
  eventTypePlural,
36
37
  };
37
38
 
39
+ const exportsMapping = [
40
+ [
41
+ "GET",
42
+ `/${baseResource}/exports`,
43
+ "exports",
44
+ "events/exports",
45
+ "index",
46
+ ExportsHandler.index,
47
+ ],
48
+ ];
49
+ adapter.routeCollection(exportsMapping, resourceConfig);
50
+
38
51
  routeCollection(adapter, baseResource, EventsHandler, {
39
52
  ...resourceConfig,
40
53
  viewsFallback: "events",
@@ -13,6 +13,7 @@ import WidgetHandler from "./handlers/page_revisions/WidgetHandler";
13
13
  import ChangeHandler from "./handlers/pages/ChangeHandler";
14
14
  import ClonesHandler from "./handlers/pages/ClonesHandler";
15
15
  import DeletesHandler from "./handlers/pages/DeletesHandler";
16
+ import ExportsHandler from "./handlers/pages/ExportsHandler";
16
17
  import HistoryHandler from "./handlers/pages/HistoryHandler";
17
18
  import PagesAPIHandler from "./handlers/pages/PagesAPIHandler";
18
19
  import PagesHandler from "./handlers/pages/PagesHandler";
@@ -35,6 +36,18 @@ function routePagesModule(
35
36
  pageTypePlural,
36
37
  };
37
38
 
39
+ const exportsMapping = [
40
+ [
41
+ "GET",
42
+ `/${baseResource}/exports`,
43
+ "exports",
44
+ "pages/exports",
45
+ "index",
46
+ ExportsHandler.index,
47
+ ],
48
+ ];
49
+ adapter.routeCollection(exportsMapping, resourceConfig);
50
+
38
51
  routeCollection(adapter, pageTypePlural, PagesHandler, {
39
52
  ...resourceConfig,
40
53
  viewsFallback: "pages",
@@ -9,6 +9,10 @@ function routeSettings(adapter) {
9
9
  routeMember(adapter, "settings/layout", SettingsHandler, {
10
10
  resource: "settings/layout",
11
11
  });
12
+
13
+ routeMember(adapter, "settings/variables", SettingsHandler, {
14
+ resource: "settings/variables",
15
+ });
12
16
  }
13
17
 
14
18
  export default routeSettings;
@@ -0,0 +1,636 @@
1
+ import RaketaClient from '../cloud/RaketaClient'
2
+ import { request } from 'graphql-request'
3
+
4
+ jest.mock('graphql-request', () => ({
5
+ request: jest.fn(),
6
+ }))
7
+
8
+ describe('RaketaClient', () => {
9
+ function buildMockConfig(overrides = {}) {
10
+ return {
11
+ cloud: {
12
+ gateway_url: 'https://api.example.com',
13
+ gateway_project: 'test-project',
14
+ dxp_service_private: 'test-service',
15
+ },
16
+ ...overrides,
17
+ }
18
+ }
19
+
20
+ function buildClient(config = buildMockConfig(), token = 'test-token') {
21
+ const getConfig = () => config
22
+ return new RaketaClient(getConfig, token)
23
+ }
24
+
25
+ beforeEach(() => {
26
+ jest.clearAllMocks()
27
+ })
28
+
29
+ describe('constructor', () => {
30
+ test('initializes with getConfig and token', () => {
31
+ const getConfig = jest.fn(() => buildMockConfig())
32
+ const client = new RaketaClient(getConfig, 'my-token')
33
+
34
+ expect(client.getConfig).toBe(getConfig)
35
+ expect(client.token).toBe('my-token')
36
+ })
37
+ })
38
+
39
+ describe('gqlRequest', () => {
40
+ test('makes GraphQL request with correct parameters', async () => {
41
+ const client = buildClient()
42
+ const query = 'query { test }'
43
+ const variables = { id: '123' }
44
+ const headers = { 'x-custom': 'value' }
45
+
46
+ request.mockResolvedValue({ data: 'response' })
47
+
48
+ await client.gqlRequest('https://api.example.com/graphql', query, variables, headers)
49
+
50
+ expect(request).toHaveBeenCalledWith({
51
+ url: 'https://api.example.com/graphql',
52
+ document: query,
53
+ variables: variables,
54
+ requestHeaders: headers,
55
+ })
56
+ })
57
+
58
+ test('makes request with default empty variables and headers', async () => {
59
+ const client = buildClient()
60
+ const query = 'query { test }'
61
+
62
+ request.mockResolvedValue({ data: 'response' })
63
+
64
+ await client.gqlRequest('https://api.example.com/graphql', query)
65
+
66
+ expect(request).toHaveBeenCalledWith({
67
+ url: 'https://api.example.com/graphql',
68
+ document: query,
69
+ variables: {},
70
+ requestHeaders: {},
71
+ })
72
+ })
73
+ })
74
+
75
+ describe('query', () => {
76
+ test('makes request to gateway with proper headers', async () => {
77
+ const client = buildClient()
78
+ const query = 'query { users }'
79
+ const variables = { limit: 10 }
80
+
81
+ request.mockResolvedValue({ data: { users: [] } })
82
+
83
+ await client.query(query, variables)
84
+
85
+ expect(request).toHaveBeenCalledWith({
86
+ url: 'https://api.example.com/dispatch',
87
+ document: query,
88
+ variables: variables,
89
+ requestHeaders: {
90
+ 'x-raketa-project': 'test-project',
91
+ 'x-raketa-service': 'test-service',
92
+ 'x-raketa-identity': 'test-token',
93
+ },
94
+ })
95
+ })
96
+
97
+ test('uses token from constructor in headers', async () => {
98
+ const client = buildClient(buildMockConfig(), 'custom-token-123')
99
+ const query = 'query { test }'
100
+
101
+ request.mockResolvedValue({ data: {} })
102
+
103
+ await client.query(query)
104
+
105
+ expect(request).toHaveBeenCalledWith(
106
+ expect.objectContaining({
107
+ requestHeaders: expect.objectContaining({
108
+ 'x-raketa-identity': 'custom-token-123',
109
+ }),
110
+ })
111
+ )
112
+ })
113
+ })
114
+
115
+ describe('logError', () => {
116
+ test('throws error with message', () => {
117
+ const client = buildClient()
118
+ const consoleSpy = jest.spyOn(console, 'error').mockImplementation()
119
+
120
+ expect(() => client.logError('Something went wrong')).toThrow('Something went wrong')
121
+
122
+ consoleSpy.mockRestore()
123
+ })
124
+
125
+ test('logs context to console when provided', () => {
126
+ const client = buildClient()
127
+ const consoleSpy = jest.spyOn(console, 'error').mockImplementation()
128
+ const context = { code: 'ERR_001', details: 'Invalid input' }
129
+
130
+ expect(() => client.logError('Error occurred', context)).toThrow('Error occurred')
131
+ expect(consoleSpy).toHaveBeenCalledWith(context)
132
+
133
+ consoleSpy.mockRestore()
134
+ })
135
+
136
+ test('does not log when context is not provided', () => {
137
+ const client = buildClient()
138
+ const consoleSpy = jest.spyOn(console, 'error').mockImplementation()
139
+
140
+ expect(() => client.logError('Error occurred')).toThrow('Error occurred')
141
+ expect(consoleSpy).not.toHaveBeenCalled()
142
+
143
+ consoleSpy.mockRestore()
144
+ })
145
+ })
146
+
147
+ describe('formatErrors', () => {
148
+ test('formats single error into object', () => {
149
+ const client = buildClient()
150
+ const errors = [{ key: 'email', message: 'Email is invalid' }]
151
+
152
+ const result = client.formatErrors(errors)
153
+
154
+ expect(result).toEqual({
155
+ email: 'Email is invalid',
156
+ })
157
+ })
158
+
159
+ test('formats multiple errors for different keys', () => {
160
+ const client = buildClient()
161
+ const errors = [
162
+ { key: 'email', message: 'Email is required' },
163
+ { key: 'password', message: 'Password is too short' },
164
+ { key: 'username', message: 'Username is taken' },
165
+ ]
166
+
167
+ const result = client.formatErrors(errors)
168
+
169
+ expect(result).toEqual({
170
+ email: 'Email is required',
171
+ password: 'Password is too short',
172
+ username: 'Username is taken',
173
+ })
174
+ })
175
+
176
+ test('concatenates multiple messages for same key', () => {
177
+ const client = buildClient()
178
+ const errors = [
179
+ { key: 'email', message: 'Email is required' },
180
+ { key: 'email', message: 'Email format is invalid' },
181
+ { key: 'password', message: 'Password is required' },
182
+ ]
183
+
184
+ const result = client.formatErrors(errors)
185
+
186
+ expect(result).toEqual({
187
+ email: 'Email is required, Email format is invalid',
188
+ password: 'Password is required',
189
+ })
190
+ })
191
+
192
+ test('remaps generalError to specified key', () => {
193
+ const client = buildClient()
194
+ const errors = [
195
+ { key: 'generalError', message: 'Something went wrong' },
196
+ { key: 'email', message: 'Email is invalid' },
197
+ ]
198
+
199
+ const result = client.formatErrors(errors, 'base')
200
+
201
+ expect(result).toEqual({
202
+ base: 'Something went wrong',
203
+ email: 'Email is invalid',
204
+ })
205
+ })
206
+
207
+ test('keeps generalError key when no remap specified', () => {
208
+ const client = buildClient()
209
+ const errors = [{ key: 'generalError', message: 'Operation failed' }]
210
+
211
+ const result = client.formatErrors(errors)
212
+
213
+ expect(result).toEqual({
214
+ generalError: 'Operation failed',
215
+ })
216
+ })
217
+
218
+ test('handles empty errors array', () => {
219
+ const client = buildClient()
220
+ const result = client.formatErrors([])
221
+
222
+ expect(result).toEqual({})
223
+ })
224
+ })
225
+
226
+ describe('buildFiltersInput', () => {
227
+ test('extracts filters with f: prefix', () => {
228
+ const client = buildClient()
229
+ const query = {
230
+ 'f:status': 'active',
231
+ 'f:category': 'news',
232
+ page: '2',
233
+ order_by: 'created_at',
234
+ }
235
+
236
+ const result = client.buildFiltersInput(query)
237
+
238
+ expect(result).toEqual({
239
+ status: 'active',
240
+ category: 'news',
241
+ })
242
+ })
243
+
244
+ test('ignores filters with empty values', () => {
245
+ const client = buildClient()
246
+ const query = {
247
+ 'f:status': 'active',
248
+ 'f:category': '',
249
+ 'f:tag': null,
250
+ 'f:author': undefined,
251
+ }
252
+
253
+ const result = client.buildFiltersInput(query)
254
+
255
+ expect(result).toEqual({
256
+ status: 'active',
257
+ })
258
+ })
259
+
260
+ test('returns empty object when no filters present', () => {
261
+ const client = buildClient()
262
+ const query = {
263
+ page: '1',
264
+ order_by: 'title',
265
+ }
266
+
267
+ const result = client.buildFiltersInput(query)
268
+
269
+ expect(result).toEqual({})
270
+ })
271
+
272
+ test('returns empty object when query is null or undefined', () => {
273
+ const client = buildClient()
274
+
275
+ expect(client.buildFiltersInput(null)).toEqual({})
276
+ expect(client.buildFiltersInput(undefined)).toEqual({})
277
+ })
278
+
279
+ test('returns empty object when only page param exists', () => {
280
+ const client = buildClient()
281
+ const query = { page: '3' }
282
+
283
+ const result = client.buildFiltersInput(query)
284
+
285
+ expect(result).toEqual({})
286
+ })
287
+ })
288
+
289
+ describe('buildOrderInput', () => {
290
+ test('builds order object with field and direction', () => {
291
+ const client = buildClient()
292
+ const query = {
293
+ order_by: 'created_at',
294
+ order_direction: 'DESC',
295
+ }
296
+
297
+ const result = client.buildOrderInput(query)
298
+
299
+ expect(result).toEqual({
300
+ field: 'created_at',
301
+ direction: 'DESC',
302
+ })
303
+ })
304
+
305
+ test('defaults to ASC when direction not provided', () => {
306
+ const client = buildClient()
307
+ const query = { order_by: 'title' }
308
+
309
+ const result = client.buildOrderInput(query)
310
+
311
+ expect(result).toEqual({
312
+ field: 'title',
313
+ direction: 'ASC',
314
+ })
315
+ })
316
+
317
+ test('returns null when order_by not provided', () => {
318
+ const client = buildClient()
319
+ const query = { order_direction: 'DESC' }
320
+
321
+ const result = client.buildOrderInput(query)
322
+
323
+ expect(result).toBeNull()
324
+ })
325
+
326
+ test('returns null when order_by is empty string', () => {
327
+ const client = buildClient()
328
+ const query = { order_by: '' }
329
+
330
+ const result = client.buildOrderInput(query)
331
+
332
+ expect(result).toBeNull()
333
+ })
334
+ })
335
+
336
+ describe('buildPaginationInput', () => {
337
+ test('builds pagination with page number from query', () => {
338
+ const client = buildClient()
339
+ const query = { page: '3' }
340
+
341
+ const result = client.buildPaginationInput(query)
342
+
343
+ expect(result).toEqual({
344
+ page: 3,
345
+ pageSize: 25,
346
+ })
347
+ })
348
+
349
+ test('defaults to page 1 when not provided', () => {
350
+ const client = buildClient()
351
+ const query = {}
352
+
353
+ const result = client.buildPaginationInput(query)
354
+
355
+ expect(result).toEqual({
356
+ page: 1,
357
+ pageSize: 25,
358
+ })
359
+ })
360
+
361
+ test('parses string page number to integer', () => {
362
+ const client = buildClient()
363
+ const query = { page: '10' }
364
+
365
+ const result = client.buildPaginationInput(query)
366
+
367
+ expect(result.page).toBe(10)
368
+ expect(typeof result.page).toBe('number')
369
+ })
370
+
371
+ test('handles invalid page values by defaulting to 1', () => {
372
+ const client = buildClient()
373
+
374
+ expect(client.buildPaginationInput({ page: 'invalid' })).toEqual({
375
+ page: NaN,
376
+ pageSize: 25,
377
+ })
378
+ })
379
+ })
380
+
381
+ describe('buildListInput', () => {
382
+ test('builds complete input with pagination, filters, orderBy, and locale', () => {
383
+ const client = buildClient()
384
+ const query = {
385
+ page: '2',
386
+ 'f:status': 'published',
387
+ 'f:category': 'tech',
388
+ order_by: 'created_at',
389
+ order_direction: 'DESC',
390
+ }
391
+
392
+ const result = client.buildListInput(query, 'en')
393
+
394
+ expect(result).toEqual({
395
+ input: {
396
+ pagination: { page: 2, pageSize: 25 },
397
+ filters: { status: 'published', category: 'tech' },
398
+ orderBy: { field: 'created_at', direction: 'DESC' },
399
+ locale: 'en',
400
+ },
401
+ })
402
+ })
403
+
404
+ test('omits orderBy when not provided', () => {
405
+ const client = buildClient()
406
+ const query = { page: '1', 'f:status': 'active' }
407
+
408
+ const result = client.buildListInput(query)
409
+
410
+ expect(result).toEqual({
411
+ input: {
412
+ pagination: { page: 1, pageSize: 25 },
413
+ filters: { status: 'active' },
414
+ },
415
+ })
416
+ })
417
+
418
+ test('omits locale when not provided', () => {
419
+ const client = buildClient()
420
+ const query = { page: '1' }
421
+
422
+ const result = client.buildListInput(query)
423
+
424
+ expect(result.input).not.toHaveProperty('locale')
425
+ })
426
+
427
+ test('includes locale when provided', () => {
428
+ const client = buildClient()
429
+ const query = { page: '1' }
430
+
431
+ const result = client.buildListInput(query, 'de')
432
+
433
+ expect(result.input.locale).toBe('de')
434
+ })
435
+
436
+ test('handles empty query object', () => {
437
+ const client = buildClient()
438
+
439
+ const result = client.buildListInput({})
440
+
441
+ expect(result).toEqual({
442
+ input: {
443
+ pagination: { page: 1, pageSize: 25 },
444
+ filters: {},
445
+ },
446
+ })
447
+ })
448
+ })
449
+
450
+ describe('buildAllInput', () => {
451
+ test('builds input without pagination', () => {
452
+ const client = buildClient()
453
+ const query = {
454
+ page: '2',
455
+ 'f:status': 'active',
456
+ order_by: 'title',
457
+ order_direction: 'ASC',
458
+ }
459
+
460
+ const result = client.buildAllInput(query, 'fr')
461
+
462
+ expect(result).toEqual({
463
+ input: {
464
+ filters: { status: 'active' },
465
+ orderBy: { field: 'title', direction: 'ASC' },
466
+ locale: 'fr',
467
+ },
468
+ })
469
+ expect(result.input).not.toHaveProperty('pagination')
470
+ })
471
+
472
+ test('omits orderBy when not provided', () => {
473
+ const client = buildClient()
474
+ const query = { 'f:category': 'news' }
475
+
476
+ const result = client.buildAllInput(query)
477
+
478
+ expect(result).toEqual({
479
+ input: {
480
+ filters: { category: 'news' },
481
+ },
482
+ })
483
+ })
484
+
485
+ test('omits locale when not provided', () => {
486
+ const client = buildClient()
487
+ const query = {}
488
+
489
+ const result = client.buildAllInput(query)
490
+
491
+ expect(result.input).not.toHaveProperty('locale')
492
+ })
493
+ })
494
+
495
+ describe('deepMerge', () => {
496
+ test('merges two simple objects', () => {
497
+ const client = buildClient()
498
+ const target = { a: 1, b: 2 }
499
+ const source = { b: 3, c: 4 }
500
+
501
+ const result = client.deepMerge(target, source)
502
+
503
+ expect(result).toEqual({ a: 1, b: 3, c: 4 })
504
+ // Verify original is not modified
505
+ expect(target).toEqual({ a: 1, b: 2 })
506
+ })
507
+
508
+ test('merges nested objects deeply', () => {
509
+ const client = buildClient()
510
+ const target = {
511
+ settings: { theme: 'dark', locale: 'en' },
512
+ profile: { name: 'John' },
513
+ }
514
+ const source = {
515
+ settings: { locale: 'fr', notifications: true },
516
+ profile: { age: 30 },
517
+ }
518
+
519
+ const result = client.deepMerge(target, source)
520
+
521
+ expect(result).toEqual({
522
+ settings: { theme: 'dark', locale: 'fr', notifications: true },
523
+ profile: { name: 'John', age: 30 },
524
+ })
525
+ })
526
+
527
+ test('replaces arrays instead of merging', () => {
528
+ const client = buildClient()
529
+ const target = { tags: ['javascript', 'node'] }
530
+ const source = { tags: ['typescript', 'deno'] }
531
+
532
+ const result = client.deepMerge(target, source)
533
+
534
+ expect(result).toEqual({ tags: ['typescript', 'deno'] })
535
+ })
536
+
537
+ test('parses JSON strings in source', () => {
538
+ const client = buildClient()
539
+ const target = { config: {} }
540
+ const source = { config: '{"enabled": true, "count": 5}' }
541
+
542
+ const result = client.deepMerge(target, source)
543
+
544
+ expect(result.config).toEqual({ enabled: true, count: 5 })
545
+ })
546
+
547
+ test('parses JSON strings in nested objects', () => {
548
+ const client = buildClient()
549
+ const target = { settings: { meta: {} } }
550
+ const source = { settings: { meta: '{"title": "Test", "tags": ["a", "b"]}' } }
551
+
552
+ const result = client.deepMerge(target, source)
553
+
554
+ expect(result.settings.meta).toEqual({ title: 'Test', tags: ['a', 'b'] })
555
+ })
556
+
557
+ test('handles null values', () => {
558
+ const client = buildClient()
559
+ const target = { a: 1, b: null }
560
+ const source = { b: 2, c: null }
561
+
562
+ const result = client.deepMerge(target, source)
563
+
564
+ expect(result).toEqual({ a: 1, b: 2, c: null })
565
+ })
566
+
567
+ test('does not modify target object', () => {
568
+ const client = buildClient()
569
+ const target = { nested: { value: 'original' } }
570
+ const source = { nested: { value: 'modified' } }
571
+
572
+ client.deepMerge(target, source)
573
+
574
+ expect(target.nested.value).toBe('original')
575
+ })
576
+
577
+ test('handles deeply nested JSON strings', () => {
578
+ const client = buildClient()
579
+ const target = {
580
+ data: {
581
+ user: {
582
+ preferences: {},
583
+ },
584
+ },
585
+ }
586
+ const source = {
587
+ data: {
588
+ user: {
589
+ preferences: '{"theme": "dark", "language": "en"}',
590
+ },
591
+ },
592
+ }
593
+
594
+ const result = client.deepMerge(target, source)
595
+
596
+ expect(result.data.user.preferences).toEqual({
597
+ theme: 'dark',
598
+ language: 'en',
599
+ })
600
+ })
601
+
602
+ test('handles array of objects with JSON strings', () => {
603
+ const client = buildClient()
604
+ const target = { items: [] }
605
+ const source = {
606
+ items: [{ id: 1, data: '{"active": true}' }, { id: 2, data: '{"active": false}' }],
607
+ }
608
+
609
+ const result = client.deepMerge(target, source)
610
+
611
+ expect(result.items[0].data).toEqual({ active: true })
612
+ expect(result.items[1].data).toEqual({ active: false })
613
+ })
614
+
615
+ test('preserves non-JSON strings', () => {
616
+ const client = buildClient()
617
+ const target = { title: 'old' }
618
+ const source = { title: 'new title', description: 'just a string' }
619
+
620
+ const result = client.deepMerge(target, source)
621
+
622
+ expect(result).toEqual({
623
+ title: 'new title',
624
+ description: 'just a string',
625
+ })
626
+ })
627
+
628
+ test('handles empty objects', () => {
629
+ const client = buildClient()
630
+
631
+ expect(client.deepMerge({}, { a: 1 })).toEqual({ a: 1 })
632
+ expect(client.deepMerge({ a: 1 }, {})).toEqual({ a: 1 })
633
+ expect(client.deepMerge({}, {})).toEqual({})
634
+ })
635
+ })
636
+ })