@umituz/react-native-ai-generation-content 1.64.0 → 1.65.1
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/infrastructure/repositories/creation-create.operations.ts +40 -0
- package/src/domains/creations/infrastructure/repositories/creation-delete.operations.ts +63 -0
- package/src/domains/creations/infrastructure/repositories/creation-update.operations.ts +77 -0
- package/src/domains/creations/infrastructure/repositories/creations-operations.ts +9 -210
- package/src/domains/creations/presentation/hooks/creation-persistence.types.ts +27 -0
- package/src/domains/creations/presentation/hooks/creation-validators.ts +102 -0
- package/src/domains/creations/presentation/hooks/useCreationPersistence.ts +32 -219
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.65.1",
|
|
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,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Creation Create Operations
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { setDoc } from "firebase/firestore";
|
|
6
|
+
import { type FirestorePathResolver } from "@umituz/react-native-firebase";
|
|
7
|
+
import type { Creation, CreationDocument } from "../../domain/entities/Creation";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Creates a new creation document
|
|
11
|
+
*/
|
|
12
|
+
export async function createCreation(
|
|
13
|
+
pathResolver: FirestorePathResolver,
|
|
14
|
+
userId: string,
|
|
15
|
+
creation: Creation
|
|
16
|
+
): Promise<void> {
|
|
17
|
+
const docRef = pathResolver.getDocRef(userId, creation.id);
|
|
18
|
+
if (!docRef) throw new Error("Firestore not initialized");
|
|
19
|
+
|
|
20
|
+
const data: CreationDocument = {
|
|
21
|
+
type: creation.type,
|
|
22
|
+
uri: creation.uri,
|
|
23
|
+
createdAt: creation.createdAt,
|
|
24
|
+
metadata: creation.metadata || {},
|
|
25
|
+
isShared: creation.isShared || false,
|
|
26
|
+
isFavorite: creation.isFavorite || false,
|
|
27
|
+
...(creation.status !== undefined && { status: creation.status }),
|
|
28
|
+
...(creation.output !== undefined && { output: creation.output }),
|
|
29
|
+
...(creation.prompt !== undefined && { prompt: creation.prompt }),
|
|
30
|
+
...(creation.requestId !== undefined && { requestId: creation.requestId }),
|
|
31
|
+
...(creation.model !== undefined && { model: creation.model }),
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
await setDoc(docRef, data);
|
|
36
|
+
} catch (error) {
|
|
37
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
38
|
+
throw new Error(`Failed to create creation ${creation.id}: ${errorMessage}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Creation Delete Operations
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { updateDoc, deleteDoc } from "firebase/firestore";
|
|
6
|
+
import { type FirestorePathResolver } from "@umituz/react-native-firebase";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Soft deletes a creation
|
|
10
|
+
*/
|
|
11
|
+
export async function deleteCreation(
|
|
12
|
+
pathResolver: FirestorePathResolver,
|
|
13
|
+
userId: string,
|
|
14
|
+
creationId: string
|
|
15
|
+
): Promise<boolean> {
|
|
16
|
+
const docRef = pathResolver.getDocRef(userId, creationId);
|
|
17
|
+
if (!docRef) return false;
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
await updateDoc(docRef, { deletedAt: new Date() });
|
|
21
|
+
return true;
|
|
22
|
+
} catch {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Hard deletes a creation
|
|
29
|
+
*/
|
|
30
|
+
export async function hardDeleteCreation(
|
|
31
|
+
pathResolver: FirestorePathResolver,
|
|
32
|
+
userId: string,
|
|
33
|
+
creationId: string
|
|
34
|
+
): Promise<boolean> {
|
|
35
|
+
const docRef = pathResolver.getDocRef(userId, creationId);
|
|
36
|
+
if (!docRef) return false;
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
await deleteDoc(docRef);
|
|
40
|
+
return true;
|
|
41
|
+
} catch {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Restores a soft-deleted creation
|
|
48
|
+
*/
|
|
49
|
+
export async function restoreCreation(
|
|
50
|
+
pathResolver: FirestorePathResolver,
|
|
51
|
+
userId: string,
|
|
52
|
+
creationId: string
|
|
53
|
+
): Promise<boolean> {
|
|
54
|
+
const docRef = pathResolver.getDocRef(userId, creationId);
|
|
55
|
+
if (!docRef) return false;
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
await updateDoc(docRef, { deletedAt: null });
|
|
59
|
+
return true;
|
|
60
|
+
} catch {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Creation Update Operations
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { updateDoc } from "firebase/firestore";
|
|
6
|
+
import { type FirestorePathResolver } from "@umituz/react-native-firebase";
|
|
7
|
+
import type { Creation } from "../../domain/entities/Creation";
|
|
8
|
+
import { CREATION_FIELDS, type CreationFieldName } from "../../domain/constants";
|
|
9
|
+
|
|
10
|
+
declare const __DEV__: boolean;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Updatable fields list
|
|
14
|
+
*/
|
|
15
|
+
export const UPDATABLE_FIELDS: ReadonlyArray<CreationFieldName> = [
|
|
16
|
+
CREATION_FIELDS.URI,
|
|
17
|
+
CREATION_FIELDS.STATUS,
|
|
18
|
+
CREATION_FIELDS.OUTPUT,
|
|
19
|
+
CREATION_FIELDS.IMAGE_URL,
|
|
20
|
+
CREATION_FIELDS.VIDEO_URL,
|
|
21
|
+
CREATION_FIELDS.METADATA,
|
|
22
|
+
CREATION_FIELDS.IS_SHARED,
|
|
23
|
+
CREATION_FIELDS.IS_FAVORITE,
|
|
24
|
+
CREATION_FIELDS.RATING,
|
|
25
|
+
CREATION_FIELDS.RATED_AT,
|
|
26
|
+
CREATION_FIELDS.DELETED_AT,
|
|
27
|
+
CREATION_FIELDS.REQUEST_ID,
|
|
28
|
+
CREATION_FIELDS.MODEL,
|
|
29
|
+
CREATION_FIELDS.PROMPT,
|
|
30
|
+
"type" as CreationFieldName,
|
|
31
|
+
] as const;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Updates a creation document
|
|
35
|
+
*/
|
|
36
|
+
export async function updateCreation(
|
|
37
|
+
pathResolver: FirestorePathResolver,
|
|
38
|
+
userId: string,
|
|
39
|
+
id: string,
|
|
40
|
+
updates: Partial<Creation>
|
|
41
|
+
): Promise<boolean> {
|
|
42
|
+
const docRef = pathResolver.getDocRef(userId, id);
|
|
43
|
+
|
|
44
|
+
if (!docRef) {
|
|
45
|
+
throw new Error(
|
|
46
|
+
`Cannot update: Document not found for user ${userId}, creation ${id}`
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const updateData: Record<string, unknown> = {};
|
|
51
|
+
for (const field of UPDATABLE_FIELDS) {
|
|
52
|
+
if (updates[field as keyof Creation] !== undefined) {
|
|
53
|
+
updateData[field] = updates[field as keyof Creation];
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (Object.keys(updateData).length === 0) {
|
|
58
|
+
if (__DEV__) {
|
|
59
|
+
console.warn("[updateCreation] No fields to update", { id });
|
|
60
|
+
}
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
await updateDoc(docRef, updateData);
|
|
66
|
+
if (__DEV__) {
|
|
67
|
+
console.log("[updateCreation] Updated", {
|
|
68
|
+
id,
|
|
69
|
+
fields: Object.keys(updateData),
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
return true;
|
|
73
|
+
} catch (error) {
|
|
74
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
75
|
+
throw new Error(`Failed to update creation ${id}: ${message}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -1,213 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Creation CRUD Operations
|
|
3
|
-
*
|
|
2
|
+
* Creation CRUD Operations - Barrel Export
|
|
3
|
+
* Split into modular files for maintainability
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
|
33
|
-
] as const;
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Creates a new creation document
|
|
37
|
-
*/
|
|
38
|
-
export async function createCreation(
|
|
39
|
-
pathResolver: FirestorePathResolver,
|
|
40
|
-
userId: string,
|
|
41
|
-
creation: Creation
|
|
42
|
-
): Promise<void> {
|
|
43
|
-
const docRef = pathResolver.getDocRef(userId, creation.id);
|
|
44
|
-
if (!docRef) throw new Error("Firestore not initialized");
|
|
45
|
-
|
|
46
|
-
const data: CreationDocument = {
|
|
47
|
-
type: creation.type,
|
|
48
|
-
uri: creation.uri,
|
|
49
|
-
createdAt: creation.createdAt,
|
|
50
|
-
metadata: creation.metadata || {},
|
|
51
|
-
isShared: creation.isShared || false,
|
|
52
|
-
isFavorite: creation.isFavorite || false,
|
|
53
|
-
...(creation.status !== undefined && { status: creation.status }),
|
|
54
|
-
...(creation.output !== undefined && { output: creation.output }),
|
|
55
|
-
...(creation.prompt !== undefined && { prompt: creation.prompt }),
|
|
56
|
-
...(creation.requestId !== undefined && { requestId: creation.requestId }),
|
|
57
|
-
...(creation.model !== undefined && { model: creation.model }),
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
try {
|
|
61
|
-
await setDoc(docRef, data);
|
|
62
|
-
} catch (error) {
|
|
63
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
64
|
-
throw new Error(`Failed to create creation ${creation.id}: ${errorMessage}`);
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
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
|
-
* ```
|
|
85
|
-
*/
|
|
86
|
-
export async function updateCreation(
|
|
87
|
-
pathResolver: FirestorePathResolver,
|
|
88
|
-
userId: string,
|
|
89
|
-
id: string,
|
|
90
|
-
updates: Partial<Creation>
|
|
91
|
-
): Promise<boolean> {
|
|
92
|
-
const docRef = pathResolver.getDocRef(userId, id);
|
|
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
|
-
}
|
|
106
|
-
|
|
107
|
-
try {
|
|
108
|
-
// Filter to only updatable fields
|
|
109
|
-
const updateData: Record<string, unknown> = {};
|
|
110
|
-
for (const field of UPDATABLE_FIELDS) {
|
|
111
|
-
if (updates[field as keyof Creation] !== undefined) {
|
|
112
|
-
updateData[field] = updates[field as keyof Creation];
|
|
113
|
-
}
|
|
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
|
|
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
|
-
|
|
138
|
-
return true;
|
|
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
|
-
);
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
/**
|
|
159
|
-
* Soft deletes a creation (marks as deleted)
|
|
160
|
-
*/
|
|
161
|
-
export async function deleteCreation(
|
|
162
|
-
pathResolver: FirestorePathResolver,
|
|
163
|
-
userId: string,
|
|
164
|
-
creationId: string
|
|
165
|
-
): Promise<boolean> {
|
|
166
|
-
const docRef = pathResolver.getDocRef(userId, creationId);
|
|
167
|
-
if (!docRef) return false;
|
|
168
|
-
|
|
169
|
-
try {
|
|
170
|
-
await updateDoc(docRef, { deletedAt: new Date() });
|
|
171
|
-
return true;
|
|
172
|
-
} catch {
|
|
173
|
-
return false;
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
/**
|
|
178
|
-
* Hard deletes a creation (removes from database)
|
|
179
|
-
*/
|
|
180
|
-
export async function hardDeleteCreation(
|
|
181
|
-
pathResolver: FirestorePathResolver,
|
|
182
|
-
userId: string,
|
|
183
|
-
creationId: string
|
|
184
|
-
): Promise<boolean> {
|
|
185
|
-
const docRef = pathResolver.getDocRef(userId, creationId);
|
|
186
|
-
if (!docRef) return false;
|
|
187
|
-
|
|
188
|
-
try {
|
|
189
|
-
await deleteDoc(docRef);
|
|
190
|
-
return true;
|
|
191
|
-
} catch {
|
|
192
|
-
return false;
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
/**
|
|
197
|
-
* Restores a soft-deleted creation
|
|
198
|
-
*/
|
|
199
|
-
export async function restoreCreation(
|
|
200
|
-
pathResolver: FirestorePathResolver,
|
|
201
|
-
userId: string,
|
|
202
|
-
creationId: string
|
|
203
|
-
): Promise<boolean> {
|
|
204
|
-
const docRef = pathResolver.getDocRef(userId, creationId);
|
|
205
|
-
if (!docRef) return false;
|
|
206
|
-
|
|
207
|
-
try {
|
|
208
|
-
await updateDoc(docRef, { deletedAt: null });
|
|
209
|
-
return true;
|
|
210
|
-
} catch {
|
|
211
|
-
return false;
|
|
212
|
-
}
|
|
213
|
-
}
|
|
6
|
+
export { createCreation } from "./creation-create.operations";
|
|
7
|
+
export { updateCreation, UPDATABLE_FIELDS } from "./creation-update.operations";
|
|
8
|
+
export {
|
|
9
|
+
deleteCreation,
|
|
10
|
+
hardDeleteCreation,
|
|
11
|
+
restoreCreation,
|
|
12
|
+
} from "./creation-delete.operations";
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Creation Persistence Types
|
|
3
|
+
* Type definitions for persistence hook
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface UseCreationPersistenceConfig {
|
|
7
|
+
readonly type: string;
|
|
8
|
+
readonly collectionName?: string;
|
|
9
|
+
readonly creditCost?: number;
|
|
10
|
+
readonly onCreditDeduct?: (cost: number) => Promise<void | boolean>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface BaseProcessingStartData {
|
|
14
|
+
readonly creationId: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface BaseProcessingResult {
|
|
18
|
+
readonly creationId?: string;
|
|
19
|
+
readonly imageUrl?: string;
|
|
20
|
+
readonly videoUrl?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface UseCreationPersistenceReturn {
|
|
24
|
+
readonly onProcessingStart: <T extends BaseProcessingStartData>(data: T) => void;
|
|
25
|
+
readonly onProcessingComplete: <T extends BaseProcessingResult>(result: T) => void;
|
|
26
|
+
readonly onError: (error: string, creationId?: string) => void;
|
|
27
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Creation Validation Utilities
|
|
3
|
+
*
|
|
4
|
+
* Centralized validation logic for creation persistence.
|
|
5
|
+
* Keeps validation rules separate from hook logic.
|
|
6
|
+
*
|
|
7
|
+
* @module CreationValidators
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
CREATION_VALIDATION,
|
|
12
|
+
CREATION_STATUS,
|
|
13
|
+
CREATION_FIELDS,
|
|
14
|
+
} from "../../domain/constants";
|
|
15
|
+
import type { ICreationsRepository } from "../../domain/repositories/ICreationsRepository";
|
|
16
|
+
|
|
17
|
+
declare const __DEV__: boolean;
|
|
18
|
+
|
|
19
|
+
export interface ValidationResult {
|
|
20
|
+
isValid: boolean;
|
|
21
|
+
error?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Validates that at least one URL is present
|
|
26
|
+
*/
|
|
27
|
+
export function validateHasUrl(
|
|
28
|
+
imageUrl?: string,
|
|
29
|
+
videoUrl?: string
|
|
30
|
+
): ValidationResult {
|
|
31
|
+
if (!imageUrl && !videoUrl) {
|
|
32
|
+
return {
|
|
33
|
+
isValid: false,
|
|
34
|
+
error: "No output URL provided",
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
return { isValid: true };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Validates URI protocol
|
|
42
|
+
*/
|
|
43
|
+
export function validateUriProtocol(uri: string): ValidationResult {
|
|
44
|
+
const hasValidProtocol = CREATION_VALIDATION.VALID_URI_PROTOCOLS.some(
|
|
45
|
+
(protocol) => uri.startsWith(protocol)
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
if (!hasValidProtocol) {
|
|
49
|
+
return {
|
|
50
|
+
isValid: false,
|
|
51
|
+
error: `Invalid URI protocol. Expected one of: ${CREATION_VALIDATION.VALID_URI_PROTOCOLS.join(", ")}`,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return { isValid: true };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Validates URI length
|
|
60
|
+
*/
|
|
61
|
+
export function validateUriLength(uri: string): ValidationResult {
|
|
62
|
+
if (uri.length > CREATION_VALIDATION.MAX_URI_LENGTH) {
|
|
63
|
+
return {
|
|
64
|
+
isValid: false,
|
|
65
|
+
error: `URI length (${uri.length}) exceeds maximum (${CREATION_VALIDATION.MAX_URI_LENGTH})`,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return { isValid: true };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Runs all validations and returns first error
|
|
74
|
+
*/
|
|
75
|
+
export function runAllValidations(
|
|
76
|
+
imageUrl?: string,
|
|
77
|
+
videoUrl?: string
|
|
78
|
+
): ValidationResult {
|
|
79
|
+
const urlCheck = validateHasUrl(imageUrl, videoUrl);
|
|
80
|
+
if (!urlCheck.isValid) return urlCheck;
|
|
81
|
+
|
|
82
|
+
const uri = imageUrl || videoUrl || "";
|
|
83
|
+
const protocolCheck = validateUriProtocol(uri);
|
|
84
|
+
if (!protocolCheck.isValid) return protocolCheck;
|
|
85
|
+
|
|
86
|
+
return validateUriLength(uri);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Marks creation as failed
|
|
91
|
+
*/
|
|
92
|
+
export function markCreationAsFailed(
|
|
93
|
+
repository: ICreationsRepository,
|
|
94
|
+
userId: string,
|
|
95
|
+
creationId: string,
|
|
96
|
+
error: string
|
|
97
|
+
): void {
|
|
98
|
+
repository.update(userId, creationId, {
|
|
99
|
+
[CREATION_FIELDS.STATUS]: CREATION_STATUS.FAILED,
|
|
100
|
+
[CREATION_FIELDS.METADATA]: { error, failedAt: new Date().toISOString() },
|
|
101
|
+
});
|
|
102
|
+
}
|
|
@@ -1,73 +1,36 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* useCreationPersistence Hook
|
|
3
|
-
* Encapsulates Firestore persistence logic for AI generation features
|
|
4
|
-
* Realtime listener handles UI updates automatically
|
|
5
3
|
*/
|
|
6
4
|
|
|
7
5
|
import { useCallback, useMemo } from "react";
|
|
8
6
|
import { useAuth } from "@umituz/react-native-auth";
|
|
9
7
|
import { createCreationsRepository } from "../../infrastructure/adapters";
|
|
10
8
|
import type { Creation } from "../../domain/entities/Creation";
|
|
11
|
-
import {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
9
|
+
import { CREATION_STATUS, CREATION_FIELDS } from "../../domain/constants";
|
|
10
|
+
import { runAllValidations, markCreationAsFailed } from "./creation-validators";
|
|
11
|
+
import type {
|
|
12
|
+
UseCreationPersistenceConfig,
|
|
13
|
+
UseCreationPersistenceReturn,
|
|
14
|
+
BaseProcessingStartData,
|
|
15
|
+
BaseProcessingResult,
|
|
16
|
+
} from "./creation-persistence.types";
|
|
16
17
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
export interface UseCreationPersistenceConfig {
|
|
20
|
-
readonly type: string;
|
|
21
|
-
readonly collectionName?: string;
|
|
22
|
-
readonly creditCost?: number;
|
|
23
|
-
readonly onCreditDeduct?: (cost: number) => Promise<void | boolean>;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export interface BaseProcessingStartData {
|
|
27
|
-
readonly creationId: string;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export interface BaseProcessingResult {
|
|
31
|
-
readonly creationId?: string;
|
|
32
|
-
readonly imageUrl?: string;
|
|
33
|
-
readonly videoUrl?: string;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export interface UseCreationPersistenceReturn {
|
|
37
|
-
readonly onProcessingStart: <T extends BaseProcessingStartData>(data: T) => void;
|
|
38
|
-
readonly onProcessingComplete: <T extends BaseProcessingResult>(result: T) => void;
|
|
39
|
-
readonly onError: (error: string, creationId?: string) => void;
|
|
40
|
-
}
|
|
18
|
+
export type * from "./creation-persistence.types";
|
|
41
19
|
|
|
42
20
|
export function useCreationPersistence(
|
|
43
|
-
config: UseCreationPersistenceConfig
|
|
21
|
+
config: UseCreationPersistenceConfig
|
|
44
22
|
): UseCreationPersistenceReturn {
|
|
45
23
|
const { type, collectionName = "creations", creditCost, onCreditDeduct } = config;
|
|
46
24
|
const { userId } = useAuth();
|
|
47
|
-
|
|
48
|
-
const repository = useMemo(
|
|
49
|
-
() => createCreationsRepository(collectionName),
|
|
50
|
-
[collectionName],
|
|
51
|
-
);
|
|
25
|
+
const repository = useMemo(() => createCreationsRepository(collectionName), [collectionName]);
|
|
52
26
|
|
|
53
27
|
const onProcessingStart = useCallback(
|
|
54
28
|
<T extends BaseProcessingStartData>(data: T) => {
|
|
55
|
-
if (
|
|
56
|
-
console.log("[useCreationPersistence] onProcessingStart", { type, userId });
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
if (!userId) {
|
|
60
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
61
|
-
console.log("[useCreationPersistence] No userId, skipping");
|
|
62
|
-
}
|
|
63
|
-
return;
|
|
64
|
-
}
|
|
65
|
-
|
|
29
|
+
if (!userId) return;
|
|
66
30
|
const { creationId, ...rest } = data;
|
|
67
31
|
const cleanMetadata = Object.fromEntries(
|
|
68
|
-
Object.entries(rest).filter(([, v]) => v !== undefined && v !== null)
|
|
32
|
+
Object.entries(rest).filter(([, v]) => v !== undefined && v !== null)
|
|
69
33
|
);
|
|
70
|
-
|
|
71
34
|
const creation: Creation = {
|
|
72
35
|
id: creationId,
|
|
73
36
|
uri: "",
|
|
@@ -78,199 +41,49 @@ export function useCreationPersistence(
|
|
|
78
41
|
isFavorite: false,
|
|
79
42
|
metadata: cleanMetadata,
|
|
80
43
|
};
|
|
81
|
-
|
|
82
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
83
|
-
console.log("[useCreationPersistence] Creating document", { creationId, type });
|
|
84
|
-
}
|
|
85
|
-
|
|
86
44
|
repository.create(userId, creation);
|
|
87
45
|
},
|
|
88
|
-
[userId, repository, type]
|
|
46
|
+
[userId, repository, type]
|
|
89
47
|
);
|
|
90
48
|
|
|
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
|
-
*/
|
|
101
49
|
const onProcessingComplete = useCallback(
|
|
102
50
|
<T extends BaseProcessingResult>(result: T) => {
|
|
103
|
-
if (
|
|
104
|
-
console.log("[useCreationPersistence] onProcessingComplete", {
|
|
105
|
-
creationId: result.creationId,
|
|
106
|
-
hasImageUrl: !!result.imageUrl,
|
|
107
|
-
hasVideoUrl: !!result.videoUrl,
|
|
108
|
-
});
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// Validation: User ID and Creation ID
|
|
112
|
-
if (!userId || !result.creationId) {
|
|
113
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
114
|
-
console.warn("[useCreationPersistence] Missing required fields", {
|
|
115
|
-
hasUserId: !!userId,
|
|
116
|
-
hasCreationId: !!result.creationId,
|
|
117
|
-
});
|
|
118
|
-
}
|
|
119
|
-
return;
|
|
120
|
-
}
|
|
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
|
-
}
|
|
51
|
+
if (!userId || !result.creationId) return;
|
|
129
52
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
[CREATION_FIELDS.METADATA]: {
|
|
134
|
-
error: "No output URL provided",
|
|
135
|
-
failedAt: new Date().toISOString(),
|
|
136
|
-
},
|
|
137
|
-
});
|
|
53
|
+
const validation = runAllValidations(result.imageUrl, result.videoUrl);
|
|
54
|
+
if (!validation.isValid) {
|
|
55
|
+
markCreationAsFailed(repository, userId, result.creationId, validation.error!);
|
|
138
56
|
return;
|
|
139
57
|
}
|
|
140
58
|
|
|
141
59
|
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
|
|
190
|
-
const output = result.imageUrl
|
|
191
|
-
? { imageUrl: result.imageUrl }
|
|
192
|
-
: result.videoUrl
|
|
193
|
-
? { videoUrl: result.videoUrl }
|
|
194
|
-
: undefined;
|
|
195
|
-
|
|
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> = {
|
|
60
|
+
repository.update(userId, result.creationId, {
|
|
199
61
|
[CREATION_FIELDS.URI]: uri,
|
|
200
62
|
[CREATION_FIELDS.STATUS]: CREATION_STATUS.COMPLETED,
|
|
201
|
-
[CREATION_FIELDS.OUTPUT]:
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
}
|
|
208
|
-
if (result.videoUrl) {
|
|
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
|
-
});
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
repository.update(userId, result.creationId, updates);
|
|
63
|
+
[CREATION_FIELDS.OUTPUT]: result.imageUrl
|
|
64
|
+
? { imageUrl: result.imageUrl }
|
|
65
|
+
: { videoUrl: result.videoUrl },
|
|
66
|
+
...(result.imageUrl && { [CREATION_FIELDS.IMAGE_URL]: result.imageUrl }),
|
|
67
|
+
...(result.videoUrl && { [CREATION_FIELDS.VIDEO_URL]: result.videoUrl }),
|
|
68
|
+
});
|
|
221
69
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
225
|
-
console.log("[useCreationPersistence] Deducting credits", { cost: creditCost });
|
|
226
|
-
}
|
|
227
|
-
onCreditDeduct(creditCost).catch((err) => {
|
|
228
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
229
|
-
console.error("[useCreationPersistence] Credit deduction failed", err);
|
|
230
|
-
}
|
|
231
|
-
});
|
|
70
|
+
if (creditCost && onCreditDeduct) {
|
|
71
|
+
onCreditDeduct(creditCost).catch(() => {});
|
|
232
72
|
}
|
|
233
73
|
},
|
|
234
|
-
[userId, repository, creditCost, onCreditDeduct]
|
|
74
|
+
[userId, repository, creditCost, onCreditDeduct]
|
|
235
75
|
);
|
|
236
76
|
|
|
237
|
-
/**
|
|
238
|
-
* Handles generation errors
|
|
239
|
-
* Marks creation as FAILED with error context
|
|
240
|
-
*/
|
|
241
77
|
const onError = useCallback(
|
|
242
78
|
(error: string, creationId?: string) => {
|
|
243
|
-
if (
|
|
244
|
-
|
|
245
|
-
error,
|
|
246
|
-
creationId,
|
|
247
|
-
timestamp: new Date().toISOString(),
|
|
248
|
-
});
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
if (!userId || !creationId) {
|
|
252
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
253
|
-
console.warn("[useCreationPersistence] Cannot mark error - missing required fields", {
|
|
254
|
-
hasUserId: !!userId,
|
|
255
|
-
hasCreationId: !!creationId,
|
|
256
|
-
});
|
|
257
|
-
}
|
|
258
|
-
return;
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
repository.update(userId, creationId, {
|
|
262
|
-
[CREATION_FIELDS.STATUS]: CREATION_STATUS.FAILED,
|
|
263
|
-
[CREATION_FIELDS.METADATA]: {
|
|
264
|
-
error,
|
|
265
|
-
failedAt: new Date().toISOString(),
|
|
266
|
-
},
|
|
267
|
-
});
|
|
79
|
+
if (!userId || !creationId) return;
|
|
80
|
+
markCreationAsFailed(repository, userId, creationId, error);
|
|
268
81
|
},
|
|
269
|
-
[userId, repository]
|
|
82
|
+
[userId, repository]
|
|
270
83
|
);
|
|
271
84
|
|
|
272
85
|
return useMemo(
|
|
273
86
|
() => ({ onProcessingStart, onProcessingComplete, onError }),
|
|
274
|
-
[onProcessingStart, onProcessingComplete, onError]
|
|
87
|
+
[onProcessingStart, onProcessingComplete, onError]
|
|
275
88
|
);
|
|
276
89
|
}
|