@umituz/web-cloudflare 1.0.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.
Files changed (41) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +621 -0
  3. package/package.json +87 -0
  4. package/src/config/patterns.ts +469 -0
  5. package/src/config/types.ts +648 -0
  6. package/src/domain/entities/analytics.entity.ts +47 -0
  7. package/src/domain/entities/d1.entity.ts +37 -0
  8. package/src/domain/entities/image.entity.ts +48 -0
  9. package/src/domain/entities/index.ts +11 -0
  10. package/src/domain/entities/kv.entity.ts +34 -0
  11. package/src/domain/entities/r2.entity.ts +55 -0
  12. package/src/domain/entities/worker.entity.ts +35 -0
  13. package/src/domain/index.ts +7 -0
  14. package/src/domain/interfaces/index.ts +6 -0
  15. package/src/domain/interfaces/services.interface.ts +82 -0
  16. package/src/index.ts +53 -0
  17. package/src/infrastructure/constants/index.ts +13 -0
  18. package/src/infrastructure/domain/ai-gateway.entity.ts +169 -0
  19. package/src/infrastructure/domain/workflows.entity.ts +108 -0
  20. package/src/infrastructure/middleware/index.ts +405 -0
  21. package/src/infrastructure/router/index.ts +549 -0
  22. package/src/infrastructure/services/ai-gateway/index.ts +416 -0
  23. package/src/infrastructure/services/analytics/analytics.service.ts +189 -0
  24. package/src/infrastructure/services/analytics/index.ts +7 -0
  25. package/src/infrastructure/services/d1/d1.service.ts +191 -0
  26. package/src/infrastructure/services/d1/index.ts +7 -0
  27. package/src/infrastructure/services/images/images.service.ts +227 -0
  28. package/src/infrastructure/services/images/index.ts +7 -0
  29. package/src/infrastructure/services/kv/index.ts +7 -0
  30. package/src/infrastructure/services/kv/kv.service.ts +116 -0
  31. package/src/infrastructure/services/r2/index.ts +7 -0
  32. package/src/infrastructure/services/r2/r2.service.ts +164 -0
  33. package/src/infrastructure/services/workers/index.ts +7 -0
  34. package/src/infrastructure/services/workers/workers.service.ts +164 -0
  35. package/src/infrastructure/services/workflows/index.ts +437 -0
  36. package/src/infrastructure/utils/helpers.ts +732 -0
  37. package/src/infrastructure/utils/index.ts +6 -0
  38. package/src/infrastructure/utils/utils.util.ts +150 -0
  39. package/src/presentation/hooks/cloudflare.hooks.ts +314 -0
  40. package/src/presentation/hooks/index.ts +6 -0
  41. package/src/worker.example.ts +41 -0
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Cloudflare Utilities
3
+ * Subpath: @umituz/web-cloudflare/infrastructure
4
+ */
5
+
6
+ export { dateUtils, cacheUtils, validationUtils, transformUtils } from "./utils.util";
@@ -0,0 +1,150 @@
1
+ /**
2
+ * Utility Functions
3
+ * @description Common utility functions for Cloudflare operations
4
+ */
5
+
6
+ /**
7
+ * Date utilities
8
+ */
9
+ export const dateUtils = {
10
+ /**
11
+ * Calculate expiration time from TTL
12
+ */
13
+ getExpirationTime(ttl: number): number {
14
+ return Math.floor(Date.now() / 1000) + ttl;
15
+ },
16
+
17
+ /**
18
+ * Convert timestamp to Date
19
+ */
20
+ fromTimestamp(timestamp: number): Date {
21
+ return new Date(timestamp * 1000);
22
+ },
23
+
24
+ /**
25
+ * Calculate remaining TTL from expiration
26
+ */
27
+ getRemainingTTL(expiration: number): number {
28
+ return Math.max(0, expiration - Math.floor(Date.now() / 1000));
29
+ },
30
+ };
31
+
32
+ /**
33
+ * Cache utilities
34
+ */
35
+ export const cacheUtils = {
36
+ /**
37
+ * Generate cache key from parts
38
+ */
39
+ generateKey(...parts: readonly string[]): string {
40
+ return parts.join(":");
41
+ },
42
+
43
+ /**
44
+ * Parse cache key
45
+ */
46
+ parseKey(key: string): readonly string[] {
47
+ return key.split(":");
48
+ },
49
+
50
+ /**
51
+ * Generate cache control header
52
+ */
53
+ getCacheControl(maxAge: number, immutable = false): string {
54
+ const directives = [`public`, `max-age=${maxAge}`];
55
+ if (immutable) {
56
+ directives.push("immutable");
57
+ }
58
+ return directives.join(", ");
59
+ },
60
+ };
61
+
62
+ /**
63
+ * Validation utilities
64
+ */
65
+ export const validationUtils = {
66
+ /**
67
+ * Validate KV key
68
+ */
69
+ isValidKVKey(key: string): boolean {
70
+ return /^[\w\-.:]+$/.test(key) && key.length <= 512;
71
+ },
72
+
73
+ /**
74
+ * Validate R2 object key
75
+ */
76
+ isValidR2Key(key: string): boolean {
77
+ return key.length > 0 && key.length <= 1024 && !key.includes("//");
78
+ },
79
+
80
+ /**
81
+ * Validate image file type
82
+ */
83
+ isValidImageType(type: string): boolean {
84
+ return ["image/jpeg", "image/png", "image/gif", "image/webp", "image/avif"].includes(type);
85
+ },
86
+
87
+ /**
88
+ * Validate image size
89
+ */
90
+ isValidImageSize(size: number, maxSize = 10 * 1024 * 1024): boolean {
91
+ return size > 0 && size <= maxSize;
92
+ },
93
+ };
94
+
95
+ /**
96
+ * Transformation utilities
97
+ */
98
+ export const transformUtils = {
99
+ /**
100
+ * Convert file to blob
101
+ */
102
+ async fileToBlob(file: File): Promise<Blob> {
103
+ return file;
104
+ },
105
+
106
+ /**
107
+ * Convert blob to array buffer
108
+ */
109
+ async blobToArrayBuffer(blob: Blob): Promise<ArrayBuffer> {
110
+ return await blob.arrayBuffer();
111
+ },
112
+
113
+ /**
114
+ * Convert readable stream to blob
115
+ */
116
+ async streamToBlob(stream: ReadableStream): Promise<Blob> {
117
+ const reader = stream.getReader();
118
+ const chunks: Uint8Array[] = [];
119
+
120
+ while (true) {
121
+ const { done, value } = await reader.read();
122
+ if (done) break;
123
+ chunks.push(value);
124
+ }
125
+
126
+ return new Blob(chunks);
127
+ },
128
+
129
+ /**
130
+ * Generate image transformation URL
131
+ */
132
+ generateTransformURL(baseUrl: string, transform: {
133
+ width?: number;
134
+ height?: number;
135
+ fit?: string;
136
+ format?: string;
137
+ quality?: number;
138
+ }): string {
139
+ const url = new URL(baseUrl);
140
+ const params = url.searchParams;
141
+
142
+ if (transform.width) params.set("width", transform.width.toString());
143
+ if (transform.height) params.set("height", transform.height.toString());
144
+ if (transform.fit) params.set("fit", transform.fit);
145
+ if (transform.format) params.set("format", transform.format);
146
+ if (transform.quality) params.set("quality", transform.quality.toString());
147
+
148
+ return url.toString();
149
+ },
150
+ };
@@ -0,0 +1,314 @@
1
+ /**
2
+ * Cloudflare React Hooks
3
+ * @description React hooks for Cloudflare services (client-side)
4
+ */
5
+
6
+ import { useCallback, useState, useEffect } from "react";
7
+ import { kvService } from "../../infrastructure/services/kv";
8
+ import { r2Service } from "../../infrastructure/services/r2";
9
+ import { imagesService } from "../../infrastructure/services/images";
10
+ import { analyticsService } from "../../infrastructure/services/analytics";
11
+
12
+ /**
13
+ * Cloudflare Worker Hook
14
+ * @description Fetch data from Cloudflare Workers
15
+ */
16
+ export interface UseCloudflareWorkerOptions {
17
+ readonly enabled?: boolean;
18
+ readonly refetchInterval?: number;
19
+ }
20
+
21
+ export function useCloudflareWorker<T = unknown>(
22
+ url: string,
23
+ options?: UseCloudflareWorkerOptions
24
+ ) {
25
+ const [data, setData] = useState<T | null>(null);
26
+ const [loading, setLoading] = useState(false);
27
+ const [error, setError] = useState<Error | null>(null);
28
+
29
+ const fetcher = useCallback(async () => {
30
+ if (!options?.enabled ?? true) {
31
+ setLoading(true);
32
+ setError(null);
33
+
34
+ try {
35
+ const response = await fetch(url);
36
+ if (!response.ok) {
37
+ throw new Error(`HTTP error! status: ${response.status}`);
38
+ }
39
+ const json = await response.json();
40
+ setData(json);
41
+ } catch (err) {
42
+ setError(err as Error);
43
+ } finally {
44
+ setLoading(false);
45
+ }
46
+ }
47
+ }, [url, options?.enabled]);
48
+
49
+ useEffect(() => {
50
+ fetcher();
51
+
52
+ if (options?.refetchInterval) {
53
+ const interval = setInterval(fetcher, options.refetchInterval);
54
+ return () => clearInterval(interval);
55
+ }
56
+ }, [fetcher, options?.refetchInterval]);
57
+
58
+ return { data, loading, error, refetch: fetcher };
59
+ }
60
+
61
+ /**
62
+ * Cloudflare KV Hook
63
+ * @description Read from Cloudflare KV (client-side, via Worker API)
64
+ */
65
+ export interface UseCloudflareKVOptions {
66
+ readonly apiURL: string;
67
+ readonly enabled?: boolean;
68
+ }
69
+
70
+ export function useCloudflareKV<T = unknown>(
71
+ key: string,
72
+ options: UseCloudflareKVOptions
73
+ ) {
74
+ const [data, setData] = useState<T | null>(null);
75
+ const [loading, setLoading] = useState(false);
76
+ const [error, setError] = useState<Error | null>(null);
77
+
78
+ useEffect(() => {
79
+ if (!options.enabled ?? true) {
80
+ setLoading(true);
81
+ setError(null);
82
+
83
+ fetch(`${options.apiURL}/kv/${key}`)
84
+ .then((res) => {
85
+ if (!res.ok) throw new Error(`HTTP error! status: ${res.status}`);
86
+ return res.json();
87
+ })
88
+ .then((json) => setData(json.value))
89
+ .catch((err) => setError(err))
90
+ .finally(() => setLoading(false));
91
+ }
92
+ }, [key, options.apiURL, options.enabled]);
93
+
94
+ return { data, loading, error };
95
+ }
96
+
97
+ /**
98
+ * Cloudflare R2 Hook
99
+ * @description Upload files to R2 (client-side, via Worker API)
100
+ */
101
+ export interface UseCloudflareR2Options {
102
+ readonly uploadURL: string;
103
+ }
104
+
105
+ export function useCloudflareR2(options: UseCloudflareR2Options) {
106
+ const [uploading, setUploading] = useState(false);
107
+ const [progress, setProgress] = useState(0);
108
+ const [error, setError] = useState<Error | null>(null);
109
+
110
+ const upload = useCallback(
111
+ async (file: File, key?: string) => {
112
+ setUploading(true);
113
+ setProgress(0);
114
+ setError(null);
115
+
116
+ try {
117
+ const formData = new FormData();
118
+ formData.append("file", file);
119
+ if (key) formData.append("key", key);
120
+
121
+ const xhr = new XMLHttpRequest();
122
+
123
+ xhr.upload.addEventListener("progress", (e) => {
124
+ if (e.lengthComputable) {
125
+ setProgress((e.loaded / e.total) * 100);
126
+ }
127
+ });
128
+
129
+ const promise = new Promise<{ key: string; url: string }>((resolve, reject) => {
130
+ xhr.addEventListener("load", () => {
131
+ if (xhr.status === 200) {
132
+ const data = JSON.parse(xhr.responseText);
133
+ resolve(data);
134
+ } else {
135
+ reject(new Error(`Upload failed: ${xhr.statusText}`));
136
+ }
137
+ });
138
+
139
+ xhr.addEventListener("error", () => {
140
+ reject(new Error("Upload failed"));
141
+ });
142
+
143
+ xhr.open("POST", options.uploadURL);
144
+ xhr.send(formData);
145
+ });
146
+
147
+ return await promise;
148
+ } catch (err) {
149
+ setError(err as Error);
150
+ throw err;
151
+ } finally {
152
+ setUploading(false);
153
+ setProgress(0);
154
+ }
155
+ },
156
+ [options.uploadURL]
157
+ );
158
+
159
+ return { upload, uploading, progress, error };
160
+ }
161
+
162
+ /**
163
+ * Cloudflare D1 Hook
164
+ * @description Query D1 database (client-side, via Worker API)
165
+ */
166
+ export interface UseCloudflareD1Options {
167
+ readonly apiURL: string;
168
+ readonly enabled?: boolean;
169
+ }
170
+
171
+ export function useCloudflareD1<T = unknown>(
172
+ query: string,
173
+ params?: readonly unknown[],
174
+ options?: UseCloudflareD1Options
175
+ ) {
176
+ const [data, setData] = useState<T[] | null>(null);
177
+ const [loading, setLoading] = useState(false);
178
+ const [error, setError] = useState<Error | null>(null);
179
+
180
+ useEffect(() => {
181
+ if (!options?.enabled ?? true) {
182
+ setLoading(true);
183
+ setError(null);
184
+
185
+ fetch(`${options?.apiURL}/d1/query`, {
186
+ method: "POST",
187
+ headers: { "Content-Type": "application/json" },
188
+ body: JSON.stringify({ query, params }),
189
+ })
190
+ .then((res) => {
191
+ if (!res.ok) throw new Error(`HTTP error! status: ${res.status}`);
192
+ return res.json();
193
+ })
194
+ .then((json) => setData(json.results))
195
+ .catch((err) => setError(err))
196
+ .finally(() => setLoading(false));
197
+ }
198
+ }, [query, params, options?.apiURL, options?.enabled]);
199
+
200
+ return { data, loading, error };
201
+ }
202
+
203
+ /**
204
+ * Cloudflare Images Hook
205
+ * @description Upload and manage images
206
+ */
207
+ export interface UseCloudflareImagesOptions {
208
+ readonly accountId?: string;
209
+ readonly apiToken?: string;
210
+ readonly uploadURL?: string;
211
+ }
212
+
213
+ export function useCloudflareImages(options?: UseCloudflareImagesOptions) {
214
+ const [uploading, setUploading] = useState(false);
215
+ const [error, setError] = useState<Error | null>(null);
216
+
217
+ useEffect(() => {
218
+ if (options?.accountId && options?.apiToken) {
219
+ imagesService.initialize({
220
+ accountId: options.accountId,
221
+ apiToken: options.apiToken,
222
+ });
223
+ }
224
+ }, [options?.accountId, options?.apiToken]);
225
+
226
+ const upload = useCallback(
227
+ async (file: File, metadata?: Record<string, string>) => {
228
+ setUploading(true);
229
+ setError(null);
230
+
231
+ try {
232
+ if (options?.uploadURL) {
233
+ // Upload via Worker proxy
234
+ const formData = new FormData();
235
+ formData.append("file", file);
236
+ if (metadata) {
237
+ for (const [key, value] of Object.entries(metadata)) {
238
+ formData.append(`metadata[${key}]`, value);
239
+ }
240
+ }
241
+
242
+ const response = await fetch(options.uploadURL, {
243
+ method: "POST",
244
+ body: formData,
245
+ });
246
+
247
+ if (!response.ok) {
248
+ throw new Error(`Upload failed: ${response.statusText}`);
249
+ }
250
+
251
+ return await response.json();
252
+ } else {
253
+ // Direct upload to Cloudflare Images
254
+ return await imagesService.upload(file, { metadata });
255
+ }
256
+ } catch (err) {
257
+ setError(err as Error);
258
+ throw err;
259
+ } finally {
260
+ setUploading(false);
261
+ }
262
+ },
263
+ [options?.uploadURL]
264
+ );
265
+
266
+ const getTransformedURL = useCallback(
267
+ (imageId: string, transform: {
268
+ width?: number;
269
+ height?: number;
270
+ fit?: string;
271
+ format?: string;
272
+ quality?: number;
273
+ }) => {
274
+ return imagesService.getTransformedURL(imageId, transform);
275
+ },
276
+ []
277
+ );
278
+
279
+ return { upload, uploading, error, getTransformedURL };
280
+ }
281
+
282
+ /**
283
+ * Cloudflare Analytics Hook
284
+ * @description Track analytics events
285
+ */
286
+ export interface UseCloudflareAnalyticsOptions {
287
+ readonly siteId: string;
288
+ readonly scriptUrl?: string;
289
+ }
290
+
291
+ export function useCloudflareAnalytics(options: UseCloudflareAnalyticsOptions) {
292
+ useEffect(() => {
293
+ analyticsService.initialize({
294
+ siteId: options.siteId,
295
+ scriptUrl: options.scriptUrl,
296
+ });
297
+ }, [options.siteId, options.scriptUrl]);
298
+
299
+ const trackPageview = useCallback((title: string, referrer?: string) => {
300
+ if (typeof window !== "undefined") {
301
+ analyticsService.trackPageview(window.location.href, title, referrer);
302
+ }
303
+ }, []);
304
+
305
+ const trackEvent = useCallback((eventName: string, data?: Record<string, unknown>) => {
306
+ analyticsService.trackCustom(eventName, data);
307
+ }, []);
308
+
309
+ const getScriptTag = useCallback(() => {
310
+ return analyticsService.getScriptTag();
311
+ }, []);
312
+
313
+ return { trackPageview, trackEvent, getScriptTag };
314
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Cloudflare React Hooks
3
+ * Subpath: @umituz/web-cloudflare/presentation
4
+ */
5
+
6
+ export { useCloudflareWorker, useCloudflareKV, useCloudflareR2, useCloudflareD1, useCloudflareImages, useCloudflareAnalytics } from "./cloudflare.hooks";
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Cloudflare Worker Example
3
+ * @description Example worker using @umituz/web-cloudflare package
4
+ */
5
+
6
+ import { workersService, kvService, r2Service } from "@umituz/web-cloudflare";
7
+
8
+ // Configure routes
9
+ workersService.route("/", async () => {
10
+ return workersService.json({ message: "Hello from edge!" });
11
+ });
12
+
13
+ workersService.route("/api/hello", async (request) => {
14
+ const { name } = await request.json();
15
+
16
+ return workersService.json({ message: `Hello, ${name}!` });
17
+ });
18
+
19
+ workersService.route("/api/cache/:key", async (request, env) => {
20
+ const url = new URL(request.url);
21
+ const key = url.pathname.split("/").pop();
22
+
23
+ if (request.method === "GET") {
24
+ const value = await kvService.get(key, env?.KV ? "KV" : undefined);
25
+ return workersService.json({ key, value });
26
+ }
27
+
28
+ if (request.method === "POST") {
29
+ const { value } = await request.json();
30
+ await kvService.put(key, value, { ttl: 3600 });
31
+ return workersService.json({ success: true });
32
+ }
33
+
34
+ return workersService.error("Method not allowed", 405);
35
+ });
36
+
37
+ // Export for Cloudflare Workers
38
+ export default {
39
+ fetch: (request: Request, env: Env, ctx: ExecutionContext) =>
40
+ workersService.fetch(request, env, ctx),
41
+ };