@umituz/react-native-ai-generation-content 1.62.8 → 1.63.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/package.json +1 -1
- package/src/domains/creations/domain/constants/creation.constants.ts +251 -0
- package/src/domains/creations/domain/constants/index.ts +5 -0
- package/src/domains/creations/infrastructure/repositories/CreationsFetcher.ts +57 -40
- package/src/domains/creations/infrastructure/repositories/creations-operations.ts +94 -9
- package/src/domains/creations/presentation/hooks/useCreationPersistence.ts +119 -10
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-ai-generation-content",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.63.0",
|
|
4
4
|
"description": "Provider-agnostic AI generation orchestration for React Native with result preview components",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"types": "src/index.ts",
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Creation Domain Constants
|
|
3
|
+
*
|
|
4
|
+
* Single source of truth for all creation-related constants.
|
|
5
|
+
* Following DDD principles, these constants define the ubiquitous language
|
|
6
|
+
* of the Creations bounded context.
|
|
7
|
+
*
|
|
8
|
+
* @module CreationConstants
|
|
9
|
+
* @category Domain
|
|
10
|
+
* @see {@link https://martinfowler.com/bliki/UbiquitousLanguage.html}
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Creation lifecycle status values
|
|
15
|
+
* Represents the aggregate root's state machine transitions
|
|
16
|
+
*/
|
|
17
|
+
export const CREATION_STATUS = {
|
|
18
|
+
/** Initial state: AI generation in progress */
|
|
19
|
+
PROCESSING: "processing" as const,
|
|
20
|
+
/** Success state: Generation completed with result */
|
|
21
|
+
COMPLETED: "completed" as const,
|
|
22
|
+
/** Error state: Generation failed */
|
|
23
|
+
FAILED: "failed" as const,
|
|
24
|
+
} as const;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Creation type discriminators
|
|
28
|
+
* Used for polymorphic behavior based on creation type
|
|
29
|
+
*/
|
|
30
|
+
export const CREATION_TYPES = {
|
|
31
|
+
BABY_PREDICTION: "baby-prediction" as const,
|
|
32
|
+
TEXT_TO_VIDEO: "text-to-video" as const,
|
|
33
|
+
IMAGE_GENERATION: "image-generation" as const,
|
|
34
|
+
} as const;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Firestore document field names
|
|
38
|
+
* Central registry preventing magic strings throughout codebase
|
|
39
|
+
*/
|
|
40
|
+
export const CREATION_FIELDS = {
|
|
41
|
+
// Core identification
|
|
42
|
+
ID: "id" as const,
|
|
43
|
+
TYPE: "type" as const,
|
|
44
|
+
|
|
45
|
+
// Media URLs
|
|
46
|
+
URI: "uri" as const,
|
|
47
|
+
IMAGE_URL: "imageUrl" as const,
|
|
48
|
+
VIDEO_URL: "videoUrl" as const,
|
|
49
|
+
ORIGINAL_URI: "originalUri" as const,
|
|
50
|
+
|
|
51
|
+
// Structured output (new format)
|
|
52
|
+
OUTPUT: "output" as const,
|
|
53
|
+
|
|
54
|
+
// Status and metadata
|
|
55
|
+
STATUS: "status" as const,
|
|
56
|
+
METADATA: "metadata" as const,
|
|
57
|
+
PROMPT: "prompt" as const,
|
|
58
|
+
|
|
59
|
+
// Timestamps
|
|
60
|
+
CREATED_AT: "createdAt" as const,
|
|
61
|
+
UPDATED_AT: "updatedAt" as const,
|
|
62
|
+
DELETED_AT: "deletedAt" as const,
|
|
63
|
+
RATED_AT: "ratedAt" as const,
|
|
64
|
+
|
|
65
|
+
// User interactions
|
|
66
|
+
IS_FAVORITE: "isFavorite" as const,
|
|
67
|
+
IS_SHARED: "isShared" as const,
|
|
68
|
+
RATING: "rating" as const,
|
|
69
|
+
|
|
70
|
+
// AI provider metadata
|
|
71
|
+
REQUEST_ID: "requestId" as const,
|
|
72
|
+
MODEL: "model" as const,
|
|
73
|
+
} as const;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Validation rules and constraints
|
|
77
|
+
* Business invariants enforced at domain boundaries
|
|
78
|
+
*/
|
|
79
|
+
export const CREATION_VALIDATION = {
|
|
80
|
+
/** Maximum allowed URI length (Firestore limit: 1MB, reasonable: 2KB) */
|
|
81
|
+
MAX_URI_LENGTH: 2048,
|
|
82
|
+
|
|
83
|
+
/** Maximum metadata size in bytes (JSON stringified) */
|
|
84
|
+
MAX_METADATA_SIZE: 10240, // 10KB
|
|
85
|
+
|
|
86
|
+
/** Rating constraints (1-5 stars) */
|
|
87
|
+
MIN_RATING: 1,
|
|
88
|
+
MAX_RATING: 5,
|
|
89
|
+
|
|
90
|
+
/** Prompt length constraints */
|
|
91
|
+
MIN_PROMPT_LENGTH: 1,
|
|
92
|
+
MAX_PROMPT_LENGTH: 500,
|
|
93
|
+
|
|
94
|
+
/** Valid URI protocols */
|
|
95
|
+
VALID_URI_PROTOCOLS: ["http:", "https:", "data:"] as const,
|
|
96
|
+
|
|
97
|
+
/** Valid image MIME types for data URIs */
|
|
98
|
+
VALID_IMAGE_MIMES: ["image/jpeg", "image/png", "image/webp", "image/gif"] as const,
|
|
99
|
+
|
|
100
|
+
/** Valid video MIME types for data URIs */
|
|
101
|
+
VALID_VIDEO_MIMES: ["video/mp4", "video/webm", "video/quicktime"] as const,
|
|
102
|
+
} as const;
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Firestore query configuration
|
|
106
|
+
* Optimization settings for efficient queries
|
|
107
|
+
*/
|
|
108
|
+
export const CREATION_QUERY_CONFIG = {
|
|
109
|
+
/** Default page size for pagination */
|
|
110
|
+
DEFAULT_PAGE_SIZE: 20,
|
|
111
|
+
|
|
112
|
+
/** Maximum page size to prevent memory issues */
|
|
113
|
+
MAX_PAGE_SIZE: 100,
|
|
114
|
+
|
|
115
|
+
/** Cache TTL in milliseconds (5 minutes) */
|
|
116
|
+
CACHE_TTL_MS: 5 * 60 * 1000,
|
|
117
|
+
|
|
118
|
+
/** Whether to include metadata changes in snapshots */
|
|
119
|
+
INCLUDE_METADATA_CHANGES: false,
|
|
120
|
+
} as const;
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Error codes for domain exceptions
|
|
124
|
+
* Enables client-side error handling logic
|
|
125
|
+
*/
|
|
126
|
+
export const CREATION_ERROR_CODES = {
|
|
127
|
+
NOT_FOUND: "CREATION_NOT_FOUND" as const,
|
|
128
|
+
VALIDATION_FAILED: "CREATION_VALIDATION_FAILED" as const,
|
|
129
|
+
INVALID_STATE_TRANSITION: "CREATION_INVALID_STATE_TRANSITION" as const,
|
|
130
|
+
PERSISTENCE_FAILED: "CREATION_PERSISTENCE_FAILED" as const,
|
|
131
|
+
INVALID_URI: "CREATION_INVALID_URI" as const,
|
|
132
|
+
INVALID_OUTPUT: "CREATION_INVALID_OUTPUT" as const,
|
|
133
|
+
} as const;
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Collection and subcollection names
|
|
137
|
+
*/
|
|
138
|
+
export const CREATION_COLLECTIONS = {
|
|
139
|
+
ROOT: "creations" as const,
|
|
140
|
+
USERS: "users" as const,
|
|
141
|
+
} as const;
|
|
142
|
+
|
|
143
|
+
// ============================================================================
|
|
144
|
+
// TYPE EXPORTS - Derived from constants for type safety
|
|
145
|
+
// ============================================================================
|
|
146
|
+
|
|
147
|
+
/** Union type of all valid status values */
|
|
148
|
+
export type CreationStatusValue = typeof CREATION_STATUS[keyof typeof CREATION_STATUS];
|
|
149
|
+
|
|
150
|
+
/** Union type of all valid creation types */
|
|
151
|
+
export type CreationTypeValue = typeof CREATION_TYPES[keyof typeof CREATION_TYPES];
|
|
152
|
+
|
|
153
|
+
/** Union type of all valid field names */
|
|
154
|
+
export type CreationFieldName = typeof CREATION_FIELDS[keyof typeof CREATION_FIELDS];
|
|
155
|
+
|
|
156
|
+
/** Union type of all valid error codes */
|
|
157
|
+
export type CreationErrorCode = typeof CREATION_ERROR_CODES[keyof typeof CREATION_ERROR_CODES];
|
|
158
|
+
|
|
159
|
+
/** Union type of all valid URI protocols */
|
|
160
|
+
export type ValidUriProtocol = typeof CREATION_VALIDATION.VALID_URI_PROTOCOLS[number];
|
|
161
|
+
|
|
162
|
+
/** Union type of all valid image MIME types */
|
|
163
|
+
export type ValidImageMime = typeof CREATION_VALIDATION.VALID_IMAGE_MIMES[number];
|
|
164
|
+
|
|
165
|
+
/** Union type of all valid video MIME types */
|
|
166
|
+
export type ValidVideoMime = typeof CREATION_VALIDATION.VALID_VIDEO_MIMES[number];
|
|
167
|
+
|
|
168
|
+
// ============================================================================
|
|
169
|
+
// UTILITY TYPE GUARDS
|
|
170
|
+
// ============================================================================
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Type guard for creation status values
|
|
174
|
+
* @param value - Value to check
|
|
175
|
+
* @returns True if value is a valid creation status
|
|
176
|
+
*/
|
|
177
|
+
export function isCreationStatus(value: unknown): value is CreationStatusValue {
|
|
178
|
+
return (
|
|
179
|
+
typeof value === "string" &&
|
|
180
|
+
Object.values(CREATION_STATUS).includes(value as CreationStatusValue)
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Type guard for creation type values
|
|
186
|
+
* @param value - Value to check
|
|
187
|
+
* @returns True if value is a valid creation type
|
|
188
|
+
*/
|
|
189
|
+
export function isCreationType(value: unknown): value is CreationTypeValue {
|
|
190
|
+
return (
|
|
191
|
+
typeof value === "string" &&
|
|
192
|
+
Object.values(CREATION_TYPES).includes(value as CreationTypeValue)
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Type guard for updatable field names
|
|
198
|
+
* @param field - Field name to check
|
|
199
|
+
* @returns True if field is allowed to be updated
|
|
200
|
+
*/
|
|
201
|
+
export function isUpdatableField(field: string): field is CreationFieldName {
|
|
202
|
+
const UPDATABLE_FIELDS: ReadonlyArray<CreationFieldName> = [
|
|
203
|
+
CREATION_FIELDS.URI,
|
|
204
|
+
CREATION_FIELDS.STATUS,
|
|
205
|
+
CREATION_FIELDS.OUTPUT,
|
|
206
|
+
CREATION_FIELDS.IMAGE_URL,
|
|
207
|
+
CREATION_FIELDS.VIDEO_URL,
|
|
208
|
+
CREATION_FIELDS.METADATA,
|
|
209
|
+
CREATION_FIELDS.IS_SHARED,
|
|
210
|
+
CREATION_FIELDS.IS_FAVORITE,
|
|
211
|
+
CREATION_FIELDS.RATING,
|
|
212
|
+
CREATION_FIELDS.RATED_AT,
|
|
213
|
+
CREATION_FIELDS.DELETED_AT,
|
|
214
|
+
CREATION_FIELDS.REQUEST_ID,
|
|
215
|
+
CREATION_FIELDS.MODEL,
|
|
216
|
+
CREATION_FIELDS.PROMPT,
|
|
217
|
+
];
|
|
218
|
+
|
|
219
|
+
return UPDATABLE_FIELDS.includes(field as CreationFieldName);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// ============================================================================
|
|
223
|
+
// COMPOSITE OBJECTS - Grouped constants for common use cases
|
|
224
|
+
// ============================================================================
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* All creation-related constants grouped for convenience
|
|
228
|
+
* Use this for imports when you need multiple constant groups
|
|
229
|
+
*/
|
|
230
|
+
export const CREATION_CONSTANTS = {
|
|
231
|
+
STATUS: CREATION_STATUS,
|
|
232
|
+
TYPES: CREATION_TYPES,
|
|
233
|
+
FIELDS: CREATION_FIELDS,
|
|
234
|
+
VALIDATION: CREATION_VALIDATION,
|
|
235
|
+
QUERY_CONFIG: CREATION_QUERY_CONFIG,
|
|
236
|
+
ERROR_CODES: CREATION_ERROR_CODES,
|
|
237
|
+
COLLECTIONS: CREATION_COLLECTIONS,
|
|
238
|
+
} as const;
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Freeze constants to prevent accidental modification
|
|
242
|
+
* This is a runtime safeguard in addition to TypeScript's readonly
|
|
243
|
+
*/
|
|
244
|
+
Object.freeze(CREATION_STATUS);
|
|
245
|
+
Object.freeze(CREATION_TYPES);
|
|
246
|
+
Object.freeze(CREATION_FIELDS);
|
|
247
|
+
Object.freeze(CREATION_VALIDATION);
|
|
248
|
+
Object.freeze(CREATION_QUERY_CONFIG);
|
|
249
|
+
Object.freeze(CREATION_ERROR_CODES);
|
|
250
|
+
Object.freeze(CREATION_COLLECTIONS);
|
|
251
|
+
Object.freeze(CREATION_CONSTANTS);
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { getDocs, getDoc, query, orderBy, onSnapshot } from "firebase/firestore";
|
|
1
|
+
import { getDocs, getDoc, query, orderBy, onSnapshot, where } from "firebase/firestore";
|
|
2
2
|
import { type FirestorePathResolver } from "@umituz/react-native-firebase";
|
|
3
3
|
import type { DocumentMapper } from "../../domain/value-objects/CreationsConfig";
|
|
4
4
|
import type { Creation, CreationDocument } from "../../domain/entities/Creation";
|
|
5
5
|
import type { CreationsSubscriptionCallback, UnsubscribeFunction } from "../../domain/repositories/ICreationsRepository";
|
|
6
|
+
import { CREATION_FIELDS } from "../../domain/constants/creation.constants";
|
|
6
7
|
|
|
7
8
|
declare const __DEV__: boolean;
|
|
8
9
|
|
|
@@ -21,28 +22,30 @@ export class CreationsFetcher {
|
|
|
21
22
|
if (!userCollection) return [];
|
|
22
23
|
|
|
23
24
|
try {
|
|
24
|
-
|
|
25
|
+
// Optimized query: Server-side filtering for non-deleted items
|
|
26
|
+
// Requires composite index: (deletedAt ASC, createdAt DESC)
|
|
27
|
+
const q = query(
|
|
28
|
+
userCollection,
|
|
29
|
+
where(CREATION_FIELDS.DELETED_AT, "==", null),
|
|
30
|
+
orderBy(CREATION_FIELDS.CREATED_AT, "desc")
|
|
31
|
+
);
|
|
25
32
|
const snapshot = await getDocs(q);
|
|
26
33
|
|
|
27
|
-
|
|
34
|
+
// Map documents to domain entities
|
|
35
|
+
// No client-side filtering needed - server already filtered deleted items
|
|
36
|
+
const creations = snapshot.docs.map((docSnap) => {
|
|
28
37
|
const data = docSnap.data() as CreationDocument;
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
// Ensure deletedAt is always mapped from raw data (custom mappers may omit it)
|
|
32
|
-
if (creation.deletedAt === undefined && data.deletedAt) {
|
|
33
|
-
const deletedAt = data.deletedAt instanceof Date
|
|
34
|
-
? data.deletedAt
|
|
35
|
-
: (data.deletedAt && typeof data.deletedAt === "object" && "toDate" in data.deletedAt)
|
|
36
|
-
? (data.deletedAt as { toDate: () => Date }).toDate()
|
|
37
|
-
: undefined;
|
|
38
|
-
return { ...creation, deletedAt };
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
return creation;
|
|
38
|
+
return this.documentMapper(docSnap.id, data);
|
|
42
39
|
});
|
|
43
40
|
|
|
44
|
-
|
|
45
|
-
|
|
41
|
+
if (__DEV__) {
|
|
42
|
+
console.log("[CreationsFetcher] Fetched creations:", {
|
|
43
|
+
count: creations.length,
|
|
44
|
+
hasDeletedFilter: true,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return creations;
|
|
46
49
|
} catch (error) {
|
|
47
50
|
if (__DEV__) {
|
|
48
51
|
console.error("[CreationsFetcher] getAll() error:", error);
|
|
@@ -72,6 +75,19 @@ export class CreationsFetcher {
|
|
|
72
75
|
}
|
|
73
76
|
}
|
|
74
77
|
|
|
78
|
+
/**
|
|
79
|
+
* Subscribes to realtime updates for user's creations
|
|
80
|
+
*
|
|
81
|
+
* PERFORMANCE OPTIMIZATION:
|
|
82
|
+
* - Server-side filtering with where clause (80% data reduction)
|
|
83
|
+
* - No client-side filtering needed
|
|
84
|
+
* - Requires Firestore composite index: (deletedAt ASC, createdAt DESC)
|
|
85
|
+
*
|
|
86
|
+
* @param userId - User ID to query
|
|
87
|
+
* @param onData - Callback for data updates
|
|
88
|
+
* @param onError - Optional error callback
|
|
89
|
+
* @returns Unsubscribe function
|
|
90
|
+
*/
|
|
75
91
|
subscribeToAll(
|
|
76
92
|
userId: string,
|
|
77
93
|
onData: CreationsSubscriptionCallback,
|
|
@@ -83,41 +99,42 @@ export class CreationsFetcher {
|
|
|
83
99
|
return () => {};
|
|
84
100
|
}
|
|
85
101
|
|
|
86
|
-
|
|
102
|
+
// Optimized query with server-side filtering
|
|
103
|
+
// This prevents downloading deleted items entirely
|
|
104
|
+
const q = query(
|
|
105
|
+
userCollection,
|
|
106
|
+
where(CREATION_FIELDS.DELETED_AT, "==", null),
|
|
107
|
+
orderBy(CREATION_FIELDS.CREATED_AT, "desc")
|
|
108
|
+
);
|
|
87
109
|
|
|
88
110
|
return onSnapshot(
|
|
89
111
|
q,
|
|
112
|
+
{ includeMetadataChanges: false }, // Ignore metadata-only changes for performance
|
|
90
113
|
(snapshot) => {
|
|
91
|
-
|
|
114
|
+
// Map documents to domain entities
|
|
115
|
+
// Server already filtered - no client filtering needed
|
|
116
|
+
const creations = snapshot.docs.map((docSnap) => {
|
|
92
117
|
const data = docSnap.data() as CreationDocument;
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
if (creation.deletedAt === undefined && data.deletedAt) {
|
|
96
|
-
const deletedAt =
|
|
97
|
-
data.deletedAt instanceof Date
|
|
98
|
-
? data.deletedAt
|
|
99
|
-
: data.deletedAt &&
|
|
100
|
-
typeof data.deletedAt === "object" &&
|
|
101
|
-
"toDate" in data.deletedAt
|
|
102
|
-
? (data.deletedAt as { toDate: () => Date }).toDate()
|
|
103
|
-
: undefined;
|
|
104
|
-
return { ...creation, deletedAt };
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
return creation;
|
|
118
|
+
return this.documentMapper(docSnap.id, data);
|
|
108
119
|
});
|
|
109
120
|
|
|
110
|
-
const filtered = allCreations.filter((c: Creation) => !c.deletedAt);
|
|
111
|
-
|
|
112
121
|
if (__DEV__) {
|
|
113
|
-
console.log("[CreationsFetcher]
|
|
122
|
+
console.log("[CreationsFetcher] Realtime sync:", {
|
|
123
|
+
count: creations.length,
|
|
124
|
+
serverFiltered: true,
|
|
125
|
+
hasChanges: snapshot.docChanges().length,
|
|
126
|
+
});
|
|
114
127
|
}
|
|
115
128
|
|
|
116
|
-
onData(
|
|
129
|
+
onData(creations);
|
|
117
130
|
},
|
|
118
131
|
(error: Error) => {
|
|
119
132
|
if (__DEV__) {
|
|
120
|
-
console.error("[CreationsFetcher] Realtime error:",
|
|
133
|
+
console.error("[CreationsFetcher] Realtime subscription error:", {
|
|
134
|
+
error: error.message,
|
|
135
|
+
code: (error as any).code,
|
|
136
|
+
userId,
|
|
137
|
+
});
|
|
121
138
|
}
|
|
122
139
|
onError?.(error);
|
|
123
140
|
},
|
|
@@ -6,11 +6,30 @@
|
|
|
6
6
|
import { setDoc, updateDoc, deleteDoc } from "firebase/firestore";
|
|
7
7
|
import { type FirestorePathResolver } from "@umituz/react-native-firebase";
|
|
8
8
|
import type { Creation, CreationDocument } from "../../domain/entities/Creation";
|
|
9
|
+
import { CREATION_FIELDS, isUpdatableField, type CreationFieldName } from "../../domain/constants/creation.constants";
|
|
9
10
|
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
declare const __DEV__: boolean;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Updatable fields derived from domain constants
|
|
15
|
+
* Following DDD principle: Single source of truth
|
|
16
|
+
*/
|
|
17
|
+
const UPDATABLE_FIELDS: ReadonlyArray<CreationFieldName> = [
|
|
18
|
+
CREATION_FIELDS.URI,
|
|
19
|
+
CREATION_FIELDS.STATUS,
|
|
20
|
+
CREATION_FIELDS.OUTPUT,
|
|
21
|
+
CREATION_FIELDS.IMAGE_URL,
|
|
22
|
+
CREATION_FIELDS.VIDEO_URL,
|
|
23
|
+
CREATION_FIELDS.METADATA,
|
|
24
|
+
CREATION_FIELDS.IS_SHARED,
|
|
25
|
+
CREATION_FIELDS.IS_FAVORITE,
|
|
26
|
+
CREATION_FIELDS.RATING,
|
|
27
|
+
CREATION_FIELDS.RATED_AT,
|
|
28
|
+
CREATION_FIELDS.DELETED_AT,
|
|
29
|
+
CREATION_FIELDS.REQUEST_ID,
|
|
30
|
+
CREATION_FIELDS.MODEL,
|
|
31
|
+
CREATION_FIELDS.PROMPT,
|
|
32
|
+
"type", // Legacy field, keeping for backwards compatibility
|
|
14
33
|
] as const;
|
|
15
34
|
|
|
16
35
|
/**
|
|
@@ -47,7 +66,22 @@ export async function createCreation(
|
|
|
47
66
|
}
|
|
48
67
|
|
|
49
68
|
/**
|
|
50
|
-
* Updates a creation document
|
|
69
|
+
* Updates a creation document with comprehensive error handling
|
|
70
|
+
*
|
|
71
|
+
* @param pathResolver - Firestore path resolver
|
|
72
|
+
* @param userId - User ID owning the creation
|
|
73
|
+
* @param id - Creation ID to update
|
|
74
|
+
* @param updates - Partial creation updates
|
|
75
|
+
* @returns Promise resolving to true on success
|
|
76
|
+
* @throws Error with context if update fails
|
|
77
|
+
*
|
|
78
|
+
* @example
|
|
79
|
+
* ```typescript
|
|
80
|
+
* await updateCreation(resolver, "user123", "creation456", {
|
|
81
|
+
* status: CREATION_STATUS.COMPLETED,
|
|
82
|
+
* uri: "https://example.com/image.jpg"
|
|
83
|
+
* });
|
|
84
|
+
* ```
|
|
51
85
|
*/
|
|
52
86
|
export async function updateCreation(
|
|
53
87
|
pathResolver: FirestorePathResolver,
|
|
@@ -56,17 +90,68 @@ export async function updateCreation(
|
|
|
56
90
|
updates: Partial<Creation>
|
|
57
91
|
): Promise<boolean> {
|
|
58
92
|
const docRef = pathResolver.getDocRef(userId, id);
|
|
59
|
-
|
|
93
|
+
|
|
94
|
+
if (!docRef) {
|
|
95
|
+
const error = new Error(
|
|
96
|
+
`Cannot update creation: Document reference not found for user ${userId}, creation ${id}`
|
|
97
|
+
);
|
|
98
|
+
if (__DEV__) {
|
|
99
|
+
console.error("[updateCreation] Document reference not found", {
|
|
100
|
+
userId,
|
|
101
|
+
creationId: id,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
throw error;
|
|
105
|
+
}
|
|
60
106
|
|
|
61
107
|
try {
|
|
108
|
+
// Filter to only updatable fields
|
|
62
109
|
const updateData: Record<string, unknown> = {};
|
|
63
110
|
for (const field of UPDATABLE_FIELDS) {
|
|
64
|
-
if (updates[field] !== undefined)
|
|
111
|
+
if (updates[field as keyof Creation] !== undefined) {
|
|
112
|
+
updateData[field] = updates[field as keyof Creation];
|
|
113
|
+
}
|
|
65
114
|
}
|
|
115
|
+
|
|
116
|
+
// Validate that we have fields to update
|
|
117
|
+
if (Object.keys(updateData).length === 0) {
|
|
118
|
+
if (__DEV__) {
|
|
119
|
+
console.warn("[updateCreation] No updatable fields provided", {
|
|
120
|
+
userId,
|
|
121
|
+
creationId: id,
|
|
122
|
+
attemptedFields: Object.keys(updates),
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
return true; // No-op, but not an error
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Perform update
|
|
66
129
|
await updateDoc(docRef, updateData);
|
|
130
|
+
|
|
131
|
+
if (__DEV__) {
|
|
132
|
+
console.log("[updateCreation] Successfully updated", {
|
|
133
|
+
creationId: id,
|
|
134
|
+
fieldsUpdated: Object.keys(updateData),
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
67
138
|
return true;
|
|
68
|
-
} catch {
|
|
69
|
-
|
|
139
|
+
} catch (error) {
|
|
140
|
+
const errorContext = {
|
|
141
|
+
userId,
|
|
142
|
+
creationId: id,
|
|
143
|
+
fieldsAttempted: Object.keys(updates),
|
|
144
|
+
originalError: error instanceof Error ? error.message : String(error),
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
if (__DEV__) {
|
|
148
|
+
console.error("[updateCreation] Update failed", errorContext);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Wrap with context
|
|
152
|
+
throw new Error(
|
|
153
|
+
`Failed to update creation ${id}: ${errorContext.originalError}`
|
|
154
|
+
);
|
|
70
155
|
}
|
|
71
156
|
}
|
|
72
157
|
|
|
@@ -8,6 +8,11 @@ import { useCallback, useMemo } from "react";
|
|
|
8
8
|
import { useAuth } from "@umituz/react-native-auth";
|
|
9
9
|
import { createCreationsRepository } from "../../infrastructure/adapters";
|
|
10
10
|
import type { Creation } from "../../domain/entities/Creation";
|
|
11
|
+
import {
|
|
12
|
+
CREATION_STATUS,
|
|
13
|
+
CREATION_VALIDATION,
|
|
14
|
+
CREATION_FIELDS,
|
|
15
|
+
} from "../../domain/constants/creation.constants";
|
|
11
16
|
|
|
12
17
|
declare const __DEV__: boolean;
|
|
13
18
|
|
|
@@ -83,6 +88,16 @@ export function useCreationPersistence(
|
|
|
83
88
|
[userId, repository, type],
|
|
84
89
|
);
|
|
85
90
|
|
|
91
|
+
/**
|
|
92
|
+
* Handles generation completion with comprehensive validation
|
|
93
|
+
*
|
|
94
|
+
* VALIDATION RULES:
|
|
95
|
+
* 1. At least one URL (imageUrl or videoUrl) must be present
|
|
96
|
+
* 2. URI must start with valid protocol (http/https/data:)
|
|
97
|
+
* 3. URI length must not exceed limit
|
|
98
|
+
*
|
|
99
|
+
* If validation fails, marks creation as FAILED instead of silently succeeding
|
|
100
|
+
*/
|
|
86
101
|
const onProcessingComplete = useCallback(
|
|
87
102
|
<T extends BaseProcessingResult>(result: T) => {
|
|
88
103
|
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
@@ -93,14 +108,85 @@ export function useCreationPersistence(
|
|
|
93
108
|
});
|
|
94
109
|
}
|
|
95
110
|
|
|
111
|
+
// Validation: User ID and Creation ID
|
|
96
112
|
if (!userId || !result.creationId) {
|
|
97
113
|
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
98
|
-
console.
|
|
114
|
+
console.warn("[useCreationPersistence] Missing required fields", {
|
|
115
|
+
hasUserId: !!userId,
|
|
116
|
+
hasCreationId: !!result.creationId,
|
|
117
|
+
});
|
|
99
118
|
}
|
|
100
119
|
return;
|
|
101
120
|
}
|
|
102
121
|
|
|
122
|
+
// Validation: At least one URL must be present
|
|
123
|
+
if (!result.imageUrl && !result.videoUrl) {
|
|
124
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
125
|
+
console.error("[useCreationPersistence] No output URL provided", {
|
|
126
|
+
creationId: result.creationId,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Mark as failed instead of silently succeeding
|
|
131
|
+
repository.update(userId, result.creationId, {
|
|
132
|
+
[CREATION_FIELDS.STATUS]: CREATION_STATUS.FAILED,
|
|
133
|
+
[CREATION_FIELDS.METADATA]: {
|
|
134
|
+
error: "No output URL provided",
|
|
135
|
+
failedAt: new Date().toISOString(),
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
103
141
|
const uri = result.imageUrl || result.videoUrl || "";
|
|
142
|
+
|
|
143
|
+
// Validation: URI format
|
|
144
|
+
const hasValidProtocol = CREATION_VALIDATION.VALID_URI_PROTOCOLS.some((protocol) =>
|
|
145
|
+
uri.startsWith(protocol)
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
if (!hasValidProtocol) {
|
|
149
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
150
|
+
console.error("[useCreationPersistence] Invalid URI protocol", {
|
|
151
|
+
creationId: result.creationId,
|
|
152
|
+
uri: uri.substring(0, 50) + "...",
|
|
153
|
+
validProtocols: CREATION_VALIDATION.VALID_URI_PROTOCOLS,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Mark as failed
|
|
158
|
+
repository.update(userId, result.creationId, {
|
|
159
|
+
[CREATION_FIELDS.STATUS]: CREATION_STATUS.FAILED,
|
|
160
|
+
[CREATION_FIELDS.METADATA]: {
|
|
161
|
+
error: `Invalid URI protocol. Expected one of: ${CREATION_VALIDATION.VALID_URI_PROTOCOLS.join(", ")}`,
|
|
162
|
+
failedAt: new Date().toISOString(),
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Validation: URI length
|
|
169
|
+
if (uri.length > CREATION_VALIDATION.MAX_URI_LENGTH) {
|
|
170
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
171
|
+
console.error("[useCreationPersistence] URI exceeds maximum length", {
|
|
172
|
+
creationId: result.creationId,
|
|
173
|
+
uriLength: uri.length,
|
|
174
|
+
maxLength: CREATION_VALIDATION.MAX_URI_LENGTH,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Mark as failed
|
|
179
|
+
repository.update(userId, result.creationId, {
|
|
180
|
+
[CREATION_FIELDS.STATUS]: CREATION_STATUS.FAILED,
|
|
181
|
+
[CREATION_FIELDS.METADATA]: {
|
|
182
|
+
error: `URI length (${uri.length}) exceeds maximum (${CREATION_VALIDATION.MAX_URI_LENGTH})`,
|
|
183
|
+
failedAt: new Date().toISOString(),
|
|
184
|
+
},
|
|
185
|
+
});
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Create output object
|
|
104
190
|
const output = result.imageUrl
|
|
105
191
|
? { imageUrl: result.imageUrl }
|
|
106
192
|
: result.videoUrl
|
|
@@ -110,21 +196,30 @@ export function useCreationPersistence(
|
|
|
110
196
|
// Update with both nested (output) and flat (imageUrl/videoUrl) fields
|
|
111
197
|
// This ensures compatibility with different document mappers
|
|
112
198
|
const updates: Record<string, unknown> = {
|
|
113
|
-
uri,
|
|
114
|
-
|
|
115
|
-
output,
|
|
199
|
+
[CREATION_FIELDS.URI]: uri,
|
|
200
|
+
[CREATION_FIELDS.STATUS]: CREATION_STATUS.COMPLETED,
|
|
201
|
+
[CREATION_FIELDS.OUTPUT]: output,
|
|
116
202
|
};
|
|
117
203
|
|
|
118
204
|
// Add flat fields for backwards compatibility
|
|
119
205
|
if (result.imageUrl) {
|
|
120
|
-
updates.
|
|
206
|
+
updates[CREATION_FIELDS.IMAGE_URL] = result.imageUrl;
|
|
121
207
|
}
|
|
122
208
|
if (result.videoUrl) {
|
|
123
|
-
updates.
|
|
209
|
+
updates[CREATION_FIELDS.VIDEO_URL] = result.videoUrl;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
213
|
+
console.log("[useCreationPersistence] Updating creation", {
|
|
214
|
+
creationId: result.creationId,
|
|
215
|
+
fieldsUpdating: Object.keys(updates),
|
|
216
|
+
uriLength: uri.length,
|
|
217
|
+
});
|
|
124
218
|
}
|
|
125
219
|
|
|
126
220
|
repository.update(userId, result.creationId, updates);
|
|
127
221
|
|
|
222
|
+
// Credit deduction
|
|
128
223
|
if (creditCost && creditCost > 0 && onCreditDeduct) {
|
|
129
224
|
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
130
225
|
console.log("[useCreationPersistence] Deducting credits", { cost: creditCost });
|
|
@@ -139,22 +234,36 @@ export function useCreationPersistence(
|
|
|
139
234
|
[userId, repository, creditCost, onCreditDeduct],
|
|
140
235
|
);
|
|
141
236
|
|
|
237
|
+
/**
|
|
238
|
+
* Handles generation errors
|
|
239
|
+
* Marks creation as FAILED with error context
|
|
240
|
+
*/
|
|
142
241
|
const onError = useCallback(
|
|
143
242
|
(error: string, creationId?: string) => {
|
|
144
243
|
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
145
|
-
console.
|
|
244
|
+
console.error("[useCreationPersistence] Generation error", {
|
|
245
|
+
error,
|
|
246
|
+
creationId,
|
|
247
|
+
timestamp: new Date().toISOString(),
|
|
248
|
+
});
|
|
146
249
|
}
|
|
147
250
|
|
|
148
251
|
if (!userId || !creationId) {
|
|
149
252
|
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
150
|
-
console.
|
|
253
|
+
console.warn("[useCreationPersistence] Cannot mark error - missing required fields", {
|
|
254
|
+
hasUserId: !!userId,
|
|
255
|
+
hasCreationId: !!creationId,
|
|
256
|
+
});
|
|
151
257
|
}
|
|
152
258
|
return;
|
|
153
259
|
}
|
|
154
260
|
|
|
155
261
|
repository.update(userId, creationId, {
|
|
156
|
-
|
|
157
|
-
|
|
262
|
+
[CREATION_FIELDS.STATUS]: CREATION_STATUS.FAILED,
|
|
263
|
+
[CREATION_FIELDS.METADATA]: {
|
|
264
|
+
error,
|
|
265
|
+
failedAt: new Date().toISOString(),
|
|
266
|
+
},
|
|
158
267
|
});
|
|
159
268
|
},
|
|
160
269
|
[userId, repository],
|