@umituz/react-native-ai-fal-provider 3.2.51 → 3.2.53
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/package.json +1 -4
- package/src/domain/services/ErrorClassificationService.ts +201 -0
- package/src/domain/services/ImageProcessingService.ts +89 -0
- package/src/domain/services/PricingService.ts +101 -0
- package/src/domain/services/ValidationService.ts +132 -0
- package/src/domain/services/index.ts +13 -0
- package/src/index.ts +24 -22
- package/src/infrastructure/utils/fal-error-handler.util.ts +14 -146
- package/src/infrastructure/utils/fal-storage.util.ts +2 -3
- package/src/infrastructure/utils/helpers/index.ts +2 -4
- package/src/infrastructure/utils/image-helpers.util.ts +13 -27
- package/src/infrastructure/utils/input-preprocessor.util.ts +6 -12
- package/src/infrastructure/utils/input-validator.util.ts +17 -156
- package/src/infrastructure/utils/pricing/fal-pricing.util.ts +26 -53
- package/src/infrastructure/utils/type-guards/index.ts +2 -2
- package/src/shared/helpers.ts +149 -0
- package/src/shared/index.ts +8 -0
- package/src/shared/type-guards.ts +122 -0
- package/src/shared/validators.ts +281 -0
- package/src/infrastructure/utils/helpers/calculation-helpers.util.ts +0 -168
- package/src/infrastructure/utils/helpers/error-helpers.util.ts +0 -65
- package/src/infrastructure/utils/helpers/object-helpers.util.ts +0 -44
- package/src/infrastructure/utils/helpers/timing-helpers.util.ts +0 -11
- package/src/infrastructure/utils/type-guards/model-type-guards.util.ts +0 -56
- package/src/infrastructure/utils/type-guards/validation-guards.util.ts +0 -101
- package/src/infrastructure/utils/validators/data-uri-validator.util.ts +0 -91
- package/src/infrastructure/utils/validators/string-validator.util.ts +0 -64
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-ai-fal-provider",
|
|
3
|
-
"version": "3.2.
|
|
3
|
+
"version": "3.2.53",
|
|
4
4
|
"description": "FAL AI provider for React Native - implements IAIProvider interface for unified AI generation",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"types": "./src/index.ts",
|
|
@@ -47,9 +47,6 @@
|
|
|
47
47
|
"@gorhom/bottom-sheet": "^5.2.8",
|
|
48
48
|
"@react-native-async-storage/async-storage": "^2.2.0",
|
|
49
49
|
"@react-native-community/slider": "^5.1.1",
|
|
50
|
-
"@react-navigation/bottom-tabs": "^7.9.0",
|
|
51
|
-
"@react-navigation/native": "^7.1.26",
|
|
52
|
-
"@react-navigation/stack": "^7.6.13",
|
|
53
50
|
"@tanstack/query-async-storage-persister": "^5.66.7",
|
|
54
51
|
"@tanstack/react-query": "^5.66.7",
|
|
55
52
|
"@tanstack/react-query-persist-client": "^5.66.7",
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Domain Service: Error Classification
|
|
3
|
+
* Pure business logic for categorizing and extracting error information
|
|
4
|
+
* No infrastructure dependencies (except for @fal-ai/client types)
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { ApiError, ValidationError } from "@fal-ai/client";
|
|
8
|
+
import type { FalErrorInfo, FalErrorCategory } from "../entities/error.types";
|
|
9
|
+
import { FalErrorType } from "../entities/error.types";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* HTTP status code to error type mapping
|
|
13
|
+
*/
|
|
14
|
+
const STATUS_TO_ERROR_TYPE: Record<number, FalErrorType> = {
|
|
15
|
+
400: FalErrorType.VALIDATION,
|
|
16
|
+
401: FalErrorType.AUTHENTICATION,
|
|
17
|
+
402: FalErrorType.QUOTA_EXCEEDED,
|
|
18
|
+
403: FalErrorType.AUTHENTICATION,
|
|
19
|
+
404: FalErrorType.MODEL_NOT_FOUND,
|
|
20
|
+
422: FalErrorType.VALIDATION,
|
|
21
|
+
429: FalErrorType.RATE_LIMIT,
|
|
22
|
+
500: FalErrorType.API_ERROR,
|
|
23
|
+
502: FalErrorType.API_ERROR,
|
|
24
|
+
503: FalErrorType.API_ERROR,
|
|
25
|
+
504: FalErrorType.API_ERROR,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const RETRYABLE_TYPES = new Set<FalErrorType>([
|
|
29
|
+
FalErrorType.NETWORK,
|
|
30
|
+
FalErrorType.TIMEOUT,
|
|
31
|
+
FalErrorType.RATE_LIMIT,
|
|
32
|
+
]);
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Message-based error type detection (for non-ApiError errors)
|
|
36
|
+
*/
|
|
37
|
+
const MESSAGE_PATTERNS: Array<{ type: FalErrorType; patterns: string[] }> = [
|
|
38
|
+
{
|
|
39
|
+
type: FalErrorType.NETWORK,
|
|
40
|
+
patterns: ["network", "fetch", "econnrefused", "enotfound", "etimedout"],
|
|
41
|
+
},
|
|
42
|
+
{ type: FalErrorType.TIMEOUT, patterns: ["timeout", "timed out"] },
|
|
43
|
+
{
|
|
44
|
+
type: FalErrorType.CONTENT_POLICY,
|
|
45
|
+
patterns: ["nsfw", "content_policy", "content policy", "policy violation"],
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
type: FalErrorType.IMAGE_TOO_SMALL,
|
|
49
|
+
patterns: [
|
|
50
|
+
"image_too_small",
|
|
51
|
+
"image dimensions are too small",
|
|
52
|
+
"minimum dimensions",
|
|
53
|
+
],
|
|
54
|
+
},
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Domain service for error classification
|
|
59
|
+
* Single responsibility: categorize errors and extract user-friendly messages
|
|
60
|
+
*/
|
|
61
|
+
export class ErrorClassificationService {
|
|
62
|
+
/**
|
|
63
|
+
* Categorize error using @fal-ai/client error types
|
|
64
|
+
* Priority: ApiError status code > message pattern matching
|
|
65
|
+
*
|
|
66
|
+
* @param error - Error to categorize
|
|
67
|
+
* @returns Error category with type, message key, and retryable flag
|
|
68
|
+
*/
|
|
69
|
+
static categorizeError(error: unknown): FalErrorCategory {
|
|
70
|
+
// 1. ApiError (includes ValidationError) - use HTTP status code
|
|
71
|
+
if (error instanceof ApiError) {
|
|
72
|
+
const typeFromStatus = STATUS_TO_ERROR_TYPE[error.status];
|
|
73
|
+
if (typeFromStatus) {
|
|
74
|
+
return {
|
|
75
|
+
type: typeFromStatus,
|
|
76
|
+
messageKey: typeFromStatus,
|
|
77
|
+
retryable: RETRYABLE_TYPES.has(typeFromStatus),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
// Unknown status code - still an API error
|
|
81
|
+
return {
|
|
82
|
+
type: FalErrorType.API_ERROR,
|
|
83
|
+
messageKey: "api_error",
|
|
84
|
+
retryable: false,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// 2. Standard Error - match message patterns
|
|
89
|
+
const message = (
|
|
90
|
+
error instanceof Error
|
|
91
|
+
? error.message
|
|
92
|
+
: error != null
|
|
93
|
+
? String(error)
|
|
94
|
+
: "unknown"
|
|
95
|
+
).toLowerCase();
|
|
96
|
+
|
|
97
|
+
for (const { type, patterns } of MESSAGE_PATTERNS) {
|
|
98
|
+
if (patterns.some((p) => message.includes(p))) {
|
|
99
|
+
return {
|
|
100
|
+
type,
|
|
101
|
+
messageKey: type,
|
|
102
|
+
retryable: RETRYABLE_TYPES.has(type),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// 3. No match - UNKNOWN, not retryable
|
|
108
|
+
return {
|
|
109
|
+
type: FalErrorType.UNKNOWN,
|
|
110
|
+
messageKey: "unknown",
|
|
111
|
+
retryable: false,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Extract user-readable message from error
|
|
117
|
+
* Uses @fal-ai/client types for structured extraction
|
|
118
|
+
*
|
|
119
|
+
* @param error - Error to extract message from
|
|
120
|
+
* @returns User-friendly error message
|
|
121
|
+
*/
|
|
122
|
+
static extractMessage(error: unknown): string {
|
|
123
|
+
// ValidationError - extract field-level messages
|
|
124
|
+
if (error instanceof ValidationError) {
|
|
125
|
+
const fieldErrors = error.fieldErrors;
|
|
126
|
+
if (Array.isArray(fieldErrors) && fieldErrors.length > 0) {
|
|
127
|
+
const messages = fieldErrors
|
|
128
|
+
.map(
|
|
129
|
+
(e) =>
|
|
130
|
+
e && typeof e === "object" && "msg" in e && typeof e.msg === "string"
|
|
131
|
+
? e.msg
|
|
132
|
+
: null
|
|
133
|
+
)
|
|
134
|
+
.filter((msg): msg is string => msg !== null);
|
|
135
|
+
if (messages.length > 0) return messages.join("; ");
|
|
136
|
+
}
|
|
137
|
+
return error.message;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ApiError - extract from body or message
|
|
141
|
+
if (error instanceof ApiError) {
|
|
142
|
+
const body = error.body as { detail?: Array<{ msg?: string }> } | undefined;
|
|
143
|
+
|
|
144
|
+
if (
|
|
145
|
+
body &&
|
|
146
|
+
body.detail &&
|
|
147
|
+
Array.isArray(body.detail) &&
|
|
148
|
+
body.detail.length > 0
|
|
149
|
+
) {
|
|
150
|
+
const firstItem = body.detail[0];
|
|
151
|
+
if (
|
|
152
|
+
firstItem &&
|
|
153
|
+
typeof firstItem === "object" &&
|
|
154
|
+
typeof firstItem.msg === "string" &&
|
|
155
|
+
firstItem.msg
|
|
156
|
+
) {
|
|
157
|
+
return firstItem.msg;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return error.message;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Standard Error
|
|
164
|
+
if (error instanceof Error) {
|
|
165
|
+
return error.message;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return error != null ? String(error) : "Unknown error";
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Map error to FalErrorInfo with full categorization
|
|
173
|
+
*
|
|
174
|
+
* @param error - Error to map
|
|
175
|
+
* @returns Complete error information
|
|
176
|
+
*/
|
|
177
|
+
static mapError(error: unknown): FalErrorInfo {
|
|
178
|
+
const category = this.categorizeError(error);
|
|
179
|
+
const message = this.extractMessage(error);
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
type: category.type,
|
|
183
|
+
messageKey: `fal.errors.${category.messageKey}`,
|
|
184
|
+
retryable: category.retryable,
|
|
185
|
+
originalError: message,
|
|
186
|
+
originalErrorName: error instanceof Error ? error.name : undefined,
|
|
187
|
+
stack: error instanceof Error ? error.stack : undefined,
|
|
188
|
+
statusCode: error instanceof ApiError ? error.status : undefined,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Check if error is retryable
|
|
194
|
+
*
|
|
195
|
+
* @param error - Error to check
|
|
196
|
+
* @returns True if error is retryable
|
|
197
|
+
*/
|
|
198
|
+
static isRetryable(error: unknown): boolean {
|
|
199
|
+
return this.categorizeError(error).retryable;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Domain Service: Image Processing
|
|
3
|
+
* Pure business logic for image data URI manipulation
|
|
4
|
+
* No infrastructure dependencies
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Domain service for image data processing
|
|
9
|
+
* Single responsibility: manipulate and validate image data URIs
|
|
10
|
+
*/
|
|
11
|
+
export class ImageProcessingService {
|
|
12
|
+
/**
|
|
13
|
+
* Format image as data URI if not already formatted
|
|
14
|
+
*
|
|
15
|
+
* @param base64 - Base64 string (with or without data URI prefix)
|
|
16
|
+
* @returns Data URI formatted string
|
|
17
|
+
*/
|
|
18
|
+
static formatImageDataUri(base64: string): string {
|
|
19
|
+
if (base64.startsWith("data:")) {
|
|
20
|
+
return base64;
|
|
21
|
+
}
|
|
22
|
+
return `data:image/jpeg;base64,${base64}`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Extract base64 from data URI
|
|
27
|
+
* Uses indexOf instead of split to handle edge cases where comma might appear in base64
|
|
28
|
+
*
|
|
29
|
+
* @param dataUri - Data URI string
|
|
30
|
+
* @returns Base64 content
|
|
31
|
+
* @throws Error if data URI format is invalid
|
|
32
|
+
*/
|
|
33
|
+
static extractBase64(dataUri: string): string {
|
|
34
|
+
if (!dataUri.startsWith("data:")) {
|
|
35
|
+
return dataUri;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Find the first comma which separates header from data
|
|
39
|
+
const commaIndex = dataUri.indexOf(",");
|
|
40
|
+
if (commaIndex === -1) {
|
|
41
|
+
throw new Error(
|
|
42
|
+
`Invalid data URI format (no comma separator): ${dataUri.substring(0, 50)}...`
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Extract everything after the first comma
|
|
47
|
+
const base64Part = dataUri.substring(commaIndex + 1);
|
|
48
|
+
|
|
49
|
+
if (!base64Part || base64Part.length === 0) {
|
|
50
|
+
throw new Error(
|
|
51
|
+
`Empty base64 data in URI: ${dataUri.substring(0, 50)}...`
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return base64Part;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Get file extension from data URI
|
|
60
|
+
*
|
|
61
|
+
* @param dataUri - Data URI string
|
|
62
|
+
* @returns File extension (e.g., "png", "jpeg") or null
|
|
63
|
+
*/
|
|
64
|
+
static getDataUriExtension(dataUri: string): string | null {
|
|
65
|
+
const match = dataUri.match(/^data:image\/(\w+);base64/);
|
|
66
|
+
return match ? match[1] : null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Extract MIME type from data URI
|
|
71
|
+
*
|
|
72
|
+
* @param dataUri - Data URI string
|
|
73
|
+
* @returns MIME type (e.g., "image/png") or null
|
|
74
|
+
*/
|
|
75
|
+
static extractMimeType(dataUri: string): string | null {
|
|
76
|
+
const match = dataUri.match(/^data:([^;,]+)/);
|
|
77
|
+
return match ? match[1] : null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Check if data URI is base64-encoded
|
|
82
|
+
*
|
|
83
|
+
* @param dataUri - Data URI string
|
|
84
|
+
* @returns True if base64-encoded
|
|
85
|
+
*/
|
|
86
|
+
static isBase64DataUri(dataUri: string): boolean {
|
|
87
|
+
return dataUri.startsWith("data:") && dataUri.includes("base64,");
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Domain Service: Pricing
|
|
3
|
+
* Pure business logic for credit calculations
|
|
4
|
+
* No infrastructure dependencies
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { VideoModelConfig } from "@umituz/react-native-ai-generation-content";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Pricing constants
|
|
11
|
+
* FAL AI pricing with markup and credit conversion
|
|
12
|
+
*/
|
|
13
|
+
const COSTS = {
|
|
14
|
+
VIDEO_480P_PER_SECOND: 0.05,
|
|
15
|
+
VIDEO_720P_PER_SECOND: 0.07,
|
|
16
|
+
IMAGE_INPUT: 0.002,
|
|
17
|
+
IMAGE: 0.03,
|
|
18
|
+
} as const;
|
|
19
|
+
|
|
20
|
+
const MARKUP = 3.5;
|
|
21
|
+
const CREDIT_PRICE = 0.1;
|
|
22
|
+
|
|
23
|
+
export type GenerationResolution = "480p" | "720p";
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Domain service for pricing calculations
|
|
27
|
+
* Single responsibility: calculate credit costs for generations
|
|
28
|
+
*/
|
|
29
|
+
export class PricingService {
|
|
30
|
+
/**
|
|
31
|
+
* Calculate credits for video generation
|
|
32
|
+
*
|
|
33
|
+
* @param duration - Video duration in seconds
|
|
34
|
+
* @param resolution - Video resolution (480p or 720p)
|
|
35
|
+
* @param hasImageInput - Whether an image input is provided
|
|
36
|
+
* @returns Number of credits required
|
|
37
|
+
*/
|
|
38
|
+
static calculateVideoCredits(
|
|
39
|
+
duration: number,
|
|
40
|
+
resolution: GenerationResolution,
|
|
41
|
+
hasImageInput: boolean = false
|
|
42
|
+
): number {
|
|
43
|
+
const costPerSec =
|
|
44
|
+
resolution === "480p"
|
|
45
|
+
? COSTS.VIDEO_480P_PER_SECOND
|
|
46
|
+
: COSTS.VIDEO_720P_PER_SECOND;
|
|
47
|
+
let cost = costPerSec * duration;
|
|
48
|
+
if (hasImageInput) cost += COSTS.IMAGE_INPUT;
|
|
49
|
+
return Math.max(1, Math.ceil((cost * MARKUP) / CREDIT_PRICE));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Calculate credits for image generation
|
|
54
|
+
*
|
|
55
|
+
* @returns Number of credits required
|
|
56
|
+
*/
|
|
57
|
+
static calculateImageCredits(): number {
|
|
58
|
+
return Math.max(1, Math.ceil((COSTS.IMAGE * MARKUP) / CREDIT_PRICE));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Calculate credits from video model config
|
|
63
|
+
*
|
|
64
|
+
* @param config - Video model configuration with pricing info
|
|
65
|
+
* @param duration - Video duration in seconds
|
|
66
|
+
* @param resolution - Video resolution string
|
|
67
|
+
* @returns Number of credits required
|
|
68
|
+
* @throws Error if config structure is invalid
|
|
69
|
+
*/
|
|
70
|
+
static calculateCreditsFromConfig(
|
|
71
|
+
config: VideoModelConfig,
|
|
72
|
+
duration: number,
|
|
73
|
+
resolution: string
|
|
74
|
+
): number {
|
|
75
|
+
// Validate config structure
|
|
76
|
+
if (
|
|
77
|
+
!config ||
|
|
78
|
+
typeof config !== "object" ||
|
|
79
|
+
!config.pricing ||
|
|
80
|
+
typeof config.pricing !== "object" ||
|
|
81
|
+
!config.pricing.costPerSecond ||
|
|
82
|
+
typeof config.pricing.costPerSecond !== "object"
|
|
83
|
+
) {
|
|
84
|
+
throw new Error(
|
|
85
|
+
"Invalid VideoModelConfig: pricing structure is missing or invalid"
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const costPerSecondMap = config.pricing.costPerSecond;
|
|
90
|
+
const costPerSec = costPerSecondMap[resolution] ?? 0;
|
|
91
|
+
|
|
92
|
+
if (typeof costPerSec !== "number" || costPerSec < 0) {
|
|
93
|
+
throw new Error(
|
|
94
|
+
`Invalid cost per second for resolution "${resolution}": must be a non-negative number`
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const cost = costPerSec * duration;
|
|
99
|
+
return Math.max(1, Math.ceil((cost * MARKUP) / CREDIT_PRICE));
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Domain Service: Validation
|
|
3
|
+
* Pure business logic for input validation
|
|
4
|
+
* No infrastructure dependencies
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { IMAGE_URL_FIELDS } from "../../infrastructure/utils/constants/image-fields.constants";
|
|
8
|
+
import {
|
|
9
|
+
isValidPrompt,
|
|
10
|
+
isNonEmptyString,
|
|
11
|
+
isValidAndSafeUrl,
|
|
12
|
+
isImageDataUri,
|
|
13
|
+
} from "../../shared/validators";
|
|
14
|
+
|
|
15
|
+
const SUSPICIOUS_PATTERNS = [
|
|
16
|
+
/<script/i,
|
|
17
|
+
/javascript:/i,
|
|
18
|
+
/on\w+\s*=/i,
|
|
19
|
+
/<iframe/i,
|
|
20
|
+
/<embed/i,
|
|
21
|
+
/<object/i,
|
|
22
|
+
/data:(?!image\/)/i,
|
|
23
|
+
/vbscript:/i,
|
|
24
|
+
] as const;
|
|
25
|
+
|
|
26
|
+
function hasSuspiciousContent(value: string): boolean {
|
|
27
|
+
return SUSPICIOUS_PATTERNS.some((pattern) => pattern.test(value));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface ValidationError {
|
|
31
|
+
field: string;
|
|
32
|
+
message: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export class InputValidationError extends Error {
|
|
36
|
+
public readonly errors: readonly ValidationError[];
|
|
37
|
+
|
|
38
|
+
constructor(errors: ValidationError[]) {
|
|
39
|
+
const message = errors.map((e) => `${e.field}: ${e.message}`).join("; ");
|
|
40
|
+
super(`Input validation failed: ${message}`);
|
|
41
|
+
this.name = "InputValidationError";
|
|
42
|
+
this.errors = errors;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Domain service for input validation
|
|
48
|
+
* Single responsibility: validate all inputs before API calls
|
|
49
|
+
*/
|
|
50
|
+
export class ValidationService {
|
|
51
|
+
/**
|
|
52
|
+
* Validate model and input parameters
|
|
53
|
+
*
|
|
54
|
+
* @param model - Model ID (unused in validation but kept for API compatibility)
|
|
55
|
+
* @param input - Input parameters to validate
|
|
56
|
+
* @throws InputValidationError if validation fails
|
|
57
|
+
*/
|
|
58
|
+
static validateInput(_model: string, input: Record<string, unknown>): void {
|
|
59
|
+
const errors: ValidationError[] = [];
|
|
60
|
+
|
|
61
|
+
// Validate input is not empty
|
|
62
|
+
if (!input || typeof input !== "object" || Object.keys(input).length === 0) {
|
|
63
|
+
errors.push({ field: "input", message: "Input must be a non-empty object" });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// BLOCK sync_mode:true
|
|
67
|
+
if (input.sync_mode === true) {
|
|
68
|
+
errors.push({
|
|
69
|
+
field: "sync_mode",
|
|
70
|
+
message:
|
|
71
|
+
"sync_mode:true is forbidden. It returns base64 data URIs instead of HTTPS CDN URLs.",
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Validate prompt
|
|
76
|
+
if (input.prompt !== undefined) {
|
|
77
|
+
if (!isValidPrompt(input.prompt)) {
|
|
78
|
+
errors.push({
|
|
79
|
+
field: "prompt",
|
|
80
|
+
message: "Prompt must be a non-empty string (max 5000 characters)",
|
|
81
|
+
});
|
|
82
|
+
} else if (typeof input.prompt === "string" && hasSuspiciousContent(input.prompt)) {
|
|
83
|
+
errors.push({
|
|
84
|
+
field: "prompt",
|
|
85
|
+
message: "Prompt contains potentially unsafe content",
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Validate negative_prompt
|
|
91
|
+
if (input.negative_prompt !== undefined) {
|
|
92
|
+
if (!isValidPrompt(input.negative_prompt)) {
|
|
93
|
+
errors.push({
|
|
94
|
+
field: "negative_prompt",
|
|
95
|
+
message: "Negative prompt must be a non-empty string (max 5000 characters)",
|
|
96
|
+
});
|
|
97
|
+
} else if (typeof input.negative_prompt === "string" && hasSuspiciousContent(input.negative_prompt)) {
|
|
98
|
+
errors.push({
|
|
99
|
+
field: "negative_prompt",
|
|
100
|
+
message: "Negative prompt contains potentially unsafe content",
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Validate all image_url fields
|
|
106
|
+
for (const field of IMAGE_URL_FIELDS) {
|
|
107
|
+
const value = input[field];
|
|
108
|
+
if (value !== undefined) {
|
|
109
|
+
if (typeof value !== "string") {
|
|
110
|
+
errors.push({
|
|
111
|
+
field,
|
|
112
|
+
message: `${field} must be a string`,
|
|
113
|
+
});
|
|
114
|
+
} else if (!isNonEmptyString(value)) {
|
|
115
|
+
errors.push({
|
|
116
|
+
field,
|
|
117
|
+
message: `${field} cannot be empty`,
|
|
118
|
+
});
|
|
119
|
+
} else if (!isValidAndSafeUrl(value)) {
|
|
120
|
+
errors.push({
|
|
121
|
+
field,
|
|
122
|
+
message: `${field} must be a valid URL or image data URI`,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (errors.length > 0) {
|
|
129
|
+
throw new InputValidationError(errors);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Domain Services - Public API
|
|
3
|
+
* Pure business logic services with no infrastructure dependencies
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export { ValidationService, InputValidationError } from "./ValidationService";
|
|
7
|
+
export type { ValidationError } from "./ValidationService";
|
|
8
|
+
|
|
9
|
+
export { PricingService } from "./PricingService";
|
|
10
|
+
|
|
11
|
+
export { ImageProcessingService } from "./ImageProcessingService";
|
|
12
|
+
|
|
13
|
+
export { ErrorClassificationService } from "./ErrorClassificationService";
|
package/src/index.ts
CHANGED
|
@@ -43,11 +43,19 @@ export type {
|
|
|
43
43
|
} from "./domain/entities/error.types";
|
|
44
44
|
|
|
45
45
|
// ─── Utilities (public API) ────────────────────────────────────────────────────
|
|
46
|
+
// Shared utilities (consolidated)
|
|
46
47
|
export {
|
|
47
48
|
getErrorMessage,
|
|
48
49
|
getErrorMessageOr,
|
|
49
50
|
formatErrorMessage,
|
|
50
|
-
|
|
51
|
+
buildErrorMessage,
|
|
52
|
+
isDefined,
|
|
53
|
+
removeNullish,
|
|
54
|
+
generateUniqueId,
|
|
55
|
+
sleep,
|
|
56
|
+
getElapsedTime,
|
|
57
|
+
getActualSizeKB,
|
|
58
|
+
} from "./shared/helpers";
|
|
51
59
|
|
|
52
60
|
export {
|
|
53
61
|
IMAGE_URL_FIELDS,
|
|
@@ -56,29 +64,31 @@ export {
|
|
|
56
64
|
export type { ImageUrlField } from "./infrastructure/utils/constants/image-fields.constants";
|
|
57
65
|
|
|
58
66
|
export {
|
|
67
|
+
isEmptyString,
|
|
68
|
+
isNonEmptyString,
|
|
69
|
+
isString,
|
|
59
70
|
isDataUri,
|
|
71
|
+
isImageDataUri,
|
|
60
72
|
isBase64DataUri,
|
|
61
73
|
extractMimeType,
|
|
62
74
|
extractBase64Content,
|
|
63
|
-
} from "./infrastructure/utils/validators/data-uri-validator.util";
|
|
64
|
-
|
|
65
|
-
export {
|
|
66
|
-
isEmptyString,
|
|
67
|
-
isNonEmptyString,
|
|
68
|
-
isString,
|
|
69
|
-
} from "./infrastructure/utils/validators/string-validator.util";
|
|
70
|
-
|
|
71
|
-
export {
|
|
72
|
-
isFalModelType,
|
|
73
|
-
isModelType,
|
|
74
|
-
isFalErrorType,
|
|
75
75
|
isValidBase64Image,
|
|
76
76
|
isValidApiKey,
|
|
77
77
|
isValidModelId,
|
|
78
78
|
isValidPrompt,
|
|
79
79
|
isValidTimeout,
|
|
80
80
|
isValidRetryCount,
|
|
81
|
-
|
|
81
|
+
isValidAndSafeUrl,
|
|
82
|
+
} from "./shared/validators";
|
|
83
|
+
|
|
84
|
+
export {
|
|
85
|
+
isFalModelType,
|
|
86
|
+
isModelType,
|
|
87
|
+
isFalErrorType,
|
|
88
|
+
isRetryableError,
|
|
89
|
+
isLocalFileUri,
|
|
90
|
+
isHttpUrl,
|
|
91
|
+
} from "./shared/type-guards";
|
|
82
92
|
|
|
83
93
|
export {
|
|
84
94
|
formatImageDataUri,
|
|
@@ -91,14 +101,6 @@ export {
|
|
|
91
101
|
uploadMultipleToFalStorage,
|
|
92
102
|
} from "./infrastructure/utils/fal-storage.util";
|
|
93
103
|
|
|
94
|
-
export {
|
|
95
|
-
buildErrorMessage,
|
|
96
|
-
isDefined,
|
|
97
|
-
removeNullish,
|
|
98
|
-
generateUniqueId,
|
|
99
|
-
sleep,
|
|
100
|
-
} from "./infrastructure/utils/helpers";
|
|
101
|
-
|
|
102
104
|
export { preprocessInput } from "./infrastructure/utils/input-preprocessor.util";
|
|
103
105
|
|
|
104
106
|
export {
|