@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,167 @@
1
+ syntax = "proto3";
2
+
3
+
4
+ import "buf/validate/validate.proto";
5
+
6
+ message StockNodeAddress {
7
+ optional string id = 1;
8
+ string address_1 = 2;
9
+ string address_2 = 3;
10
+ string address_3 = 4;
11
+ string phone_number = 5;
12
+ string zip_code = 6;
13
+ string country = 7;
14
+ string country_code = 8;
15
+ string company = 9;
16
+ string city = 10;
17
+ string province = 11;
18
+ string state_code = 12;
19
+ string email_address = 13;
20
+ }
21
+
22
+ message StockNodeFulfillmentSet {
23
+ string id = 1;
24
+ string name = 2;
25
+ string type = 3;
26
+ }
27
+
28
+ message CreateStockNodeRequest {
29
+ string name = 1 [
30
+ (buf.validate.field).required = true
31
+ ];
32
+ optional string description = 2;
33
+ optional string contact_name = 3;
34
+ string contact_email = 4 [
35
+ (buf.validate.field).string.email = true
36
+ ];
37
+ repeated string tags = 5;
38
+ repeated string sales_channels = 6;
39
+ repeated string fulfillment_providers = 7;
40
+ StockNodeAddress address = 8;
41
+ }
42
+
43
+ message CreateStockNodeResponse {
44
+ string id = 1;
45
+ string name = 2;
46
+ string description = 3;
47
+ string contact_name = 4;
48
+ string contact_email = 5;
49
+ repeated string tags = 6;
50
+ repeated string sales_channels = 7;
51
+ repeated string fulfillment_providers = 8;
52
+ StockNodeAddress address = 9;
53
+ repeated StockNodeFulfillmentSet fulfillment_sets = 10;
54
+ }
55
+
56
+ message UpdateStockNodeRequest {
57
+ string id = 1 [
58
+ (buf.validate.field).required = true,
59
+ (buf.validate.field).string.pattern = "^[a-z]{3}[_][0-9a-hjkmnp-tv-z]{26}$"
60
+ ];
61
+ optional string name = 2 ;
62
+ optional string description = 3;
63
+ optional string contact_name = 4;
64
+ optional string contact_email = 5 [
65
+ (buf.validate.field).string.email = true
66
+ ];
67
+ repeated string tags = 6;
68
+ repeated string sales_channels = 7;
69
+ repeated string fulfillment_providers = 8;
70
+ optional StockNodeAddress address = 9;
71
+ }
72
+
73
+ message UpdateStockNodeResponse {
74
+ string id = 1;
75
+ string name = 2;
76
+ string description = 3;
77
+ string contact_name = 4;
78
+ string contact_email = 5;
79
+ repeated string tags = 6;
80
+ repeated string sales_channels = 7;
81
+ repeated string fulfillment_providers = 8;
82
+ StockNodeAddress address = 9;
83
+ repeated StockNodeFulfillmentSet fulfillment_sets = 10;
84
+ }
85
+
86
+ message DeleteStockNodeRequest {
87
+ string id = 1 [
88
+ (buf.validate.field).required = true,
89
+ (buf.validate.field).string.pattern = "^[a-z]{3}[_][0-9a-hjkmnp-tv-z]{26}$"
90
+ ];
91
+ }
92
+
93
+ message DeleteStockNodeResponse {
94
+ string id = 1;
95
+ }
96
+
97
+ message GetStockNodeRequest {
98
+ string id = 1 [
99
+ (buf.validate.field).required = true,
100
+ (buf.validate.field).string.pattern = "^[a-z]{3}[_][0-9a-hjkmnp-tv-z]{26}$"
101
+ ];
102
+ }
103
+
104
+ message GetStockNodeResponse {
105
+ string id = 1;
106
+ string name = 2;
107
+ string description = 3;
108
+ string contact_name = 4;
109
+ string contact_email = 5;
110
+ repeated string tags = 6;
111
+ repeated string sales_channels = 7;
112
+ repeated string fulfillment_providers = 8;
113
+ StockNodeAddress address = 9;
114
+ repeated StockNodeFulfillmentSet fulfillment_sets = 10;
115
+ }
116
+
117
+ message ListStockNodesRequest {
118
+ int32 offset = 1 [
119
+ (buf.validate.field).int32.gte = 0
120
+ ];
121
+ uint32 limit = 2 [
122
+ (buf.validate.field).uint32.gte = 1,
123
+ (buf.validate.field).uint32.lte = 100
124
+ ];
125
+ optional string order_by = 3;
126
+ optional string filter = 4;
127
+ optional bool ascending = 5;
128
+ optional string request_path = 6;
129
+ }
130
+
131
+ message ListStockNodesResponse {
132
+ repeated CreateStockNodeResponse stock_nodes = 1;
133
+ }
134
+
135
+ enum FulfillmentSetType {
136
+ SHIPPING = 0;
137
+ PICKUP = 1;
138
+ DROPOFF = 2;
139
+ }
140
+
141
+ message AddFulfillmentSetRequest {
142
+ string name = 1;
143
+ FulfillmentSetType type = 2;
144
+ string stock_node_id = 3 [
145
+ (buf.validate.field).required = true,
146
+ (buf.validate.field).string.pattern = "^[a-z]{3}[_][0-9a-hjkmnp-tv-z]{26}$"
147
+ ];
148
+ }
149
+
150
+ message AddFulfillmentSetResponse {
151
+ CreateStockNodeResponse stock_node = 1;
152
+ }
153
+
154
+ message RemoveFulfillmentSetRequest {
155
+ string stock_node_id = 1 [
156
+ (buf.validate.field).required = true,
157
+ (buf.validate.field).string.pattern = "^[a-z]{3}[_][0-9a-hjkmnp-tv-z]{26}$"
158
+ ];
159
+ string fulfillment_set_id = 2 [
160
+ (buf.validate.field).required = true,
161
+ (buf.validate.field).string.pattern = "^[a-z]{3}[_][0-9a-hjkmnp-tv-z]{26}$"
162
+ ];
163
+ }
164
+
165
+ message RemoveFulfillmentSetResponse {
166
+ CreateStockNodeResponse stock_node = 1;
167
+ }
@@ -0,0 +1,21 @@
1
+ syntax = "proto3";
2
+
3
+ package apps.enterprise.interfaces.rpc.stocknodes.v1;
4
+
5
+ option go_package = "paakd.com/packages/api/gen/stocknodes/v1;stocknodesv1";
6
+
7
+ import "src/proto/stocknodes/v1/entities/stocknode.proto";
8
+
9
+ service StockNodesService {
10
+ rpc RemoveFulfillmentSet(RemoveFulfillmentSetRequest) returns (RemoveFulfillmentSetResponse) {}
11
+ rpc AddFulfillmentSet(AddFulfillmentSetRequest) returns (AddFulfillmentSetResponse) {}
12
+ rpc CreateStockNode(CreateStockNodeRequest) returns (CreateStockNodeResponse) {}
13
+ rpc DeleteStockNode(DeleteStockNodeRequest) returns (DeleteStockNodeResponse) {}
14
+ rpc GetStockNode(GetStockNodeRequest) returns (GetStockNodeResponse) {}
15
+ rpc UpdateStockNode(UpdateStockNodeRequest) returns (UpdateStockNodeResponse) {
16
+ option idempotency_level = NO_SIDE_EFFECTS;
17
+ }
18
+ rpc ListStockNodes(ListStockNodesRequest) returns (ListStockNodesResponse) {
19
+ option idempotency_level = NO_SIDE_EFFECTS;
20
+ }
21
+ }
@@ -0,0 +1,170 @@
1
+ import { Code, ConnectError, createClient } from '@connectrpc/connect'
2
+ import { createGrpcTransport } from '@connectrpc/connect-node'
3
+ import { getCheckoutConfig } from '@paakd/config'
4
+ import { AuthService } from '../gen/src/proto/auth/v1/service_pb'
5
+ import type { RegisterRequest } from '../gen/src/proto/auth/v1/entities/auth_pb'
6
+ import { brotliCompression } from './compressor/brotli'
7
+ import {
8
+ createAuthenticationInterceptor,
9
+ createHeadersInterceptor,
10
+ } from './interceptors'
11
+
12
+ const localeMap: Record<string, number> = {
13
+ 'en-GB': 0,
14
+ 'en-US': 1,
15
+ 'nl-NL': 2,
16
+ 'de-DE': 3,
17
+ en: 0,
18
+ }
19
+
20
+ export interface RegisterCustomerRequest {
21
+ body: Omit<RegisterRequest, '$typeName'>
22
+ headers: Record<string, string | null>
23
+ }
24
+
25
+ interface VerifyEmailRequest {
26
+ body: {
27
+ email: string
28
+ locale: string
29
+ idempotencyKey?: string
30
+ }
31
+ headers: Record<string, string | null>
32
+ }
33
+
34
+ interface ValidateOTPRequest {
35
+ body: {
36
+ email: string
37
+ otp: string
38
+ }
39
+ headers: Record<string, string | null>
40
+ }
41
+
42
+ export async function verifyEmail({
43
+ body: { email, idempotencyKey, locale },
44
+ headers,
45
+ }: VerifyEmailRequest) {
46
+ const checkoutConfig = await getCheckoutConfig()
47
+ const transport = createGrpcTransport({
48
+ baseUrl: checkoutConfig.enterpriseURL,
49
+ interceptors: [
50
+ createHeadersInterceptor(headers),
51
+ createAuthenticationInterceptor(checkoutConfig),
52
+ ],
53
+ acceptCompression: [brotliCompression],
54
+ sendCompression: brotliCompression,
55
+ })
56
+ const auth = createClient(AuthService, transport)
57
+
58
+ try {
59
+ const value = await auth.verifyEmail({
60
+ email,
61
+ locale: localeMap[locale] ?? 0,
62
+ idempotencyKey,
63
+ })
64
+
65
+ return {
66
+ value,
67
+ status: 'success',
68
+ }
69
+ } catch (err: unknown) {
70
+ if (err instanceof ConnectError) {
71
+ return {
72
+ code: err.code,
73
+ rawMessage: err.rawMessage,
74
+ message: err.rawMessage,
75
+ status: 'failed',
76
+ }
77
+ }
78
+
79
+ return {
80
+ code: Code.Internal,
81
+ rawMessage: 'An unexpected error occurred while verifying the email.',
82
+ message: 'An unexpected error occurred while verifying the email.',
83
+ status: 'failed',
84
+ } as unknown as ConnectError
85
+ }
86
+ }
87
+
88
+ export async function validateOTP({
89
+ body: { email, otp },
90
+ headers,
91
+ }: ValidateOTPRequest) {
92
+ const checkoutConfig = await getCheckoutConfig()
93
+ const transport = createGrpcTransport({
94
+ baseUrl: checkoutConfig.enterpriseURL,
95
+ interceptors: [
96
+ createHeadersInterceptor(headers),
97
+ createAuthenticationInterceptor(checkoutConfig),
98
+ ],
99
+ acceptCompression: [brotliCompression],
100
+ sendCompression: brotliCompression,
101
+ })
102
+ const auth = createClient(AuthService, transport)
103
+
104
+ try {
105
+ const value = await auth.validateOTP({
106
+ email,
107
+ otp,
108
+ })
109
+
110
+ return {
111
+ value,
112
+ status: 'success',
113
+ }
114
+ } catch (err: unknown) {
115
+ if (err instanceof ConnectError) {
116
+ return {
117
+ code: err.code,
118
+ rawMessage: err.rawMessage,
119
+ message: err.rawMessage,
120
+ status: 'failed',
121
+ }
122
+ }
123
+
124
+ return {
125
+ code: Code.Internal,
126
+ rawMessage: 'An unexpected error occurred while validating the OTP.',
127
+ message: 'An unexpected error occurred while validating the OTP.',
128
+ status: 'failed',
129
+ } as unknown as ConnectError
130
+ }
131
+ }
132
+
133
+ export async function registerCustomer({
134
+ body: val,
135
+ headers,
136
+ }: RegisterCustomerRequest) {
137
+ const checkoutConfig = await getCheckoutConfig()
138
+ const transport = createGrpcTransport({
139
+ baseUrl: checkoutConfig.enterpriseURL,
140
+ interceptors: [
141
+ createHeadersInterceptor(headers),
142
+ createAuthenticationInterceptor(checkoutConfig),
143
+ ],
144
+ acceptCompression: [brotliCompression],
145
+ sendCompression: brotliCompression,
146
+ })
147
+ const auth = createClient(AuthService, transport)
148
+
149
+ try {
150
+ await auth.register(val)
151
+ return {
152
+ status: 'success',
153
+ }
154
+ } catch (err: unknown) {
155
+ if (err instanceof ConnectError) {
156
+ return {
157
+ code: err.code,
158
+ rawMessage: err.rawMessage,
159
+ message: err.rawMessage,
160
+ status: 'failed',
161
+ }
162
+ }
163
+
164
+ return {
165
+ code: Code.Internal,
166
+ message: 'An unexpected error occurred during registration.',
167
+ status: 'failed',
168
+ } as unknown as ConnectError
169
+ }
170
+ }
@@ -0,0 +1,176 @@
1
+ import { ConnectError } from '@connectrpc/connect'
2
+ import type { Checkout } from '@paakd/config'
3
+ import { vi, type MockedFunction } from 'vitest'
4
+
5
+ // Re-export for test files
6
+ export type { Checkout, MockedFunction }
7
+
8
+ /**
9
+ * Common mock client interface for service tests
10
+ */
11
+ export type MockServiceClient = Record<
12
+ string,
13
+ MockedFunction<(...args: unknown[]) => unknown>
14
+ >
15
+
16
+ /**
17
+ * Mock Transport type
18
+ */
19
+ export type Transport = any
20
+
21
+ /**
22
+ * Base test context for API service tests
23
+ */
24
+ export interface BaseTestContext {
25
+ client: MockServiceClient
26
+ config: Checkout
27
+ interceptors: Record<string, MockedFunction<(...args: unknown[]) => unknown>>
28
+ transport: Transport
29
+ }
30
+
31
+ /**
32
+ * Creates a mock ConnectError with proper prototype chain
33
+ * This ensures that instanceof ConnectError checks work correctly
34
+ */
35
+ export function createMockConnectError(
36
+ code: number,
37
+ rawMessage: string,
38
+ message: string
39
+ ): ConnectError {
40
+ const error = {
41
+ code,
42
+ rawMessage,
43
+ message,
44
+ name: 'ConnectError',
45
+ } as ConnectError
46
+
47
+ Object.setPrototypeOf(error, ConnectError.prototype)
48
+ return error
49
+ }
50
+
51
+ /**
52
+ * Creates a default mock configuration used across API services
53
+ */
54
+ export function createMockConfig(): Checkout {
55
+ return {
56
+ hostname: 'example.com',
57
+ cmsRemoteURL: 'https://cms.example.com',
58
+ enterpriseRemoteURL: 'https://enterprise.example.com',
59
+ cmsURL: 'https://cms-local.example.com',
60
+ enterpriseURL: 'https://enterprise.example.com',
61
+ secureCookiePassword: 'secure-cookie-password',
62
+ forestAPIKey: 'forest-api-key',
63
+ saleChannelAccessKey: 'channel-access',
64
+ salesChannelAPISecret: 'channel-secret',
65
+ storeAccessKey: 'store-access',
66
+ storeAPISecret: 'store-secret',
67
+ isProduction: false,
68
+ posthogKey: 'posthog-key',
69
+ posthogDomain: 'posthog.example.com',
70
+ posthogHost: 'posthog.example.com',
71
+ assetsPath: '/assets',
72
+ assetsDomain: 'assets.example.com',
73
+ turnstileKey: 'turnstile-key',
74
+ turnstileSecret: 'turnstile-secret',
75
+ redis: {
76
+ user: 'redis-user',
77
+ host: 'localhost',
78
+ password: 'redis-password',
79
+ port: 6379,
80
+ },
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Creates a default mock interceptors object
86
+ */
87
+ export function createMockInterceptors(): Record<
88
+ string,
89
+ MockedFunction<(...args: unknown[]) => unknown>
90
+ > {
91
+ return {
92
+ headers: vi
93
+ .fn()
94
+ .mockReturnValue(Symbol('headers-interceptor')) as MockedFunction<
95
+ (...args: unknown[]) => unknown
96
+ >,
97
+ auth: vi.fn().mockReturnValue(Symbol('auth-interceptor')) as MockedFunction<
98
+ (...args: unknown[]) => unknown
99
+ >,
100
+ customer: vi
101
+ .fn()
102
+ .mockReturnValue(Symbol('customer-interceptor')) as MockedFunction<
103
+ (...args: unknown[]) => unknown
104
+ >,
105
+ }
106
+ }
107
+
108
+ /**
109
+ * Creates a mock gRPC transport
110
+ */
111
+ export function createMockTransport(): Transport {
112
+ return {} as Transport
113
+ }
114
+
115
+ /**
116
+ * Helper to setup common mocks for API service tests
117
+ * Returns objects ready to be used with setupService functions
118
+ */
119
+ export function setupCommonMocks(): {
120
+ config: Checkout
121
+ interceptors: Record<string, MockedFunction<(...args: unknown[]) => unknown>>
122
+ transport: Transport
123
+ } {
124
+ const config = createMockConfig()
125
+ const interceptors = createMockInterceptors()
126
+ const transport = createMockTransport()
127
+
128
+ return {
129
+ config,
130
+ interceptors,
131
+ transport,
132
+ }
133
+ }
134
+
135
+ /**
136
+ * Clears all Vitest mocks - useful for test isolation
137
+ */
138
+ export function clearAllMocks(): void {
139
+ vi.clearAllMocks()
140
+ }
141
+
142
+ /**
143
+ * Common error scenarios for testing
144
+ */
145
+ export const COMMON_ERROR_SCENARIOS = {
146
+ INVALID_CREDENTIALS: {
147
+ code: 16,
148
+ message: 'UNAUTHENTICATED',
149
+ rawMessage: 'Invalid credentials provided',
150
+ },
151
+ NOT_FOUND: {
152
+ code: 5,
153
+ message: 'NOT_FOUND',
154
+ rawMessage: 'Resource not found',
155
+ },
156
+ PERMISSION_DENIED: {
157
+ code: 7,
158
+ message: 'PERMISSION_DENIED',
159
+ rawMessage: 'Permission denied',
160
+ },
161
+ INTERNAL_ERROR: {
162
+ code: 13,
163
+ message: 'INTERNAL',
164
+ rawMessage: 'Internal server error',
165
+ },
166
+ SERVICE_UNAVAILABLE: {
167
+ code: 14,
168
+ message: 'UNAVAILABLE',
169
+ rawMessage: 'Service temporarily unavailable',
170
+ },
171
+ INVALID_ARGUMENT: {
172
+ code: 3,
173
+ message: 'INVALID_ARGUMENT',
174
+ rawMessage: 'Invalid argument provided',
175
+ },
176
+ } as const