@slotchain/sdk 1.0.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/README.md +167 -0
- package/dist/index.cjs.js +1463 -0
- package/dist/index.cjs.js.map +1 -0
- package/dist/index.d.mts +1149 -0
- package/dist/index.d.ts +1149 -0
- package/dist/index.esm.js +1422 -0
- package/dist/index.esm.js.map +1 -0
- package/package.json +52 -0
- package/src/client.ts +161 -0
- package/src/clients/__tests__/tenant-client.spec.ts +236 -0
- package/src/clients/booking-client.ts +98 -0
- package/src/clients/customer-client.ts +165 -0
- package/src/clients/data-quality-client.ts +93 -0
- package/src/clients/flow-client.ts +107 -0
- package/src/clients/notification-client.ts +108 -0
- package/src/clients/register-client.ts +19 -0
- package/src/clients/service-client.ts +215 -0
- package/src/clients/service-item-client.ts +103 -0
- package/src/clients/slot-client.ts +140 -0
- package/src/clients/studio-client.ts +128 -0
- package/src/clients/tenant-client.ts +148 -0
- package/src/errors.ts +41 -0
- package/src/index.ts +296 -0
- package/src/interceptors.ts +81 -0
- package/src/middleware/validateSlotlyRequest.ts +337 -0
- package/src/mocks/mockAdapter.ts +32 -0
- package/src/retry.ts +82 -0
- package/src/types/api.ts +192 -0
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { AxiosInstance } from 'axios';
|
|
2
|
+
import { ApiResponse, Tenant, TenantFullConfig } from '../types/api';
|
|
3
|
+
|
|
4
|
+
export class TenantClient {
|
|
5
|
+
constructor(private client: AxiosInstance) {}
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Get tenant configuration by slug
|
|
9
|
+
* GET /api/v1/tenant-config/:slug
|
|
10
|
+
*/
|
|
11
|
+
async getBySlug(slug: string): Promise<ApiResponse<Tenant>> {
|
|
12
|
+
// TODO: Replace with actual endpoint when available
|
|
13
|
+
const response = await this.client.get<ApiResponse<Tenant>>(
|
|
14
|
+
`/api/v1/tenant-config/${slug}`
|
|
15
|
+
);
|
|
16
|
+
return response.data;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Get tenant by ID
|
|
21
|
+
* GET /api/v1/tenants/:id
|
|
22
|
+
*/
|
|
23
|
+
async getById(id: string): Promise<ApiResponse<Tenant>> {
|
|
24
|
+
// TODO: Replace with actual endpoint when available
|
|
25
|
+
const response = await this.client.get<ApiResponse<Tenant>>(
|
|
26
|
+
`/api/v1/tenants/${id}`
|
|
27
|
+
);
|
|
28
|
+
return response.data;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* List all tenants
|
|
33
|
+
* GET /api/v1/tenants
|
|
34
|
+
*/
|
|
35
|
+
async list(params?: {
|
|
36
|
+
page?: number;
|
|
37
|
+
limit?: number;
|
|
38
|
+
}): Promise<ApiResponse<Tenant[]>> {
|
|
39
|
+
// TODO: Replace with actual endpoint when available
|
|
40
|
+
const response = await this.client.get<ApiResponse<Tenant[]>>(
|
|
41
|
+
'/api/v1/tenants',
|
|
42
|
+
{ params }
|
|
43
|
+
);
|
|
44
|
+
return response.data;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Create a new tenant
|
|
49
|
+
* POST /api/v1/tenants
|
|
50
|
+
*/
|
|
51
|
+
async create(data: Partial<Tenant>): Promise<ApiResponse<Tenant>> {
|
|
52
|
+
// TODO: Replace with actual endpoint when available
|
|
53
|
+
const response = await this.client.post<ApiResponse<Tenant>>(
|
|
54
|
+
'/api/v1/tenants',
|
|
55
|
+
data
|
|
56
|
+
);
|
|
57
|
+
return response.data;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Update tenant by ID
|
|
62
|
+
* PATCH /api/v1/tenants/:id
|
|
63
|
+
*/
|
|
64
|
+
async update(id: string, data: Partial<Tenant>): Promise<ApiResponse<Tenant>> {
|
|
65
|
+
// TODO: Replace with actual endpoint when available
|
|
66
|
+
const response = await this.client.patch<ApiResponse<Tenant>>(
|
|
67
|
+
`/api/v1/tenants/${id}`,
|
|
68
|
+
data
|
|
69
|
+
);
|
|
70
|
+
return response.data;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Delete tenant by ID
|
|
75
|
+
* DELETE /api/v1/tenants/:id
|
|
76
|
+
*/
|
|
77
|
+
async delete(id: string): Promise<ApiResponse<void>> {
|
|
78
|
+
// TODO: Replace with actual endpoint when available
|
|
79
|
+
const response = await this.client.delete<ApiResponse<void>>(
|
|
80
|
+
`/api/v1/tenants/${id}`
|
|
81
|
+
);
|
|
82
|
+
return response.data;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Get full tenant configuration including branding, services, and service items
|
|
87
|
+
* GET /api/v1/tenants/:slug
|
|
88
|
+
*
|
|
89
|
+
* This method returns the complete tenant configuration including:
|
|
90
|
+
* - Tenant basic info (id, slug, name)
|
|
91
|
+
* - Branding configuration (logo, colors, etc.)
|
|
92
|
+
* - All services associated with the tenant (with nested service items)
|
|
93
|
+
* - Feature flags and subscription level
|
|
94
|
+
*
|
|
95
|
+
* Note: Service items are nested within each service object (not as a separate top-level array).
|
|
96
|
+
* The endpoint returns full config by default (includes services with nested items).
|
|
97
|
+
*
|
|
98
|
+
* @param slug - Tenant slug identifier (required, non-nullable)
|
|
99
|
+
* @param includeServices - Include services with nested items (default: true)
|
|
100
|
+
* @returns Promise resolving to ApiResponse<TenantFullConfig>
|
|
101
|
+
*
|
|
102
|
+
* @example
|
|
103
|
+
* ```typescript
|
|
104
|
+
* const result = await slotly.tenant.getFullConfig('bright-accountants');
|
|
105
|
+
* if (result.success && result.data) {
|
|
106
|
+
* const config = result.data;
|
|
107
|
+
* console.log('Tenant:', config.name);
|
|
108
|
+
* console.log('Logo:', config.branding?.logoUrl);
|
|
109
|
+
* console.log('Services:', config.services.length);
|
|
110
|
+
*
|
|
111
|
+
* // Service items are nested within each service
|
|
112
|
+
* config.services.forEach(service => {
|
|
113
|
+
* console.log(`Service: ${service.name}`);
|
|
114
|
+
* if (service.serviceItems) {
|
|
115
|
+
* console.log(` Items: ${service.serviceItems.length}`);
|
|
116
|
+
* }
|
|
117
|
+
* });
|
|
118
|
+
* }
|
|
119
|
+
* ```
|
|
120
|
+
*
|
|
121
|
+
* **Guaranteed fields (non-nullable):**
|
|
122
|
+
* - `id`: string
|
|
123
|
+
* - `slug`: string
|
|
124
|
+
* - `name`: string
|
|
125
|
+
* - `services`: ServiceWithItems[] (may be empty array, each service may have nested `serviceItems`)
|
|
126
|
+
*
|
|
127
|
+
* **Optional fields:**
|
|
128
|
+
* - `branding`: TenantBranding | undefined
|
|
129
|
+
* - `services[].serviceItems`: ServiceItem[] | undefined (nested within each service)
|
|
130
|
+
* - `featuresEnabled`: string[] | undefined
|
|
131
|
+
* - `subscriptionLevel`: string | undefined
|
|
132
|
+
* - `config`: Record<string, any> | undefined
|
|
133
|
+
*
|
|
134
|
+
* **Error Codes:**
|
|
135
|
+
* - `TENANT_NOT_FOUND` - Tenant with slug not found
|
|
136
|
+
* - `AUTH_ERROR` - Authentication failed
|
|
137
|
+
* - `PERMISSION_DENIED` - Insufficient permissions
|
|
138
|
+
* - `NETWORK_ERROR` - Network request failed
|
|
139
|
+
*/
|
|
140
|
+
async getFullConfig(slug: string, includeServices: boolean = true): Promise<ApiResponse<TenantFullConfig>> {
|
|
141
|
+
const response = await this.client.get<ApiResponse<TenantFullConfig>>(
|
|
142
|
+
`/api/v1/tenants/${slug}`,
|
|
143
|
+
{ params: { includeServices } }
|
|
144
|
+
);
|
|
145
|
+
return response.data;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom error classes for Slotly SDK
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export class SlotlyApiError extends Error {
|
|
6
|
+
constructor(
|
|
7
|
+
public code: string,
|
|
8
|
+
message: string,
|
|
9
|
+
public statusCode?: number,
|
|
10
|
+
public details?: any
|
|
11
|
+
) {
|
|
12
|
+
super(message);
|
|
13
|
+
this.name = 'SlotlyApiError';
|
|
14
|
+
Object.setPrototypeOf(this, SlotlyApiError.prototype);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class SlotlyAuthError extends SlotlyApiError {
|
|
19
|
+
constructor(message: string, details?: any) {
|
|
20
|
+
super('AUTH_ERROR', message, 401, details);
|
|
21
|
+
this.name = 'SlotlyAuthError';
|
|
22
|
+
Object.setPrototypeOf(this, SlotlyAuthError.prototype);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export class SlotlyNetworkError extends Error {
|
|
27
|
+
constructor(message: string, public originalError?: any) {
|
|
28
|
+
super(message);
|
|
29
|
+
this.name = 'SlotlyNetworkError';
|
|
30
|
+
Object.setPrototypeOf(this, SlotlyNetworkError.prototype);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export class SlotlyConfigurationError extends Error {
|
|
35
|
+
constructor(message: string, public details?: any) {
|
|
36
|
+
super(message);
|
|
37
|
+
this.name = 'SlotlyConfigurationError';
|
|
38
|
+
Object.setPrototypeOf(this, SlotlyConfigurationError.prototype);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import axios, { AxiosInstance, InternalAxiosRequestConfig } from 'axios';
|
|
2
|
+
import { TenantClient } from './clients/tenant-client';
|
|
3
|
+
import { BookingClient } from './clients/booking-client';
|
|
4
|
+
import { ServiceClient } from './clients/service-client';
|
|
5
|
+
import { ServiceItemClient } from './clients/service-item-client';
|
|
6
|
+
import { SlotClient } from './clients/slot-client';
|
|
7
|
+
import { CustomerClient } from './clients/customer-client';
|
|
8
|
+
import { FlowClient } from './clients/flow-client';
|
|
9
|
+
import { StudioClient } from './clients/studio-client';
|
|
10
|
+
import { NotificationClient } from './clients/notification-client';
|
|
11
|
+
import { DataQualityClient } from './clients/data-quality-client';
|
|
12
|
+
import { setupRequestInterceptor, setupResponseInterceptor, setupErrorInterceptor } from './interceptors';
|
|
13
|
+
import { setupRetryInterceptor } from './retry';
|
|
14
|
+
import { SlotlyConfigurationError } from './errors';
|
|
15
|
+
|
|
16
|
+
// Re-export types
|
|
17
|
+
export type {
|
|
18
|
+
ApiResponse,
|
|
19
|
+
Tenant,
|
|
20
|
+
TenantFullConfig,
|
|
21
|
+
TenantBranding,
|
|
22
|
+
Booking,
|
|
23
|
+
Service,
|
|
24
|
+
ServiceItem,
|
|
25
|
+
ServiceWithItems,
|
|
26
|
+
ServiceItemWithService,
|
|
27
|
+
Category,
|
|
28
|
+
Slot,
|
|
29
|
+
Customer,
|
|
30
|
+
Flow,
|
|
31
|
+
FlowStep,
|
|
32
|
+
Studio,
|
|
33
|
+
Notification,
|
|
34
|
+
DataQualityIssue,
|
|
35
|
+
} from './types/api';
|
|
36
|
+
|
|
37
|
+
// Re-export errors
|
|
38
|
+
export {
|
|
39
|
+
SlotlyApiError,
|
|
40
|
+
SlotlyAuthError,
|
|
41
|
+
SlotlyNetworkError,
|
|
42
|
+
SlotlyConfigurationError,
|
|
43
|
+
} from './errors';
|
|
44
|
+
|
|
45
|
+
// Re-export middleware
|
|
46
|
+
export {
|
|
47
|
+
validateSlotlyRequest,
|
|
48
|
+
getSlotlyContext,
|
|
49
|
+
type SlotlyAuthContext,
|
|
50
|
+
type SlotlyRequest,
|
|
51
|
+
} from './middleware/validateSlotlyRequest';
|
|
52
|
+
|
|
53
|
+
export interface SlotlyClientOptions {
|
|
54
|
+
/**
|
|
55
|
+
* Required: Function that returns the API key or service token.
|
|
56
|
+
* This must return a non-empty string.
|
|
57
|
+
*/
|
|
58
|
+
getClientKey: () => Promise<string>;
|
|
59
|
+
/**
|
|
60
|
+
* Optional: Function that returns a Clerk JWT for user traceability.
|
|
61
|
+
* If provided and returns a token, it will be included in the Authorization header.
|
|
62
|
+
*/
|
|
63
|
+
getUserToken?: () => Promise<string | null>;
|
|
64
|
+
/**
|
|
65
|
+
* Optional: Custom base URL for the API.
|
|
66
|
+
* Defaults to process.env.SLOTLY_API_URL or 'https://api.slotly.dev'
|
|
67
|
+
*/
|
|
68
|
+
baseURL?: string;
|
|
69
|
+
/**
|
|
70
|
+
* Optional: Additional headers to include with every request.
|
|
71
|
+
*/
|
|
72
|
+
headers?: Record<string, string>;
|
|
73
|
+
/**
|
|
74
|
+
* Optional: Retry configuration.
|
|
75
|
+
*/
|
|
76
|
+
retry?: {
|
|
77
|
+
maxRetries?: number;
|
|
78
|
+
retryDelay?: number;
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Unified Slotly API client
|
|
84
|
+
* Provides typed access to all Slotly API domains
|
|
85
|
+
*
|
|
86
|
+
* Note: RegisterClient has been moved to @slotly/studio-sdk
|
|
87
|
+
* for studio-specific booking register operations.
|
|
88
|
+
*/
|
|
89
|
+
export interface SlotlyApi {
|
|
90
|
+
tenant: TenantClient;
|
|
91
|
+
booking: BookingClient;
|
|
92
|
+
service: ServiceClient;
|
|
93
|
+
serviceItem: ServiceItemClient;
|
|
94
|
+
slot: SlotClient;
|
|
95
|
+
customer: CustomerClient;
|
|
96
|
+
flow: FlowClient;
|
|
97
|
+
studio: StudioClient;
|
|
98
|
+
notification: NotificationClient;
|
|
99
|
+
dataQuality: DataQualityClient;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const defaultBaseURL = typeof process !== 'undefined' && process.env?.SLOTLY_API_URL
|
|
103
|
+
? process.env.SLOTLY_API_URL
|
|
104
|
+
: 'https://api.slotly.dev';
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Creates a new isolated Axios instance with authentication interceptors.
|
|
108
|
+
* Client isolation per instantiation ensures:
|
|
109
|
+
* - No cross-contamination between different auth contexts
|
|
110
|
+
* - SSR safety (each request gets its own client instance)
|
|
111
|
+
* - Proper isolation for scoped keys/roles in the future
|
|
112
|
+
*/
|
|
113
|
+
function createAuthenticatedClient(options: SlotlyClientOptions): AxiosInstance {
|
|
114
|
+
const client = axios.create({
|
|
115
|
+
baseURL: options.baseURL || defaultBaseURL,
|
|
116
|
+
headers: {
|
|
117
|
+
'Content-Type': 'application/json',
|
|
118
|
+
...options.headers,
|
|
119
|
+
},
|
|
120
|
+
timeout: 30000, // 30 seconds
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// Request interceptor: Add authentication headers
|
|
124
|
+
// This interceptor runs once per request and fetches fresh tokens
|
|
125
|
+
client.interceptors.request.use(
|
|
126
|
+
async (config: InternalAxiosRequestConfig) => {
|
|
127
|
+
try {
|
|
128
|
+
// Fetch both credentials in parallel for performance
|
|
129
|
+
const [clientKeyPromise, userTokenPromise] = [
|
|
130
|
+
options.getClientKey(),
|
|
131
|
+
options.getUserToken?.() ?? Promise.resolve(null),
|
|
132
|
+
];
|
|
133
|
+
|
|
134
|
+
const [clientKey, userToken] = await Promise.all([
|
|
135
|
+
clientKeyPromise,
|
|
136
|
+
userTokenPromise,
|
|
137
|
+
]);
|
|
138
|
+
|
|
139
|
+
// Validate that clientKey is provided and non-empty
|
|
140
|
+
if (!clientKey || typeof clientKey !== 'string' || clientKey.trim().length === 0) {
|
|
141
|
+
throw new SlotlyConfigurationError(
|
|
142
|
+
'getClientKey() must return a non-empty string. API key is required for all requests.',
|
|
143
|
+
{ received: clientKey }
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Always set the API key header
|
|
148
|
+
config.headers['x-slotly-api-key'] = clientKey;
|
|
149
|
+
|
|
150
|
+
// Conditionally set Authorization header if user token is provided
|
|
151
|
+
if (userToken && typeof userToken === 'string' && userToken.trim().length > 0) {
|
|
152
|
+
config.headers['Authorization'] = `Bearer ${userToken}`;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return config;
|
|
156
|
+
} catch (error) {
|
|
157
|
+
// If it's already a SlotlyConfigurationError, re-throw it
|
|
158
|
+
if (error instanceof SlotlyConfigurationError) {
|
|
159
|
+
throw error;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Handle errors from getClientKey() or getUserToken()
|
|
163
|
+
// If getClientKey fails, this is critical
|
|
164
|
+
if (error && typeof error === 'object' && 'message' in error) {
|
|
165
|
+
// eslint-disable-next-line no-console
|
|
166
|
+
console.error('[Slotly SDK] Failed to retrieve authentication credentials:', error);
|
|
167
|
+
|
|
168
|
+
throw new SlotlyConfigurationError(
|
|
169
|
+
`Failed to retrieve authentication credentials: ${error.message}`,
|
|
170
|
+
{ originalError: error }
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
throw error;
|
|
175
|
+
}
|
|
176
|
+
},
|
|
177
|
+
(error: Error) => Promise.reject(error)
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
// Request interceptor: Logging
|
|
181
|
+
client.interceptors.request.use(setupRequestInterceptor());
|
|
182
|
+
|
|
183
|
+
// Response interceptor: Logging and error shaping
|
|
184
|
+
client.interceptors.response.use(
|
|
185
|
+
(response: any) => setupResponseInterceptor()(response),
|
|
186
|
+
(error: any) => setupErrorInterceptor()(error)
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
// Error interceptor: Retry logic
|
|
190
|
+
client.interceptors.response.use(
|
|
191
|
+
(response: any) => response,
|
|
192
|
+
setupRetryInterceptor({
|
|
193
|
+
maxRetries: options.retry?.maxRetries ?? 3,
|
|
194
|
+
retryDelay: options.retry?.retryDelay ?? 1000,
|
|
195
|
+
})
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
return client;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Initialize and configure a new Slotly API client with dual authentication.
|
|
203
|
+
*
|
|
204
|
+
* This function creates an isolated Axios instance per call, ensuring:
|
|
205
|
+
* - SSR safety: Each request/context gets its own client instance
|
|
206
|
+
* - No cross-contamination: Different auth contexts don't interfere
|
|
207
|
+
* - Scalable: Supports future expansion for scoped keys, roles, or per-request tokens
|
|
208
|
+
*
|
|
209
|
+
* Authentication model:
|
|
210
|
+
* - **getClientKey()**: Required. Always sets `x-slotly-api-key` header.
|
|
211
|
+
* Identifies the SDK/application making the request.
|
|
212
|
+
* - **getUserToken()**: Optional. Sets `Authorization: Bearer <token>` header if provided.
|
|
213
|
+
* Provides user traceability and authorization scoping.
|
|
214
|
+
*
|
|
215
|
+
* @example Basic usage with API key (public SDK client):
|
|
216
|
+
* ```ts
|
|
217
|
+
* import { useSlotly } from '@slotly/sdk';
|
|
218
|
+
*
|
|
219
|
+
* const slotly = useSlotly({
|
|
220
|
+
* getClientKey: async () => process.env.SLOTLY_SDK_KEY!,
|
|
221
|
+
* });
|
|
222
|
+
* ```
|
|
223
|
+
*
|
|
224
|
+
* @example Server-side with Clerk (authenticated, user-scoped):
|
|
225
|
+
* ```ts
|
|
226
|
+
* import { getToken } from '@clerk/clerk-sdk-node';
|
|
227
|
+
* import { useSlotly } from '@slotly/sdk';
|
|
228
|
+
*
|
|
229
|
+
* // In an API route or server action
|
|
230
|
+
* const slotly = useSlotly({
|
|
231
|
+
* getClientKey: async () => process.env.SLOTLY_SDK_KEY!,
|
|
232
|
+
* getUserToken: async () => await getToken(req),
|
|
233
|
+
* });
|
|
234
|
+
* ```
|
|
235
|
+
*
|
|
236
|
+
* @example Client-side with Clerk (Next.js):
|
|
237
|
+
* ```ts
|
|
238
|
+
* import { useAuth } from '@clerk/nextjs';
|
|
239
|
+
* import { useSlotly } from '@slotly/sdk';
|
|
240
|
+
*
|
|
241
|
+
* function MyComponent() {
|
|
242
|
+
* const { getToken } = useAuth();
|
|
243
|
+
*
|
|
244
|
+
* const slotly = useSlotly({
|
|
245
|
+
* getClientKey: async () => process.env.NEXT_PUBLIC_SLOTLY_SDK_KEY!,
|
|
246
|
+
* getUserToken: getToken,
|
|
247
|
+
* });
|
|
248
|
+
* }
|
|
249
|
+
* ```
|
|
250
|
+
*
|
|
251
|
+
* @example Future: Scoped keys or role-based access
|
|
252
|
+
* ```ts
|
|
253
|
+
* // Future enhancement: Per-request scoping
|
|
254
|
+
* const slotly = useSlotly({
|
|
255
|
+
* getClientKey: async () => getScopedKey(role, tenantId),
|
|
256
|
+
* getUserToken: async () => getToken(),
|
|
257
|
+
* });
|
|
258
|
+
* ```
|
|
259
|
+
*
|
|
260
|
+
* @throws {SlotlyConfigurationError} If getClientKey is missing, returns null, undefined, or empty string.
|
|
261
|
+
*/
|
|
262
|
+
export const useSlotly = (options: SlotlyClientOptions): SlotlyApi => {
|
|
263
|
+
// Validate required options
|
|
264
|
+
if (!options || typeof options !== 'object') {
|
|
265
|
+
throw new SlotlyConfigurationError(
|
|
266
|
+
'useSlotly() requires an options object with getClientKey function'
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (!options.getClientKey || typeof options.getClientKey !== 'function') {
|
|
271
|
+
throw new SlotlyConfigurationError(
|
|
272
|
+
'getClientKey must be a function that returns Promise<string>'
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Create a new isolated client instance for this configuration
|
|
277
|
+
// This ensures SSR safety and prevents cross-contamination
|
|
278
|
+
const client = createAuthenticatedClient(options);
|
|
279
|
+
|
|
280
|
+
// Return the unified API object with all domain clients
|
|
281
|
+
return {
|
|
282
|
+
tenant: new TenantClient(client),
|
|
283
|
+
booking: new BookingClient(client),
|
|
284
|
+
service: new ServiceClient(client),
|
|
285
|
+
serviceItem: new ServiceItemClient(client),
|
|
286
|
+
slot: new SlotClient(client),
|
|
287
|
+
customer: new CustomerClient(client),
|
|
288
|
+
flow: new FlowClient(client),
|
|
289
|
+
studio: new StudioClient(client),
|
|
290
|
+
notification: new NotificationClient(client),
|
|
291
|
+
dataQuality: new DataQualityClient(client),
|
|
292
|
+
};
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
// Default export for convenience
|
|
296
|
+
export default useSlotly;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { AxiosError, AxiosResponse, InternalAxiosRequestConfig } from 'axios';
|
|
2
|
+
import { SlotlyApiError, SlotlyAuthError, SlotlyNetworkError } from './errors';
|
|
3
|
+
import { ApiResponse } from './types/api';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Request interceptor for logging
|
|
7
|
+
*/
|
|
8
|
+
export function setupRequestInterceptor() {
|
|
9
|
+
return (config: InternalAxiosRequestConfig) => {
|
|
10
|
+
// TODO: Add request logging if needed
|
|
11
|
+
// console.log(`[Slotly SDK] ${config.method?.toUpperCase()} ${config.url}`);
|
|
12
|
+
return config;
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Response interceptor for error shaping and logging
|
|
18
|
+
*/
|
|
19
|
+
export function setupResponseInterceptor() {
|
|
20
|
+
return (response: AxiosResponse) => {
|
|
21
|
+
// TODO: Add response logging if needed
|
|
22
|
+
// console.log(`[Slotly SDK] Response: ${response.status} ${response.config.url}`);
|
|
23
|
+
return response;
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Error interceptor for shaping errors into SlotlyApiError
|
|
29
|
+
*/
|
|
30
|
+
export function setupErrorInterceptor() {
|
|
31
|
+
return (error: AxiosError<ApiResponse<any>>) => {
|
|
32
|
+
// Network errors (no response)
|
|
33
|
+
if (!error.response) {
|
|
34
|
+
const networkError = new SlotlyNetworkError(
|
|
35
|
+
error.message || 'Network error occurred',
|
|
36
|
+
error
|
|
37
|
+
);
|
|
38
|
+
// TODO: Add error logging
|
|
39
|
+
// console.error('[Slotly SDK] Network error:', networkError);
|
|
40
|
+
return Promise.reject(networkError);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const { status, data } = error.response;
|
|
44
|
+
|
|
45
|
+
// Handle authentication errors
|
|
46
|
+
if (status === 401 || status === 403) {
|
|
47
|
+
const authError = new SlotlyAuthError(
|
|
48
|
+
data?.error?.message || 'Authentication failed',
|
|
49
|
+
data?.error?.details
|
|
50
|
+
);
|
|
51
|
+
// TODO: Add error logging
|
|
52
|
+
// console.error('[Slotly SDK] Auth error:', authError);
|
|
53
|
+
return Promise.reject(authError);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Handle API errors with structured error format
|
|
57
|
+
if (data?.error) {
|
|
58
|
+
const apiError = new SlotlyApiError(
|
|
59
|
+
data.error.code || 'API_ERROR',
|
|
60
|
+
data.error.message || 'An error occurred',
|
|
61
|
+
status,
|
|
62
|
+
data.error.details
|
|
63
|
+
);
|
|
64
|
+
// TODO: Add error logging
|
|
65
|
+
// console.error('[Slotly SDK] API error:', apiError);
|
|
66
|
+
return Promise.reject(apiError);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Handle other HTTP errors
|
|
70
|
+
const apiError = new SlotlyApiError(
|
|
71
|
+
'HTTP_ERROR',
|
|
72
|
+
error.message || `Request failed with status ${status}`,
|
|
73
|
+
status,
|
|
74
|
+
data
|
|
75
|
+
);
|
|
76
|
+
// TODO: Add error logging
|
|
77
|
+
// console.error('[Slotly SDK] HTTP error:', apiError);
|
|
78
|
+
return Promise.reject(apiError);
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|