@umituz/react-native-ai-generation-content 1.62.7 → 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 +59 -67
- package/src/domains/creations/infrastructure/repositories/CreationsRepository.ts +1 -6
- package/src/domains/creations/infrastructure/repositories/creations-operations.ts +94 -9
- package/src/domains/creations/presentation/hooks/useCreationPersistence.ts +131 -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
|
|
|
@@ -17,57 +18,43 @@ export class CreationsFetcher {
|
|
|
17
18
|
) { }
|
|
18
19
|
|
|
19
20
|
async getAll(userId: string): Promise<Creation[]> {
|
|
20
|
-
if (__DEV__) {
|
|
21
|
-
|
|
22
|
-
console.log("[CreationsRepository] getAll()", { userId });
|
|
23
|
-
}
|
|
24
|
-
|
|
25
21
|
const userCollection = this.pathResolver.getUserCollection(userId);
|
|
26
22
|
if (!userCollection) return [];
|
|
27
23
|
|
|
28
24
|
try {
|
|
29
|
-
|
|
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
|
+
);
|
|
30
32
|
const snapshot = await getDocs(q);
|
|
31
33
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const allCreations = snapshot.docs.map((docSnap) => {
|
|
34
|
+
// Map documents to domain entities
|
|
35
|
+
// No client-side filtering needed - server already filtered deleted items
|
|
36
|
+
const creations = snapshot.docs.map((docSnap) => {
|
|
38
37
|
const data = docSnap.data() as CreationDocument;
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
// Ensure deletedAt is always mapped from raw data (custom mappers may omit it)
|
|
42
|
-
if (creation.deletedAt === undefined && data.deletedAt) {
|
|
43
|
-
const deletedAt = data.deletedAt instanceof Date
|
|
44
|
-
? data.deletedAt
|
|
45
|
-
: (data.deletedAt && typeof data.deletedAt === "object" && "toDate" in data.deletedAt)
|
|
46
|
-
? (data.deletedAt as { toDate: () => Date }).toDate()
|
|
47
|
-
: undefined;
|
|
48
|
-
return { ...creation, deletedAt };
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
return creation;
|
|
38
|
+
return this.documentMapper(docSnap.id, data);
|
|
52
39
|
});
|
|
53
40
|
|
|
54
|
-
// Filter out soft-deleted creations
|
|
55
|
-
return allCreations.filter((creation: Creation) => !creation.deletedAt);
|
|
56
|
-
} catch (error) {
|
|
57
41
|
if (__DEV__) {
|
|
42
|
+
console.log("[CreationsFetcher] Fetched creations:", {
|
|
43
|
+
count: creations.length,
|
|
44
|
+
hasDeletedFilter: true,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
58
47
|
|
|
59
|
-
|
|
48
|
+
return creations;
|
|
49
|
+
} catch (error) {
|
|
50
|
+
if (__DEV__) {
|
|
51
|
+
console.error("[CreationsFetcher] getAll() error:", error);
|
|
60
52
|
}
|
|
61
53
|
return [];
|
|
62
54
|
}
|
|
63
55
|
}
|
|
64
56
|
|
|
65
57
|
async getById(userId: string, id: string): Promise<Creation | null> {
|
|
66
|
-
if (__DEV__) {
|
|
67
|
-
|
|
68
|
-
console.log("[CreationsRepository] getById()", { userId, id });
|
|
69
|
-
}
|
|
70
|
-
|
|
71
58
|
const docRef = this.pathResolver.getDocRef(userId, id);
|
|
72
59
|
if (!docRef) return null;
|
|
73
60
|
|
|
@@ -75,10 +62,6 @@ export class CreationsFetcher {
|
|
|
75
62
|
const docSnap = await getDoc(docRef);
|
|
76
63
|
|
|
77
64
|
if (!docSnap.exists()) {
|
|
78
|
-
if (__DEV__) {
|
|
79
|
-
|
|
80
|
-
console.log("[CreationsRepository] Document not found");
|
|
81
|
-
}
|
|
82
65
|
return null;
|
|
83
66
|
}
|
|
84
67
|
|
|
@@ -86,63 +69,72 @@ export class CreationsFetcher {
|
|
|
86
69
|
return this.documentMapper(docSnap.id, data);
|
|
87
70
|
} catch (error) {
|
|
88
71
|
if (__DEV__) {
|
|
89
|
-
|
|
90
|
-
console.error("[CreationsRepository] getById() ERROR", error);
|
|
72
|
+
console.error("[CreationsFetcher] getById() error:", error);
|
|
91
73
|
}
|
|
92
74
|
return null;
|
|
93
75
|
}
|
|
94
76
|
}
|
|
95
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
|
+
*/
|
|
96
91
|
subscribeToAll(
|
|
97
92
|
userId: string,
|
|
98
93
|
onData: CreationsSubscriptionCallback,
|
|
99
94
|
onError?: (error: Error) => void,
|
|
100
95
|
): UnsubscribeFunction {
|
|
101
|
-
if (__DEV__) {
|
|
102
|
-
console.log("[CreationsFetcher] subscribeToAll()", { userId });
|
|
103
|
-
}
|
|
104
|
-
|
|
105
96
|
const userCollection = this.pathResolver.getUserCollection(userId);
|
|
106
97
|
if (!userCollection) {
|
|
107
98
|
onData([]);
|
|
108
99
|
return () => {};
|
|
109
100
|
}
|
|
110
101
|
|
|
111
|
-
|
|
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
|
+
);
|
|
112
109
|
|
|
113
110
|
return onSnapshot(
|
|
114
111
|
q,
|
|
112
|
+
{ includeMetadataChanges: false }, // Ignore metadata-only changes for performance
|
|
115
113
|
(snapshot) => {
|
|
116
|
-
|
|
114
|
+
// Map documents to domain entities
|
|
115
|
+
// Server already filtered - no client filtering needed
|
|
116
|
+
const creations = snapshot.docs.map((docSnap) => {
|
|
117
117
|
const data = docSnap.data() as CreationDocument;
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
if (creation.deletedAt === undefined && data.deletedAt) {
|
|
121
|
-
const deletedAt =
|
|
122
|
-
data.deletedAt instanceof Date
|
|
123
|
-
? data.deletedAt
|
|
124
|
-
: data.deletedAt &&
|
|
125
|
-
typeof data.deletedAt === "object" &&
|
|
126
|
-
"toDate" in data.deletedAt
|
|
127
|
-
? (data.deletedAt as { toDate: () => Date }).toDate()
|
|
128
|
-
: undefined;
|
|
129
|
-
return { ...creation, deletedAt };
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
return creation;
|
|
118
|
+
return this.documentMapper(docSnap.id, data);
|
|
133
119
|
});
|
|
134
120
|
|
|
135
|
-
const filtered = allCreations.filter((c: Creation) => !c.deletedAt);
|
|
136
|
-
|
|
137
121
|
if (__DEV__) {
|
|
138
|
-
console.log("[CreationsFetcher] Realtime
|
|
122
|
+
console.log("[CreationsFetcher] Realtime sync:", {
|
|
123
|
+
count: creations.length,
|
|
124
|
+
serverFiltered: true,
|
|
125
|
+
hasChanges: snapshot.docChanges().length,
|
|
126
|
+
});
|
|
139
127
|
}
|
|
140
128
|
|
|
141
|
-
onData(
|
|
129
|
+
onData(creations);
|
|
142
130
|
},
|
|
143
131
|
(error: Error) => {
|
|
144
132
|
if (__DEV__) {
|
|
145
|
-
console.error("[CreationsFetcher]
|
|
133
|
+
console.error("[CreationsFetcher] Realtime subscription error:", {
|
|
134
|
+
error: error.message,
|
|
135
|
+
code: (error as any).code,
|
|
136
|
+
userId,
|
|
137
|
+
});
|
|
146
138
|
}
|
|
147
139
|
onError?.(error);
|
|
148
140
|
},
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) console.log("📍 [LIFECYCLE] CreationsRepository.ts - Module loading");
|
|
1
|
+
declare const __DEV__: boolean;
|
|
3
2
|
|
|
4
3
|
import { BaseRepository, FirestorePathResolver } from "@umituz/react-native-firebase";
|
|
5
4
|
import type {
|
|
@@ -43,8 +42,6 @@ export class CreationsRepository
|
|
|
43
42
|
collectionName: string,
|
|
44
43
|
options?: RepositoryOptions,
|
|
45
44
|
) {
|
|
46
|
-
|
|
47
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) console.log("📍 [LIFECYCLE] CreationsRepository - Constructor start");
|
|
48
45
|
super();
|
|
49
46
|
|
|
50
47
|
const documentMapper = options?.documentMapper ?? mapDocumentToCreation;
|
|
@@ -53,8 +50,6 @@ export class CreationsRepository
|
|
|
53
50
|
this.pathResolver = new FirestorePathResolver(collectionName, null);
|
|
54
51
|
this.fetcher = new CreationsFetcher(this.pathResolver, documentMapper);
|
|
55
52
|
this.writer = new CreationsWriter(this.pathResolver);
|
|
56
|
-
|
|
57
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) console.log("📍 [LIFECYCLE] CreationsRepository - Constructor end");
|
|
58
53
|
}
|
|
59
54
|
|
|
60
55
|
async getAll(userId: string): Promise<Creation[]> {
|
|
@@ -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,26 +108,118 @@ 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
|
|
107
193
|
? { videoUrl: result.videoUrl }
|
|
108
194
|
: undefined;
|
|
109
195
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
196
|
+
// Update with both nested (output) and flat (imageUrl/videoUrl) fields
|
|
197
|
+
// This ensures compatibility with different document mappers
|
|
198
|
+
const updates: Record<string, unknown> = {
|
|
199
|
+
[CREATION_FIELDS.URI]: uri,
|
|
200
|
+
[CREATION_FIELDS.STATUS]: CREATION_STATUS.COMPLETED,
|
|
201
|
+
[CREATION_FIELDS.OUTPUT]: output,
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
// Add flat fields for backwards compatibility
|
|
205
|
+
if (result.imageUrl) {
|
|
206
|
+
updates[CREATION_FIELDS.IMAGE_URL] = result.imageUrl;
|
|
207
|
+
}
|
|
208
|
+
if (result.videoUrl) {
|
|
209
|
+
updates[CREATION_FIELDS.VIDEO_URL] = result.videoUrl;
|
|
210
|
+
}
|
|
115
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
|
+
});
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
repository.update(userId, result.creationId, updates);
|
|
221
|
+
|
|
222
|
+
// Credit deduction
|
|
116
223
|
if (creditCost && creditCost > 0 && onCreditDeduct) {
|
|
117
224
|
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
118
225
|
console.log("[useCreationPersistence] Deducting credits", { cost: creditCost });
|
|
@@ -127,22 +234,36 @@ export function useCreationPersistence(
|
|
|
127
234
|
[userId, repository, creditCost, onCreditDeduct],
|
|
128
235
|
);
|
|
129
236
|
|
|
237
|
+
/**
|
|
238
|
+
* Handles generation errors
|
|
239
|
+
* Marks creation as FAILED with error context
|
|
240
|
+
*/
|
|
130
241
|
const onError = useCallback(
|
|
131
242
|
(error: string, creationId?: string) => {
|
|
132
243
|
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
133
|
-
console.
|
|
244
|
+
console.error("[useCreationPersistence] Generation error", {
|
|
245
|
+
error,
|
|
246
|
+
creationId,
|
|
247
|
+
timestamp: new Date().toISOString(),
|
|
248
|
+
});
|
|
134
249
|
}
|
|
135
250
|
|
|
136
251
|
if (!userId || !creationId) {
|
|
137
252
|
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
138
|
-
console.
|
|
253
|
+
console.warn("[useCreationPersistence] Cannot mark error - missing required fields", {
|
|
254
|
+
hasUserId: !!userId,
|
|
255
|
+
hasCreationId: !!creationId,
|
|
256
|
+
});
|
|
139
257
|
}
|
|
140
258
|
return;
|
|
141
259
|
}
|
|
142
260
|
|
|
143
261
|
repository.update(userId, creationId, {
|
|
144
|
-
|
|
145
|
-
|
|
262
|
+
[CREATION_FIELDS.STATUS]: CREATION_STATUS.FAILED,
|
|
263
|
+
[CREATION_FIELDS.METADATA]: {
|
|
264
|
+
error,
|
|
265
|
+
failedAt: new Date().toISOString(),
|
|
266
|
+
},
|
|
146
267
|
});
|
|
147
268
|
},
|
|
148
269
|
[userId, repository],
|