@owlmeans/client-wl 0.1.1 → 0.1.3
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/LICENSE +1 -1
- package/README.md +5 -658
- package/package.json +4 -2
- package/tsconfig.json +5 -10
package/LICENSE
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
MIT License
|
|
2
2
|
|
|
3
|
-
Copyright (c)
|
|
3
|
+
Copyright (c) 2026 OwlMeans Common — Fullstack typescript framework
|
|
4
4
|
|
|
5
5
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
6
|
of this software and associated documentation files (the "Software"), to deal
|
package/README.md
CHANGED
|
@@ -1,671 +1,18 @@
|
|
|
1
1
|
# @owlmeans/client-wl
|
|
2
2
|
|
|
3
|
-
Client-side whitelabeling
|
|
3
|
+
Client-side whitelabeling placeholder for the OwlMeans framework.
|
|
4
4
|
|
|
5
5
|
## Overview
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
- **Client Whitelabeling Foundation**: Base infrastructure for client-side whitelabeling
|
|
10
|
-
- **Entity-based Customization**: Support for entity-specific branding and configuration
|
|
11
|
-
- **Cross-platform Compatibility**: Works across web, mobile, and desktop client applications
|
|
12
|
-
- **Cache Management**: Client-side caching of whitelabeling configurations
|
|
13
|
-
- **API Integration**: Standardized interface for communicating with whitelabeling providers
|
|
14
|
-
- **Type Safety**: Comprehensive TypeScript interfaces for whitelabeling data
|
|
15
|
-
|
|
16
|
-
This package follows the OwlMeans "quadra" pattern as the **client** implementation, complementing:
|
|
17
|
-
- **@owlmeans/wled**: Common whitelabeling declarations and base functionality *(base package)*
|
|
18
|
-
- **@owlmeans/client-wl**: Base client whitelabeling functionality *(this package)*
|
|
19
|
-
- **@owlmeans/web-wl**: Web browser whitelabeling implementation
|
|
20
|
-
- **@owlmeans/server-wl**: Server-side whitelabeling implementation
|
|
7
|
+
This package is a reserved namespace for client-side whitelabeling functionality. It is currently a stub with no exports.
|
|
21
8
|
|
|
22
9
|
## Installation
|
|
23
10
|
|
|
24
11
|
```bash
|
|
25
|
-
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
## Dependencies
|
|
29
|
-
|
|
30
|
-
This package requires and integrates with:
|
|
31
|
-
- `@owlmeans/wled`: Core whitelabeling types and modules
|
|
32
|
-
- `@owlmeans/context`: Context management and service registration
|
|
33
|
-
- `@owlmeans/client`: Base client functionality and services
|
|
34
|
-
|
|
35
|
-
## Key Concepts
|
|
36
|
-
|
|
37
|
-
### Client-side Whitelabeling
|
|
38
|
-
Provides the foundational infrastructure for implementing whitelabeling on the client side:
|
|
39
|
-
- **Data Loading**: Standardized interface for loading whitelabeling data
|
|
40
|
-
- **Caching Strategy**: Efficient caching of whitelabeling configurations
|
|
41
|
-
- **Error Handling**: Robust error handling for failed whitelabeling requests
|
|
42
|
-
- **Cross-platform**: Works consistently across different client platforms
|
|
43
|
-
|
|
44
|
-
### Entity-based Customization
|
|
45
|
-
Enables dynamic customization based on entity identification:
|
|
46
|
-
- **Entity Resolution**: Map domains, subdomains, or custom identifiers to entities
|
|
47
|
-
- **Configuration Loading**: Load entity-specific branding and configuration data
|
|
48
|
-
- **Dynamic Application**: Apply configurations at runtime without deployment
|
|
49
|
-
- **Fallback Handling**: Graceful degradation when entity data is unavailable
|
|
50
|
-
|
|
51
|
-
### Whitelabeling Data Types
|
|
52
|
-
Supports various types of whitelabeling data:
|
|
53
|
-
- **Company Information**: Business details, names, descriptions
|
|
54
|
-
- **Visual Styles**: Colors, fonts, themes, and visual branding
|
|
55
|
-
- **Media Assets**: Logos, images, and brand assets
|
|
56
|
-
- **Custom URLs**: Entity-specific URL configurations
|
|
57
|
-
- **Extended Data**: Custom whitelabeling data types as needed
|
|
58
|
-
|
|
59
|
-
## API Reference
|
|
60
|
-
|
|
61
|
-
*Note: This package provides foundational types and interfaces. Specific implementations are available in platform-specific packages like `@owlmeans/web-wl`.*
|
|
62
|
-
|
|
63
|
-
### Core Interfaces
|
|
64
|
-
|
|
65
|
-
#### `ClientWhitelabelService`
|
|
66
|
-
|
|
67
|
-
Base interface for client-side whitelabeling services.
|
|
68
|
-
|
|
69
|
-
```typescript
|
|
70
|
-
interface ClientWhitelabelService extends InitializedService {
|
|
71
|
-
loadWhitelabeling: (entityId: string) => Promise<ProvidedWLSet>
|
|
72
|
-
getEntityBranding: (entityId: string) => Promise<EntityBranding | null>
|
|
73
|
-
getCachedData: (entityId: string) => ProvidedWLSet | null
|
|
74
|
-
clearCache: (entityId?: string) => void
|
|
75
|
-
}
|
|
12
|
+
bun add @owlmeans/client-wl
|
|
76
13
|
```
|
|
77
14
|
|
|
78
|
-
**Methods:**
|
|
79
|
-
|
|
80
|
-
**`loadWhitelabeling(entityId: string): Promise<ProvidedWLSet>`**
|
|
81
|
-
- **Purpose**: Load complete whitelabeling data set for an entity
|
|
82
|
-
- **Parameters**: `entityId` - Unique entity identifier
|
|
83
|
-
- **Returns**: Promise resolving to whitelabeling data from all providers
|
|
84
|
-
- **Behavior**: Fetches data from server, applies caching strategy
|
|
85
|
-
|
|
86
|
-
**`getEntityBranding(entityId: string): Promise<EntityBranding | null>`**
|
|
87
|
-
- **Purpose**: Get consolidated branding information for an entity
|
|
88
|
-
- **Parameters**: `entityId` - Entity identifier
|
|
89
|
-
- **Returns**: Promise resolving to entity branding data or null
|
|
90
|
-
- **Behavior**: Combines data from multiple providers into unified branding object
|
|
91
|
-
|
|
92
|
-
**`getCachedData(entityId: string): ProvidedWLSet | null`**
|
|
93
|
-
- **Purpose**: Retrieve cached whitelabeling data if available
|
|
94
|
-
- **Parameters**: `entityId` - Entity identifier
|
|
95
|
-
- **Returns**: Cached data or null if not cached/expired
|
|
96
|
-
- **Behavior**: Checks cache without making network requests
|
|
97
|
-
|
|
98
|
-
**`clearCache(entityId?: string): void`**
|
|
99
|
-
- **Purpose**: Clear cached whitelabeling data
|
|
100
|
-
- **Parameters**: `entityId` - Optional specific entity to clear (clears all if omitted)
|
|
101
|
-
- **Behavior**: Removes cached data, forcing fresh loads on next request
|
|
102
|
-
|
|
103
|
-
#### `EntityBranding`
|
|
104
|
-
|
|
105
|
-
Consolidated branding information for an entity.
|
|
106
|
-
|
|
107
|
-
```typescript
|
|
108
|
-
interface EntityBranding {
|
|
109
|
-
entityId: string
|
|
110
|
-
companyInfo?: CompanyInfo
|
|
111
|
-
styles?: CustomStyles
|
|
112
|
-
media?: CustomMedia
|
|
113
|
-
urls?: CustomUrls
|
|
114
|
-
metadata?: Record<string, any>
|
|
115
|
-
lastUpdated: Date
|
|
116
|
-
}
|
|
117
|
-
```
|
|
118
|
-
|
|
119
|
-
#### `WhitelabelConfig`
|
|
120
|
-
|
|
121
|
-
Configuration interface for whitelabeling services.
|
|
122
|
-
|
|
123
|
-
```typescript
|
|
124
|
-
interface WhitelabelConfig {
|
|
125
|
-
apiEndpoint?: string // Whitelabeling API endpoint
|
|
126
|
-
cacheTimeout?: number // Cache timeout in milliseconds
|
|
127
|
-
retryAttempts?: number // Number of retry attempts for failed requests
|
|
128
|
-
fallbackEntityId?: string // Fallback entity ID for missing configurations
|
|
129
|
-
enableCaching?: boolean // Enable/disable client-side caching
|
|
130
|
-
}
|
|
131
|
-
```
|
|
132
|
-
|
|
133
|
-
### Utility Functions
|
|
134
|
-
|
|
135
|
-
#### Entity Resolution
|
|
136
|
-
|
|
137
|
-
```typescript
|
|
138
|
-
// Utility functions for entity identification
|
|
139
|
-
export const resolveEntityFromDomain = (domain: string): string | null => {
|
|
140
|
-
// Extract entity from domain/subdomain patterns
|
|
141
|
-
const subdomainMatch = domain.match(/^([a-zA-Z0-9-]+)\./)
|
|
142
|
-
return subdomainMatch ? subdomainMatch[1] : null
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
export const resolveEntityFromPath = (path: string): string | null => {
|
|
146
|
-
// Extract entity from URL path patterns
|
|
147
|
-
const pathMatch = path.match(/^\/([a-zA-Z0-9-]+)\//)
|
|
148
|
-
return pathMatch ? pathMatch[1] : null
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
export const resolveEntityFromQuery = (queryParams: URLSearchParams): string | null => {
|
|
152
|
-
// Extract entity from query parameters
|
|
153
|
-
return queryParams.get('entity') || queryParams.get('tenant')
|
|
154
|
-
}
|
|
155
|
-
```
|
|
156
|
-
|
|
157
|
-
#### Data Transformation
|
|
158
|
-
|
|
159
|
-
```typescript
|
|
160
|
-
// Utility functions for data transformation
|
|
161
|
-
export const transformWhitelabelData = (
|
|
162
|
-
rawData: ProvidedWLSet
|
|
163
|
-
): EntityBranding => {
|
|
164
|
-
return {
|
|
165
|
-
entityId: rawData.entityId,
|
|
166
|
-
companyInfo: extractCompanyInfo(rawData),
|
|
167
|
-
styles: extractCustomStyles(rawData),
|
|
168
|
-
media: extractCustomMedia(rawData),
|
|
169
|
-
urls: extractCustomUrls(rawData),
|
|
170
|
-
metadata: extractMetadata(rawData),
|
|
171
|
-
lastUpdated: new Date()
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
export const mergeWhitelabelData = (
|
|
176
|
-
...dataSets: Partial<EntityBranding>[]
|
|
177
|
-
): EntityBranding => {
|
|
178
|
-
// Merge multiple whitelabeling data sets with priority
|
|
179
|
-
return dataSets.reduce((merged, data) => ({
|
|
180
|
-
...merged,
|
|
181
|
-
...data,
|
|
182
|
-
metadata: { ...merged.metadata, ...data.metadata }
|
|
183
|
-
}), {} as EntityBranding)
|
|
184
|
-
}
|
|
185
|
-
```
|
|
186
|
-
|
|
187
|
-
### Error Types
|
|
188
|
-
|
|
189
|
-
#### Whitelabeling-specific Errors
|
|
190
|
-
|
|
191
|
-
```typescript
|
|
192
|
-
export class WhitelabelingError extends Error {
|
|
193
|
-
constructor(
|
|
194
|
-
message: string,
|
|
195
|
-
public entityId: string,
|
|
196
|
-
public code: string,
|
|
197
|
-
public originalError?: Error
|
|
198
|
-
) {
|
|
199
|
-
super(message)
|
|
200
|
-
this.name = 'WhitelabelingError'
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
export class EntityNotFoundError extends WhitelabelingError {
|
|
205
|
-
constructor(entityId: string) {
|
|
206
|
-
super(`Entity not found: ${entityId}`, entityId, 'ENTITY_NOT_FOUND')
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
export class WhitelabelingDataError extends WhitelabelingError {
|
|
211
|
-
constructor(entityId: string, dataType: string, originalError?: Error) {
|
|
212
|
-
super(
|
|
213
|
-
`Failed to load ${dataType} data for entity: ${entityId}`,
|
|
214
|
-
entityId,
|
|
215
|
-
'DATA_LOAD_ERROR',
|
|
216
|
-
originalError
|
|
217
|
-
)
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
```
|
|
221
|
-
|
|
222
|
-
## Usage Examples
|
|
223
|
-
|
|
224
|
-
### Basic Service Implementation
|
|
225
|
-
|
|
226
|
-
```typescript
|
|
227
|
-
import { createService } from '@owlmeans/context'
|
|
228
|
-
import type { ClientWhitelabelService, EntityBranding, ProvidedWLSet } from '@owlmeans/client-wl'
|
|
229
|
-
|
|
230
|
-
// Basic implementation example (platform-specific packages provide concrete implementations)
|
|
231
|
-
const createWhitelabelService = (alias: string = 'whitelabel-service'): ClientWhitelabelService => {
|
|
232
|
-
const cache = new Map<string, { data: ProvidedWLSet; timestamp: number }>()
|
|
233
|
-
const CACHE_TTL = 5 * 60 * 1000 // 5 minutes
|
|
234
|
-
|
|
235
|
-
return createService<ClientWhitelabelService>(alias, {
|
|
236
|
-
async loadWhitelabeling(entityId: string): Promise<ProvidedWLSet> {
|
|
237
|
-
// Check cache first
|
|
238
|
-
const cached = this.getCachedData(entityId)
|
|
239
|
-
if (cached) {
|
|
240
|
-
return cached
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
try {
|
|
244
|
-
// Make API request (implementation would vary by platform)
|
|
245
|
-
const response = await fetch(`/api/whitelabel/${entityId}`)
|
|
246
|
-
if (!response.ok) {
|
|
247
|
-
throw new EntityNotFoundError(entityId)
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
const data: ProvidedWLSet = await response.json()
|
|
251
|
-
|
|
252
|
-
// Cache the result
|
|
253
|
-
cache.set(entityId, {
|
|
254
|
-
data,
|
|
255
|
-
timestamp: Date.now()
|
|
256
|
-
})
|
|
257
|
-
|
|
258
|
-
return data
|
|
259
|
-
} catch (error) {
|
|
260
|
-
throw new WhitelabelingDataError(entityId, 'complete', error as Error)
|
|
261
|
-
}
|
|
262
|
-
},
|
|
263
|
-
|
|
264
|
-
async getEntityBranding(entityId: string): Promise<EntityBranding | null> {
|
|
265
|
-
try {
|
|
266
|
-
const whitelabelData = await this.loadWhitelabeling(entityId)
|
|
267
|
-
return transformWhitelabelData(whitelabelData)
|
|
268
|
-
} catch (error) {
|
|
269
|
-
console.error('Failed to get entity branding:', error)
|
|
270
|
-
return null
|
|
271
|
-
}
|
|
272
|
-
},
|
|
273
|
-
|
|
274
|
-
getCachedData(entityId: string): ProvidedWLSet | null {
|
|
275
|
-
const cached = cache.get(entityId)
|
|
276
|
-
if (!cached) return null
|
|
277
|
-
|
|
278
|
-
// Check if cache is still valid
|
|
279
|
-
if (Date.now() - cached.timestamp > CACHE_TTL) {
|
|
280
|
-
cache.delete(entityId)
|
|
281
|
-
return null
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
return cached.data
|
|
285
|
-
},
|
|
286
|
-
|
|
287
|
-
clearCache(entityId?: string): void {
|
|
288
|
-
if (entityId) {
|
|
289
|
-
cache.delete(entityId)
|
|
290
|
-
} else {
|
|
291
|
-
cache.clear()
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
}, (service) => async () => {
|
|
295
|
-
// Service initialization
|
|
296
|
-
service.initialized = true
|
|
297
|
-
})
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
// Register with context
|
|
301
|
-
const context = makeClientContext(config)
|
|
302
|
-
const whitelabelService = createWhitelabelService()
|
|
303
|
-
context.registerService(whitelabelService)
|
|
304
|
-
```
|
|
305
|
-
|
|
306
|
-
### Entity Resolution Strategies
|
|
307
|
-
|
|
308
|
-
```typescript
|
|
309
|
-
// Multiple strategies for entity resolution
|
|
310
|
-
class EntityResolver {
|
|
311
|
-
private strategies: Array<(context: any) => string | null> = []
|
|
312
|
-
|
|
313
|
-
constructor() {
|
|
314
|
-
// Add resolution strategies in priority order
|
|
315
|
-
this.addStrategy(this.resolveFromUrl)
|
|
316
|
-
this.addStrategy(this.resolveFromStorage)
|
|
317
|
-
this.addStrategy(this.resolveFromConfig)
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
addStrategy(strategy: (context: any) => string | null): void {
|
|
321
|
-
this.strategies.push(strategy)
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
resolve(context: any): string | null {
|
|
325
|
-
for (const strategy of this.strategies) {
|
|
326
|
-
const entityId = strategy(context)
|
|
327
|
-
if (entityId) {
|
|
328
|
-
return entityId
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
return null
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
private resolveFromUrl(context: any): string | null {
|
|
335
|
-
if (typeof window === 'undefined') return null
|
|
336
|
-
|
|
337
|
-
const url = new URL(window.location.href)
|
|
338
|
-
|
|
339
|
-
// Try subdomain
|
|
340
|
-
const subdomain = resolveEntityFromDomain(url.hostname)
|
|
341
|
-
if (subdomain && subdomain !== 'www') {
|
|
342
|
-
return subdomain
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
// Try path
|
|
346
|
-
const pathEntity = resolveEntityFromPath(url.pathname)
|
|
347
|
-
if (pathEntity) {
|
|
348
|
-
return pathEntity
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
// Try query parameters
|
|
352
|
-
return resolveEntityFromQuery(url.searchParams)
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
private resolveFromStorage(context: any): string | null {
|
|
356
|
-
if (typeof localStorage === 'undefined') return null
|
|
357
|
-
|
|
358
|
-
return localStorage.getItem('selectedEntity')
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
private resolveFromConfig(context: any): string | null {
|
|
362
|
-
return context.cfg?.defaultEntityId || null
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
// Usage
|
|
367
|
-
const resolver = new EntityResolver()
|
|
368
|
-
const entityId = resolver.resolve(context)
|
|
369
|
-
|
|
370
|
-
if (entityId) {
|
|
371
|
-
const whitelabelService = context.service<ClientWhitelabelService>('whitelabel-service')
|
|
372
|
-
const branding = await whitelabelService.getEntityBranding(entityId)
|
|
373
|
-
}
|
|
374
|
-
```
|
|
375
|
-
|
|
376
|
-
### Multi-entity Management
|
|
377
|
-
|
|
378
|
-
```typescript
|
|
379
|
-
// Service for managing multiple entities
|
|
380
|
-
class MultiEntityWhitelabelManager {
|
|
381
|
-
private whitelabelService: ClientWhitelabelService
|
|
382
|
-
private activeEntityId: string | null = null
|
|
383
|
-
private listeners: Array<(entityId: string, branding: EntityBranding | null) => void> = []
|
|
384
|
-
|
|
385
|
-
constructor(whitelabelService: ClientWhitelabelService) {
|
|
386
|
-
this.whitelabelService = whitelabelService
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
async setActiveEntity(entityId: string): Promise<void> {
|
|
390
|
-
if (this.activeEntityId === entityId) return
|
|
391
|
-
|
|
392
|
-
this.activeEntityId = entityId
|
|
393
|
-
|
|
394
|
-
try {
|
|
395
|
-
const branding = await this.whitelabelService.getEntityBranding(entityId)
|
|
396
|
-
this.notifyListeners(entityId, branding)
|
|
397
|
-
} catch (error) {
|
|
398
|
-
console.error('Failed to load branding for entity:', entityId, error)
|
|
399
|
-
this.notifyListeners(entityId, null)
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
getActiveEntity(): string | null {
|
|
404
|
-
return this.activeEntityId
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
async getActiveBranding(): Promise<EntityBranding | null> {
|
|
408
|
-
if (!this.activeEntityId) return null
|
|
409
|
-
|
|
410
|
-
return this.whitelabelService.getEntityBranding(this.activeEntityId)
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
addListener(callback: (entityId: string, branding: EntityBranding | null) => void): void {
|
|
414
|
-
this.listeners.push(callback)
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
removeListener(callback: (entityId: string, branding: EntityBranding | null) => void): void {
|
|
418
|
-
const index = this.listeners.indexOf(callback)
|
|
419
|
-
if (index >= 0) {
|
|
420
|
-
this.listeners.splice(index, 1)
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
private notifyListeners(entityId: string, branding: EntityBranding | null): void {
|
|
425
|
-
this.listeners.forEach(listener => {
|
|
426
|
-
try {
|
|
427
|
-
listener(entityId, branding)
|
|
428
|
-
} catch (error) {
|
|
429
|
-
console.error('Error in whitelabel listener:', error)
|
|
430
|
-
}
|
|
431
|
-
})
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
async preloadEntities(entityIds: string[]): Promise<void> {
|
|
435
|
-
await Promise.all(
|
|
436
|
-
entityIds.map(async (entityId) => {
|
|
437
|
-
try {
|
|
438
|
-
await this.whitelabelService.loadWhitelabeling(entityId)
|
|
439
|
-
} catch (error) {
|
|
440
|
-
console.warn(`Failed to preload entity ${entityId}:`, error)
|
|
441
|
-
}
|
|
442
|
-
})
|
|
443
|
-
)
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
clearCache(entityId?: string): void {
|
|
447
|
-
this.whitelabelService.clearCache(entityId)
|
|
448
|
-
}
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
// Usage
|
|
452
|
-
const manager = new MultiEntityWhitelabelManager(whitelabelService)
|
|
453
|
-
|
|
454
|
-
// Listen for entity changes
|
|
455
|
-
manager.addListener((entityId, branding) => {
|
|
456
|
-
console.log('Entity changed:', entityId, branding)
|
|
457
|
-
// Update UI with new branding
|
|
458
|
-
if (branding) {
|
|
459
|
-
applyBrandingToApp(branding)
|
|
460
|
-
}
|
|
461
|
-
})
|
|
462
|
-
|
|
463
|
-
// Set active entity
|
|
464
|
-
await manager.setActiveEntity('company-123')
|
|
465
|
-
|
|
466
|
-
// Preload multiple entities for better performance
|
|
467
|
-
await manager.preloadEntities(['company-123', 'company-456', 'company-789'])
|
|
468
|
-
```
|
|
469
|
-
|
|
470
|
-
### Configuration and Setup
|
|
471
|
-
|
|
472
|
-
```typescript
|
|
473
|
-
// Configuration for whitelabeling
|
|
474
|
-
interface AppConfigWithWhitelabel extends ClientConfig {
|
|
475
|
-
whitelabel: WhitelabelConfig
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
const appConfig: AppConfigWithWhitelabel = {
|
|
479
|
-
service: 'my-app',
|
|
480
|
-
type: AppType.Frontend,
|
|
481
|
-
layer: Layer.Service,
|
|
482
|
-
whitelabel: {
|
|
483
|
-
apiEndpoint: '/api/whitelabel',
|
|
484
|
-
cacheTimeout: 5 * 60 * 1000, // 5 minutes
|
|
485
|
-
retryAttempts: 3,
|
|
486
|
-
fallbackEntityId: 'default',
|
|
487
|
-
enableCaching: true
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
// Initialize with whitelabeling support
|
|
492
|
-
const context = makeClientContext(appConfig)
|
|
493
|
-
|
|
494
|
-
// Register whitelabeling service
|
|
495
|
-
const whitelabelService = createWhitelabelService('main-whitelabel')
|
|
496
|
-
context.registerService(whitelabelService)
|
|
497
|
-
|
|
498
|
-
// Create entity manager
|
|
499
|
-
const entityManager = new MultiEntityWhitelabelManager(whitelabelService)
|
|
500
|
-
|
|
501
|
-
// Auto-resolve entity on startup
|
|
502
|
-
const entityResolver = new EntityResolver()
|
|
503
|
-
const initialEntity = entityResolver.resolve(context)
|
|
504
|
-
|
|
505
|
-
if (initialEntity) {
|
|
506
|
-
await entityManager.setActiveEntity(initialEntity)
|
|
507
|
-
}
|
|
508
|
-
```
|
|
509
|
-
|
|
510
|
-
### Data Validation and Sanitization
|
|
511
|
-
|
|
512
|
-
```typescript
|
|
513
|
-
import Ajv from 'ajv'
|
|
514
|
-
import addFormats from 'ajv-formats'
|
|
515
|
-
import { CompanyInfoSchema, CustomStylesSchema, CustomMediaSchema } from '@owlmeans/wled'
|
|
516
|
-
|
|
517
|
-
// Validation service for whitelabeling data
|
|
518
|
-
class WhitelabelDataValidator {
|
|
519
|
-
private ajv: Ajv
|
|
520
|
-
|
|
521
|
-
constructor() {
|
|
522
|
-
this.ajv = new Ajv({ allErrors: true })
|
|
523
|
-
addFormats(this.ajv)
|
|
524
|
-
|
|
525
|
-
// Add schemas
|
|
526
|
-
this.ajv.addSchema(CompanyInfoSchema, 'CompanyInfo')
|
|
527
|
-
this.ajv.addSchema(CustomStylesSchema, 'CustomStyles')
|
|
528
|
-
this.ajv.addSchema(CustomMediaSchema, 'CustomMedia')
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
validateCompanyInfo(data: any): { valid: boolean; errors?: string[] } {
|
|
532
|
-
const validate = this.ajv.getSchema('CompanyInfo')
|
|
533
|
-
const valid = validate!(data)
|
|
534
|
-
|
|
535
|
-
return {
|
|
536
|
-
valid,
|
|
537
|
-
errors: valid ? undefined : validate!.errors?.map(err => err.message) || []
|
|
538
|
-
}
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
validateCustomStyles(data: any): { valid: boolean; errors?: string[] } {
|
|
542
|
-
const validate = this.ajv.getSchema('CustomStyles')
|
|
543
|
-
const valid = validate!(data)
|
|
544
|
-
|
|
545
|
-
return {
|
|
546
|
-
valid,
|
|
547
|
-
errors: valid ? undefined : validate!.errors?.map(err => err.message) || []
|
|
548
|
-
}
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
sanitizeWhitelabelData(data: ProvidedWLSet): ProvidedWLSet {
|
|
552
|
-
const sanitized: ProvidedWLSet = {}
|
|
553
|
-
|
|
554
|
-
Object.keys(data).forEach(key => {
|
|
555
|
-
const providerData = data[key]
|
|
556
|
-
|
|
557
|
-
try {
|
|
558
|
-
// Validate based on data type
|
|
559
|
-
switch (providerData.type) {
|
|
560
|
-
case 'company':
|
|
561
|
-
const companyValidation = this.validateCompanyInfo(providerData)
|
|
562
|
-
if (companyValidation.valid) {
|
|
563
|
-
sanitized[key] = providerData
|
|
564
|
-
} else {
|
|
565
|
-
console.warn(`Invalid company data for ${key}:`, companyValidation.errors)
|
|
566
|
-
}
|
|
567
|
-
break
|
|
568
|
-
|
|
569
|
-
case 'styles':
|
|
570
|
-
const stylesValidation = this.validateCustomStyles(providerData)
|
|
571
|
-
if (stylesValidation.valid) {
|
|
572
|
-
sanitized[key] = providerData
|
|
573
|
-
} else {
|
|
574
|
-
console.warn(`Invalid styles data for ${key}:`, stylesValidation.errors)
|
|
575
|
-
}
|
|
576
|
-
break
|
|
577
|
-
|
|
578
|
-
default:
|
|
579
|
-
// Include other data types without validation
|
|
580
|
-
sanitized[key] = providerData
|
|
581
|
-
}
|
|
582
|
-
} catch (error) {
|
|
583
|
-
console.error(`Error validating data for ${key}:`, error)
|
|
584
|
-
}
|
|
585
|
-
})
|
|
586
|
-
|
|
587
|
-
return sanitized
|
|
588
|
-
}
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
// Usage with service
|
|
592
|
-
const validator = new WhitelabelDataValidator()
|
|
593
|
-
|
|
594
|
-
const validatedService = createService<ClientWhitelabelService>('validated-whitelabel', {
|
|
595
|
-
async loadWhitelabeling(entityId: string): Promise<ProvidedWLSet> {
|
|
596
|
-
const rawData = await baseService.loadWhitelabeling(entityId)
|
|
597
|
-
return validator.sanitizeWhitelabelData(rawData)
|
|
598
|
-
}
|
|
599
|
-
// ... other methods
|
|
600
|
-
})
|
|
601
|
-
```
|
|
602
|
-
|
|
603
|
-
## Integration Patterns
|
|
604
|
-
|
|
605
|
-
### Context Integration
|
|
606
|
-
```typescript
|
|
607
|
-
import { makeClientContext } from '@owlmeans/client-context'
|
|
608
|
-
|
|
609
|
-
const context = makeClientContext(config)
|
|
610
|
-
const whitelabelService = context.service<ClientWhitelabelService>('whitelabel-service')
|
|
611
|
-
```
|
|
612
|
-
|
|
613
|
-
### Module System Integration
|
|
614
|
-
```typescript
|
|
615
|
-
// Whitelabeling typically integrates at the service level
|
|
616
|
-
// Modules are handled by platform-specific implementations
|
|
617
|
-
```
|
|
618
|
-
|
|
619
|
-
### Error Handling Integration
|
|
620
|
-
```typescript
|
|
621
|
-
import { ResilientError } from '@owlmeans/error'
|
|
622
|
-
|
|
623
|
-
// Integrate with OwlMeans error handling
|
|
624
|
-
try {
|
|
625
|
-
const branding = await whitelabelService.getEntityBranding(entityId)
|
|
626
|
-
} catch (error) {
|
|
627
|
-
if (error instanceof EntityNotFoundError) {
|
|
628
|
-
// Handle entity not found
|
|
629
|
-
console.warn('Entity not found, using defaults')
|
|
630
|
-
} else {
|
|
631
|
-
throw new ResilientError('WHITELABEL_ERROR', 'Failed to load whitelabeling', {
|
|
632
|
-
originalError: error.message,
|
|
633
|
-
entityId
|
|
634
|
-
})
|
|
635
|
-
}
|
|
636
|
-
}
|
|
637
|
-
```
|
|
638
|
-
|
|
639
|
-
## Best Practices
|
|
640
|
-
|
|
641
|
-
1. **Caching Strategy**: Implement efficient caching with appropriate TTL values
|
|
642
|
-
2. **Error Handling**: Provide graceful fallbacks for missing or invalid data
|
|
643
|
-
3. **Validation**: Always validate whitelabeling data before application
|
|
644
|
-
4. **Performance**: Use lazy loading and preloading strategies appropriately
|
|
645
|
-
5. **Entity Resolution**: Implement multiple entity resolution strategies with fallbacks
|
|
646
|
-
6. **Type Safety**: Leverage TypeScript for compile-time validation
|
|
647
|
-
7. **Testing**: Test with various entity configurations and error scenarios
|
|
648
|
-
|
|
649
15
|
## Related Packages
|
|
650
16
|
|
|
651
|
-
-
|
|
652
|
-
-
|
|
653
|
-
- **@owlmeans/server-wl**: Server-side whitelabeling implementation
|
|
654
|
-
- **@owlmeans/context**: Context management and service registration
|
|
655
|
-
- **@owlmeans/client**: Base client functionality and services
|
|
656
|
-
|
|
657
|
-
## TypeScript Support
|
|
658
|
-
|
|
659
|
-
This package is written in TypeScript and provides full type safety:
|
|
660
|
-
|
|
661
|
-
```typescript
|
|
662
|
-
import type {
|
|
663
|
-
ClientWhitelabelService,
|
|
664
|
-
EntityBranding,
|
|
665
|
-
ProvidedWLSet,
|
|
666
|
-
WhitelabelConfig
|
|
667
|
-
} from '@owlmeans/client-wl'
|
|
668
|
-
|
|
669
|
-
const service: ClientWhitelabelService = createWhitelabelService()
|
|
670
|
-
const branding: EntityBranding = await service.getEntityBranding(entityId)
|
|
671
|
-
```
|
|
17
|
+
- [`@owlmeans/server-wl`](../server-wl) — server-side whitelabeling API
|
|
18
|
+
- [`@owlmeans/web-wl`](../web-wl) — React whitelabeling components (MUI)
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@owlmeans/client-wl",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
|
+
"license": "MIT",
|
|
4
5
|
"type": "module",
|
|
5
6
|
"scripts": {
|
|
6
7
|
"build": "tsc -b",
|
|
@@ -23,9 +24,10 @@
|
|
|
23
24
|
"react": "*"
|
|
24
25
|
},
|
|
25
26
|
"devDependencies": {
|
|
27
|
+
"@owlmeans/dep-config": "workspace:*",
|
|
26
28
|
"@types/react": "^19.2.7",
|
|
27
29
|
"nodemon": "^3.1.11",
|
|
28
|
-
"typescript": "^
|
|
30
|
+
"typescript": "^6.0.2"
|
|
29
31
|
},
|
|
30
32
|
"publishConfig": {
|
|
31
33
|
"access": "public"
|
package/tsconfig.json
CHANGED
|
@@ -1,16 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"extends": [
|
|
3
|
-
"
|
|
4
|
-
"
|
|
3
|
+
"@owlmeans/dep-config/tsconfig.base.json",
|
|
4
|
+
"@owlmeans/dep-config/tsconfig.react.json"
|
|
5
5
|
],
|
|
6
6
|
"compilerOptions": {
|
|
7
|
-
"rootDir": "./src/",
|
|
8
|
-
"outDir": "./build/"
|
|
9
|
-
"moduleResolution": "Bundler",
|
|
7
|
+
"rootDir": "./src/",
|
|
8
|
+
"outDir": "./build/"
|
|
10
9
|
},
|
|
11
|
-
"exclude": [
|
|
12
|
-
"./dist/**/*",
|
|
13
|
-
"./build/**/*",
|
|
14
|
-
"./*.ts"
|
|
15
|
-
]
|
|
10
|
+
"exclude": ["./dist/**/*", "./build/**/*", "./*.ts"]
|
|
16
11
|
}
|