@paakd/api 0.0.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 (41) hide show
  1. package/dist/src/index.js +21 -0
  2. package/package.json +59 -0
  3. package/src/address.spec.ts +662 -0
  4. package/src/address.ts +300 -0
  5. package/src/auth.spec.ts +771 -0
  6. package/src/auth.ts +168 -0
  7. package/src/compressor/brotli.ts +26 -0
  8. package/src/index.ts +5 -0
  9. package/src/interceptors.spec.ts +1343 -0
  10. package/src/interceptors.ts +224 -0
  11. package/src/policies.spec.ts +595 -0
  12. package/src/policies.ts +431 -0
  13. package/src/products.spec.ts +710 -0
  14. package/src/products.ts +112 -0
  15. package/src/profile.spec.ts +626 -0
  16. package/src/profile.ts +169 -0
  17. package/src/proto/auth/v1/entities/auth.proto +140 -0
  18. package/src/proto/auth/v1/entities/policy.proto +57 -0
  19. package/src/proto/auth/v1/service.proto +26 -0
  20. package/src/proto/customers/v1/entities/address.proto +101 -0
  21. package/src/proto/customers/v1/entities/profile.proto +118 -0
  22. package/src/proto/customers/v1/service.proto +36 -0
  23. package/src/proto/files/v1/entities/file.proto +62 -0
  24. package/src/proto/files/v1/service.proto +19 -0
  25. package/src/proto/products/v1/entities/category.proto +98 -0
  26. package/src/proto/products/v1/entities/collection.proto +72 -0
  27. package/src/proto/products/v1/entities/product/create.proto +41 -0
  28. package/src/proto/products/v1/entities/product/option.proto +17 -0
  29. package/src/proto/products/v1/entities/product/shared.proto +255 -0
  30. package/src/proto/products/v1/entities/product/update.proto +66 -0
  31. package/src/proto/products/v1/entities/tag.proto +73 -0
  32. package/src/proto/products/v1/entities/taxonomy.proto +146 -0
  33. package/src/proto/products/v1/entities/type.proto +98 -0
  34. package/src/proto/products/v1/entities/variant.proto +127 -0
  35. package/src/proto/products/v1/service.proto +78 -0
  36. package/src/proto/promotions/v1/entities/campaign.proto +145 -0
  37. package/src/proto/promotions/v1/service.proto +17 -0
  38. package/src/proto/stocknodes/v1/entities/stocknode.proto +167 -0
  39. package/src/proto/stocknodes/v1/service.proto +21 -0
  40. package/src/registration.ts +170 -0
  41. package/src/test-utils.ts +176 -0
@@ -0,0 +1,710 @@
1
+ import { Code, createClient } from '@connectrpc/connect'
2
+ import { createGrpcTransport } from '@connectrpc/connect-node'
3
+ import { getCheckoutConfig } from '@paakd/config'
4
+ import { createCollection, getCollectionByID } from './products'
5
+ import {
6
+ createAuthenticationInterceptor,
7
+ createHeadersInterceptor,
8
+ } from './interceptors'
9
+ import {
10
+ type BaseTestContext,
11
+ type MockServiceClient,
12
+ clearAllMocks,
13
+ createMockConnectError,
14
+ setupCommonMocks,
15
+ } from './test-utils'
16
+
17
+ // Mock dependencies
18
+ vi.mock('@connectrpc/connect', async () => {
19
+ const actual = await vi.importActual('@connectrpc/connect')
20
+ return {
21
+ ...actual,
22
+ createClient: vi.fn(),
23
+ }
24
+ })
25
+
26
+ vi.mock('@connectrpc/connect-node', () => ({
27
+ createGrpcTransport: vi.fn(),
28
+ }))
29
+
30
+ vi.mock('@paakd/config', () => ({
31
+ getCheckoutConfig: vi.fn(),
32
+ }))
33
+
34
+ vi.mock('./interceptors', () => ({
35
+ createAuthenticationInterceptor: vi.fn(),
36
+ createHeadersInterceptor: vi.fn(),
37
+ }))
38
+
39
+ vi.mock('./compressor/brotli', () => ({
40
+ brotliCompression: {
41
+ name: 'brotli',
42
+ compress: vi.fn(),
43
+ decompress: vi.fn(),
44
+ },
45
+ }))
46
+
47
+ vi.mock('../gen/src/proto/products/v1/service_pb', () => ({
48
+ ProductsService: {},
49
+ }))
50
+
51
+ const mockGetCheckoutConfig = vi.mocked(getCheckoutConfig)
52
+ const mockCreateGrpcTransport = vi.mocked(createGrpcTransport)
53
+ const mockCreateClient = vi.mocked(createClient)
54
+ const mockCreateAuthenticationInterceptor = vi.mocked(
55
+ createAuthenticationInterceptor
56
+ )
57
+ const mockCreateHeadersInterceptor = vi.mocked(createHeadersInterceptor)
58
+
59
+ /**
60
+ * Extended test context for products service
61
+ */
62
+ interface ProductsTestContext extends BaseTestContext {
63
+ client: MockServiceClient
64
+ }
65
+
66
+ /**
67
+ * Setup function following @testing-library's render pattern
68
+ */
69
+ function setupProductsService(): ProductsTestContext {
70
+ clearAllMocks()
71
+ const { config, interceptors, transport } = setupCommonMocks()
72
+
73
+ const client: MockServiceClient = {
74
+ getCollectionByID: vi.fn(),
75
+ createCollection: vi.fn(),
76
+ }
77
+
78
+ mockGetCheckoutConfig.mockResolvedValue(config)
79
+ mockCreateGrpcTransport.mockReturnValue(transport)
80
+ mockCreateHeadersInterceptor.mockImplementation(() => next => async req => {
81
+ return await next(req)
82
+ })
83
+ mockCreateAuthenticationInterceptor.mockImplementation(
84
+ () => next => async req => {
85
+ return await next(req)
86
+ }
87
+ )
88
+ mockCreateClient.mockReturnValue(client as any)
89
+
90
+ return { client, config, interceptors, transport }
91
+ }
92
+
93
+ describe('Products Service', () => {
94
+ let consoleSpy: any
95
+
96
+ beforeEach(() => {
97
+ consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
98
+ })
99
+
100
+ afterEach(() => {
101
+ consoleSpy.mockRestore()
102
+ })
103
+
104
+ describe('getCollectionByID', () => {
105
+ it('should successfully retrieve a collection by ID', async () => {
106
+ const { client } = setupProductsService()
107
+
108
+ const mockCollection = {
109
+ id: 'collection-1',
110
+ name: 'Summer Collection',
111
+ description: 'Summer 2024 collection',
112
+ productCount: 25,
113
+ }
114
+
115
+ client.getCollectionByID.mockResolvedValue(mockCollection)
116
+
117
+ const result = await getCollectionByID({
118
+ body: {
119
+ collectionId: 'collection-1',
120
+ },
121
+ headers: {},
122
+ })
123
+
124
+ expect((result as any).status).toBe('success')
125
+ expect((result as any).value).toEqual(mockCollection)
126
+ expect(client.getCollectionByID).toHaveBeenCalledWith({
127
+ id: 'collection-1',
128
+ })
129
+ })
130
+
131
+ it('should return not found when collection does not exist', async () => {
132
+ const { client } = setupProductsService()
133
+
134
+ const error = createMockConnectError(
135
+ 5,
136
+ 'NOT_FOUND',
137
+ 'Collection not found'
138
+ )
139
+ client.getCollectionByID.mockRejectedValue(error)
140
+
141
+ const result = await getCollectionByID({
142
+ body: {
143
+ collectionId: 'invalid-collection',
144
+ },
145
+ headers: {},
146
+ })
147
+
148
+ expect((result as any).status).toBe('failed')
149
+ expect((result as any).code).toBe(5)
150
+ })
151
+
152
+ it('should handle service unavailability', async () => {
153
+ const { client } = setupProductsService()
154
+
155
+ const error = createMockConnectError(
156
+ 14,
157
+ 'UNAVAILABLE',
158
+ 'Service temporarily unavailable'
159
+ )
160
+ client.getCollectionByID.mockRejectedValue(error)
161
+
162
+ const result = await getCollectionByID({
163
+ body: {
164
+ collectionId: 'collection-1',
165
+ },
166
+ headers: {},
167
+ })
168
+
169
+ expect((result as any).status).toBe('failed')
170
+ })
171
+
172
+ it('should include custom headers in request context', async () => {
173
+ const { client } = setupProductsService()
174
+
175
+ client.getCollectionByID.mockResolvedValue({
176
+ id: 'collection-1',
177
+ name: 'Summer Collection',
178
+ })
179
+
180
+ const customHeaders = {
181
+ 'x-shop-id': '123',
182
+ 'x-locale': 'en-US',
183
+ }
184
+
185
+ await getCollectionByID({
186
+ body: {
187
+ collectionId: 'collection-1',
188
+ },
189
+ headers: customHeaders,
190
+ })
191
+
192
+ expect(mockCreateHeadersInterceptor).toHaveBeenCalledWith(customHeaders)
193
+ })
194
+
195
+ it('should use checkout configuration for enterprise URL', async () => {
196
+ const { client, config } = setupProductsService()
197
+
198
+ client.getCollectionByID.mockResolvedValue({
199
+ id: 'collection-1',
200
+ name: 'Summer Collection',
201
+ })
202
+
203
+ await getCollectionByID({
204
+ body: {
205
+ collectionId: 'collection-1',
206
+ },
207
+ headers: {},
208
+ })
209
+
210
+ expect(mockCreateGrpcTransport).toHaveBeenCalledWith(
211
+ expect.objectContaining({
212
+ baseUrl: config.enterpriseURL,
213
+ })
214
+ )
215
+ })
216
+
217
+ it('should enable compression for data transfer', async () => {
218
+ const { client } = setupProductsService()
219
+
220
+ client.getCollectionByID.mockResolvedValue({
221
+ id: 'collection-1',
222
+ name: 'Summer Collection',
223
+ })
224
+
225
+ await getCollectionByID({
226
+ body: {
227
+ collectionId: 'collection-1',
228
+ },
229
+ headers: {},
230
+ })
231
+
232
+ const transportConfig = (mockCreateGrpcTransport as any).mock.calls[0][0]
233
+ expect(transportConfig).toHaveProperty('acceptCompression')
234
+ expect(transportConfig).toHaveProperty('sendCompression')
235
+ })
236
+
237
+ it('should handle invalid collection ID format', async () => {
238
+ const { client } = setupProductsService()
239
+
240
+ const error = createMockConnectError(
241
+ 3,
242
+ 'INVALID_ARGUMENT',
243
+ 'Invalid collection ID format'
244
+ )
245
+ client.getCollectionByID.mockRejectedValue(error)
246
+
247
+ const result = await getCollectionByID({
248
+ body: {
249
+ collectionId: '',
250
+ },
251
+ headers: {},
252
+ })
253
+
254
+ expect((result as any).status).toBe('failed')
255
+ })
256
+
257
+ it('should handle internal server errors gracefully', async () => {
258
+ const { client } = setupProductsService()
259
+
260
+ const error = createMockConnectError(
261
+ 13,
262
+ 'INTERNAL',
263
+ 'Internal server error'
264
+ )
265
+ client.getCollectionByID.mockRejectedValue(error)
266
+
267
+ const result = await getCollectionByID({
268
+ body: {
269
+ collectionId: 'collection-1',
270
+ },
271
+ headers: {},
272
+ })
273
+
274
+ expect((result as any).status).toBe('failed')
275
+ expect((result as any).code).toBe(Code.Internal)
276
+ })
277
+
278
+ it('should support concurrent collection retrievals', async () => {
279
+ const { client } = setupProductsService()
280
+
281
+ client.getCollectionByID.mockResolvedValue({
282
+ id: 'collection-1',
283
+ name: 'Collection',
284
+ })
285
+
286
+ const results = await Promise.all([
287
+ getCollectionByID({
288
+ body: { collectionId: 'collection-1' },
289
+ headers: {},
290
+ }),
291
+ getCollectionByID({
292
+ body: { collectionId: 'collection-2' },
293
+ headers: {},
294
+ }),
295
+ getCollectionByID({
296
+ body: { collectionId: 'collection-3' },
297
+ headers: {},
298
+ }),
299
+ ])
300
+
301
+ expect(results).toHaveLength(3)
302
+ expect(results.every(r => (r as any).status === 'success')).toBe(true)
303
+ })
304
+ })
305
+
306
+ describe('createCollection', () => {
307
+ it('should successfully create a new collection', async () => {
308
+ const { client } = setupProductsService()
309
+
310
+ const newCollection = {
311
+ name: 'Winter Collection',
312
+ description: 'Winter 2024 collection',
313
+ }
314
+
315
+ const mockCollection = {
316
+ id: 'new-collection-1',
317
+ ...newCollection,
318
+ }
319
+
320
+ const mockResponse = {
321
+ collection: mockCollection,
322
+ }
323
+
324
+ client.createCollection.mockResolvedValue(mockResponse)
325
+
326
+ const result = await createCollection({
327
+ body: newCollection as any,
328
+ headers: {},
329
+ })
330
+
331
+ expect((result as any).status).toBe('success')
332
+ expect((result as any).value).toEqual(mockResponse)
333
+ expect(client.createCollection).toHaveBeenCalledWith(
334
+ expect.objectContaining(newCollection)
335
+ )
336
+ })
337
+
338
+ it('should reject collection creation with invalid data', async () => {
339
+ const { client } = setupProductsService()
340
+
341
+ const error = createMockConnectError(
342
+ 3,
343
+ 'INVALID_ARGUMENT',
344
+ 'Invalid collection data'
345
+ )
346
+ client.createCollection.mockRejectedValue(error)
347
+
348
+ const result = await createCollection({
349
+ body: {
350
+ name: '',
351
+ } as any,
352
+ headers: {},
353
+ })
354
+
355
+ expect((result as any).status).toBe('failed')
356
+ })
357
+
358
+ it('should handle duplicate collection names', async () => {
359
+ const { client } = setupProductsService()
360
+
361
+ const error = createMockConnectError(
362
+ 6,
363
+ 'ALREADY_EXISTS',
364
+ 'Collection with this name already exists'
365
+ )
366
+ client.createCollection.mockRejectedValue(error)
367
+
368
+ const result = await createCollection({
369
+ body: {
370
+ name: 'Existing Collection',
371
+ description: 'New description',
372
+ } as any,
373
+ headers: {},
374
+ })
375
+
376
+ expect((result as any).status).toBe('failed')
377
+ })
378
+
379
+ it('should reject creation without authentication', async () => {
380
+ const { client } = setupProductsService()
381
+
382
+ const error = createMockConnectError(
383
+ 16,
384
+ 'UNAUTHENTICATED',
385
+ 'Authentication required'
386
+ )
387
+ client.createCollection.mockRejectedValue(error)
388
+
389
+ const result = await createCollection({
390
+ body: {
391
+ name: 'New Collection',
392
+ description: 'Description',
393
+ } as any,
394
+ headers: {},
395
+ })
396
+
397
+ expect((result as any).status).toBe('failed')
398
+ })
399
+
400
+ it('should handle permission denied errors', async () => {
401
+ const { client } = setupProductsService()
402
+
403
+ const error = createMockConnectError(
404
+ 7,
405
+ 'PERMISSION_DENIED',
406
+ 'User does not have permission to create collections'
407
+ )
408
+ client.createCollection.mockRejectedValue(error)
409
+
410
+ const result = await createCollection({
411
+ body: {
412
+ name: 'New Collection',
413
+ description: 'Description',
414
+ } as any,
415
+ headers: {},
416
+ })
417
+
418
+ expect((result as any).status).toBe('failed')
419
+ })
420
+
421
+ it('should include authentication interceptor', async () => {
422
+ const { client } = setupProductsService()
423
+
424
+ client.createCollection.mockResolvedValue({
425
+ id: 'new-collection-1',
426
+ name: 'New Collection',
427
+ })
428
+
429
+ await createCollection({
430
+ body: {
431
+ name: 'New Collection',
432
+ description: 'Description',
433
+ } as any,
434
+ headers: {},
435
+ })
436
+
437
+ expect(mockCreateAuthenticationInterceptor).toHaveBeenCalled()
438
+ })
439
+
440
+ it('should handle service errors during creation', async () => {
441
+ const { client } = setupProductsService()
442
+
443
+ const error = createMockConnectError(
444
+ 13,
445
+ 'INTERNAL',
446
+ 'Database error occurred'
447
+ )
448
+ client.createCollection.mockRejectedValue(error)
449
+
450
+ const result = await createCollection({
451
+ body: {
452
+ name: 'New Collection',
453
+ description: 'Description',
454
+ } as any,
455
+ headers: {},
456
+ })
457
+
458
+ expect((result as any).status).toBe('failed')
459
+ expect(result.code).toBe(Code.Internal)
460
+ })
461
+
462
+ it('should handle collection limit exceeded', async () => {
463
+ const { client } = setupProductsService()
464
+
465
+ const error = createMockConnectError(
466
+ 8,
467
+ 'RESOURCE_EXHAUSTED',
468
+ 'Collection limit exceeded'
469
+ )
470
+ client.createCollection.mockRejectedValue(error)
471
+
472
+ const result = await createCollection({
473
+ body: {
474
+ name: 'New Collection',
475
+ description: 'Description',
476
+ } as any,
477
+ headers: {},
478
+ })
479
+
480
+ expect((result as any).status).toBe('failed')
481
+ })
482
+
483
+ it('should support multiple concurrent collection creations', async () => {
484
+ const { client } = setupProductsService()
485
+
486
+ client.createCollection.mockResolvedValue({
487
+ id: 'collection-id',
488
+ name: 'Collection',
489
+ })
490
+
491
+ const results = await Promise.all([
492
+ createCollection({
493
+ body: {
494
+ name: 'Collection 1',
495
+ description: 'Desc 1',
496
+ } as any,
497
+ headers: {},
498
+ }),
499
+ createCollection({
500
+ body: {
501
+ name: 'Collection 2',
502
+ description: 'Desc 2',
503
+ } as any,
504
+ headers: {},
505
+ }),
506
+ createCollection({
507
+ body: {
508
+ name: 'Collection 3',
509
+ description: 'Desc 3',
510
+ } as any,
511
+ headers: {},
512
+ }),
513
+ ])
514
+
515
+ expect(results).toHaveLength(3)
516
+ expect(results.every(r => (r as any).status === 'success')).toBe(true)
517
+ })
518
+ })
519
+
520
+ describe('Products Service - Common Behavior', () => {
521
+ it('should apply authentication interceptor for all operations', async () => {
522
+ const { client } = setupProductsService()
523
+
524
+ client.getCollectionByID.mockResolvedValue({
525
+ id: 'collection-1',
526
+ name: 'Collection',
527
+ })
528
+
529
+ await getCollectionByID({
530
+ body: {
531
+ collectionId: 'collection-1',
532
+ },
533
+ headers: {},
534
+ })
535
+
536
+ expect(mockCreateAuthenticationInterceptor).toHaveBeenCalled()
537
+ })
538
+
539
+ it('should load configuration for each request', async () => {
540
+ const { client } = setupProductsService()
541
+
542
+ client.getCollectionByID.mockResolvedValue({
543
+ id: 'collection-1',
544
+ name: 'Collection',
545
+ })
546
+
547
+ await getCollectionByID({
548
+ body: {
549
+ collectionId: 'collection-1',
550
+ },
551
+ headers: {},
552
+ })
553
+
554
+ expect(mockGetCheckoutConfig).toHaveBeenCalled()
555
+ })
556
+
557
+ it('should create transport with compression enabled', async () => {
558
+ const { client } = setupProductsService()
559
+
560
+ client.getCollectionByID.mockResolvedValue({
561
+ id: 'collection-1',
562
+ name: 'Collection',
563
+ })
564
+
565
+ await getCollectionByID({
566
+ body: {
567
+ collectionId: 'collection-1',
568
+ },
569
+ headers: {},
570
+ })
571
+
572
+ expect(mockCreateGrpcTransport).toHaveBeenCalledWith(
573
+ expect.objectContaining({
574
+ acceptCompression: expect.any(Array),
575
+ sendCompression: expect.any(Object),
576
+ })
577
+ )
578
+ })
579
+
580
+ it('should handle concurrent mixed operations', async () => {
581
+ const { client } = setupProductsService()
582
+
583
+ client.getCollectionByID.mockResolvedValue({
584
+ id: 'collection-1',
585
+ name: 'Collection',
586
+ })
587
+ client.createCollection.mockResolvedValue({
588
+ id: 'new-collection',
589
+ name: 'New Collection',
590
+ })
591
+
592
+ const results = await Promise.all([
593
+ getCollectionByID({
594
+ body: { collectionId: 'collection-1' },
595
+ headers: {},
596
+ }),
597
+ createCollection({
598
+ body: {
599
+ name: 'New Collection',
600
+ description: 'Desc',
601
+ } as any,
602
+ headers: {},
603
+ }),
604
+ getCollectionByID({
605
+ body: { collectionId: 'collection-2' },
606
+ headers: {},
607
+ }),
608
+ ])
609
+
610
+ expect(results).toHaveLength(3)
611
+ expect(results.every(r => (r as any).status === 'success')).toBe(true)
612
+ })
613
+
614
+ it('should handle edge cases with special characters in collection names', async () => {
615
+ const { client } = setupProductsService()
616
+
617
+ const mockCollection = {
618
+ id: 'collection-special',
619
+ name: "Summer's Special Collection 2024!",
620
+ description: 'Special offer: 50% off - São Paulo exclusive',
621
+ }
622
+
623
+ client.createCollection.mockResolvedValue(mockCollection)
624
+
625
+ const result = await createCollection({
626
+ body: {
627
+ name: "Summer's Special Collection 2024!",
628
+ description: 'Special offer: 50% off - São Paulo exclusive',
629
+ } as any,
630
+ headers: {},
631
+ })
632
+
633
+ expect((result as any).status).toBe('success')
634
+ expect((result as any).value).toEqual(mockCollection)
635
+ })
636
+
637
+ it('should handle very long collection descriptions', async () => {
638
+ const { client } = setupProductsService()
639
+
640
+ const longDescription = 'A'.repeat(500)
641
+ const mockCollection = {
642
+ id: 'collection-long',
643
+ name: 'Collection',
644
+ description: longDescription,
645
+ }
646
+
647
+ client.createCollection.mockResolvedValue(mockCollection)
648
+
649
+ const result = await createCollection({
650
+ body: {
651
+ name: 'Collection',
652
+ description: longDescription,
653
+ } as any,
654
+ headers: {},
655
+ })
656
+
657
+ expect((result as any).status).toBe('success')
658
+ })
659
+
660
+ it('should handle null or undefined headers gracefully', async () => {
661
+ const { client } = setupProductsService()
662
+
663
+ client.getCollectionByID.mockResolvedValue({
664
+ id: 'collection-1',
665
+ name: 'Collection',
666
+ })
667
+
668
+ const result = await getCollectionByID({
669
+ body: {
670
+ collectionId: 'collection-1',
671
+ },
672
+ headers: {
673
+ 'x-header': null,
674
+ },
675
+ })
676
+
677
+ expect((result as any).status).toBe('success')
678
+ })
679
+
680
+ it('should consistent error handling across operations', async () => {
681
+ const { client } = setupProductsService()
682
+
683
+ const error = createMockConnectError(
684
+ 16,
685
+ 'UNAUTHENTICATED',
686
+ 'Invalid authentication'
687
+ )
688
+
689
+ client.getCollectionByID.mockRejectedValueOnce(error)
690
+ client.createCollection.mockRejectedValueOnce(error)
691
+
692
+ const results = await Promise.all([
693
+ getCollectionByID({
694
+ body: { collectionId: 'collection-1' },
695
+ headers: {},
696
+ }),
697
+ createCollection({
698
+ body: {
699
+ name: 'Collection',
700
+ description: 'Desc',
701
+ } as any,
702
+ headers: {},
703
+ }),
704
+ ])
705
+
706
+ expect(results.every(r => (r as any).status === 'failed')).toBe(true)
707
+ expect(results.every(r => r.code === 16)).toBe(true)
708
+ })
709
+ })
710
+ })