@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,191 @@
1
+ /**
2
+ * D1 Service
3
+ * @description Cloudflare D1 database operations
4
+ */
5
+
6
+ import type { D1QueryResult, D1BatchResult } from "../../../domain/entities/d1.entity";
7
+ import type { ID1Service } from "../../../domain/interfaces/services.interface";
8
+
9
+ export interface D1ExecOptions {
10
+ readonly binding?: string;
11
+ }
12
+
13
+ class D1Service implements ID1Service {
14
+ private databases: Map<string, D1Database> = new Map();
15
+
16
+ bindDatabase(name: string, database: D1Database): void {
17
+ this.databases.set(name, database);
18
+ }
19
+
20
+ private getDatabase(binding?: string): D1Database {
21
+ const name = binding || "default";
22
+ const database = this.databases.get(name);
23
+ if (!database) {
24
+ throw new Error(`D1 database "${name}" not bound`);
25
+ }
26
+ return database;
27
+ }
28
+
29
+ async query<T>(sql: string, params?: readonly unknown[], binding?: string): Promise<D1QueryResult<T>> {
30
+ const database = this.getDatabase(binding);
31
+
32
+ const stmt = database.prepare(sql);
33
+ const result = params ? await stmt.bind(...params).all() : await stmt.all();
34
+
35
+ return {
36
+ results: result.results as T[],
37
+ success: result.success,
38
+ meta: result.meta
39
+ ? {
40
+ duration: result.meta.duration,
41
+ rows_read: result.meta.rows_read,
42
+ rows_written: result.meta.rows_written,
43
+ last_row_id: result.meta.last_row_id,
44
+ changes: result.meta.changes,
45
+ }
46
+ : undefined,
47
+ };
48
+ }
49
+
50
+ async batch(
51
+ statements: readonly { sql: string; params?: readonly unknown[]; binding?: string }[]
52
+ ): Promise<D1BatchResult> {
53
+ // Group by binding
54
+ const grouped = new Map<string, D1PreparedStatement[]>();
55
+
56
+ for (const stmt of statements) {
57
+ const binding = stmt.binding || "default";
58
+ const database = this.getDatabase(binding);
59
+
60
+ if (!grouped.has(binding)) {
61
+ grouped.set(binding, []);
62
+ }
63
+
64
+ grouped.get(binding)!.push(database.prepare(stmt.sql));
65
+ }
66
+
67
+ // Note: D1 batch requires all statements to be from the same database
68
+ if (grouped.size > 1) {
69
+ throw new Error("Batch operations must be from the same database binding");
70
+ }
71
+
72
+ const [binding, statements] = Array.from(grouped.entries())[0];
73
+ const database = this.getDatabase(binding);
74
+
75
+ const results = await database.batch(statements as D1PreparedStatement[]);
76
+
77
+ return {
78
+ success: results.every((r) => r.success),
79
+ results: results.map((r) => ({
80
+ results: r.results,
81
+ success: r.success,
82
+ meta: r.meta,
83
+ })),
84
+ };
85
+ }
86
+
87
+ async execute<T>(sql: string, params?: readonly unknown[], binding?: string): Promise<D1QueryResult<T>> {
88
+ return this.query<T>(sql, params, binding);
89
+ }
90
+
91
+ /**
92
+ * Query helpers
93
+ */
94
+ async findOne<T>(sql: string, params?: readonly unknown[], binding?: string): Promise<T | null> {
95
+ const result = await this.query<T>(sql, params, binding);
96
+
97
+ return (result.results[0] as T) ?? null;
98
+ }
99
+
100
+ async insert<T>(
101
+ table: string,
102
+ data: Record<string, unknown>,
103
+ binding?: string
104
+ ): Promise<D1QueryResult<T>> {
105
+ const columns = Object.keys(data);
106
+ const values = Object.values(data);
107
+ const placeholders = values.map(() => "?").join(", ");
108
+
109
+ const sql = `INSERT INTO ${table} (${columns.join(", ")}) VALUES (${placeholders})`;
110
+
111
+ return this.query<T>(sql, values, binding);
112
+ }
113
+
114
+ async update<T>(
115
+ table: string,
116
+ data: Record<string, unknown>,
117
+ where: string,
118
+ whereParams: readonly unknown[] = [],
119
+ binding?: string
120
+ ): Promise<D1QueryResult<T>> {
121
+ const columns = Object.keys(data);
122
+ const values = Object.values(data);
123
+ const setClause = columns.map((col) => `${col} = ?`).join(", ");
124
+
125
+ const sql = `UPDATE ${table} SET ${setClause} WHERE ${where}`;
126
+ const params = [...values, ...whereParams];
127
+
128
+ return this.query<T>(sql, params, binding);
129
+ }
130
+
131
+ async delete(table: string, where: string, params?: readonly unknown[], binding?: string): Promise<D1QueryResult> {
132
+ const sql = `DELETE FROM ${table} WHERE ${where}`;
133
+
134
+ return this.query(sql, params, binding);
135
+ }
136
+
137
+ /**
138
+ * Schema helpers
139
+ */
140
+ async createTable(
141
+ table: string,
142
+ columns: Record<string, string>,
143
+ binding?: string
144
+ ): Promise<void> {
145
+ const columnDefs = Object.entries(columns)
146
+ .map(([name, type]) => `${name} ${type}`)
147
+ .join(", ");
148
+
149
+ const sql = `CREATE TABLE IF NOT EXISTS ${table} (${columnDefs})`;
150
+
151
+ await this.execute(sql, [], binding);
152
+ }
153
+
154
+ async dropTable(table: string, binding?: string): Promise<void> {
155
+ const sql = `DROP TABLE IF EXISTS ${table}`;
156
+
157
+ await this.execute(sql, [], binding);
158
+ }
159
+
160
+ async tableExists(table: string, binding?: string): Promise<boolean> {
161
+ const sql = `
162
+ SELECT name FROM sqlite_master
163
+ WHERE type='table' AND name=?
164
+ `;
165
+
166
+ const result = await this.query<{ name: string }>(sql, [table], binding);
167
+
168
+ return result.results.length > 0;
169
+ }
170
+
171
+ /**
172
+ * Transaction helpers
173
+ */
174
+ async runInTransaction<T>(
175
+ callback: (txn: D1Transaction) => Promise<T>,
176
+ binding?: string
177
+ ): Promise<T> {
178
+ const database = this.getDatabase(binding);
179
+
180
+ // Note: D1 doesn't have explicit transaction API
181
+ // Use batch operations or implement application-level logic
182
+
183
+ throw new Error("Transactions not yet implemented for D1");
184
+ }
185
+ }
186
+
187
+ interface D1Transaction {
188
+ query<T>(sql: string, params?: readonly unknown[]): Promise<D1QueryResult<T>>;
189
+ }
190
+
191
+ export const d1Service = new D1Service();
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Cloudflare D1 Service
3
+ * Subpath: @umituz/web-cloudflare/d1
4
+ */
5
+
6
+ export { D1Service, d1Service } from "./d1.service";
7
+ export type { D1ExecOptions } from "./d1.service";
@@ -0,0 +1,227 @@
1
+ /**
2
+ * Images Service
3
+ * @description Cloudflare Images operations
4
+ */
5
+
6
+ import type { ImageUploadResult, ImageUploadOptions, ImageTransformation, SignedURL } from "../../../domain/entities/image.entity";
7
+ import type { IImageService } from "../../../domain/interfaces/services.interface";
8
+ import { validationUtils, transformUtils } from "../../utils";
9
+ import { MAX_IMAGE_SIZE, ALLOWED_IMAGE_TYPES } from "../../constants";
10
+
11
+ export interface ImagesClientOptions {
12
+ readonly accountId: string;
13
+ readonly apiToken: string;
14
+ readonly customDomain?: string;
15
+ }
16
+
17
+ class ImagesService implements IImageService {
18
+ private accountId: string | null = null;
19
+ private apiToken: string | null = null;
20
+ private customDomain: string | null = null;
21
+
22
+ initialize(options: ImagesClientOptions): void {
23
+ this.accountId = options.accountId;
24
+ this.apiToken = options.apiToken;
25
+ this.customDomain = options.customDomain ?? null;
26
+ }
27
+
28
+ private getAPIBaseURL(): string {
29
+ if (!this.accountId) {
30
+ throw new Error("ImagesService not initialized");
31
+ }
32
+
33
+ return `https://api.cloudflare.com/client/v4/accounts/${this.accountId}/images/v1`;
34
+ }
35
+
36
+ private getAuthHeaders(): HeadersInit {
37
+ if (!this.apiToken) {
38
+ throw new Error("ImagesService not initialized");
39
+ }
40
+
41
+ return {
42
+ Authorization: `Bearer ${this.apiToken}`,
43
+ };
44
+ }
45
+
46
+ async upload(file: File | Blob, options?: ImageUploadOptions): Promise<ImageUploadResult> {
47
+ // Validate file
48
+ if (file instanceof File) {
49
+ if (!validationUtils.isValidImageType(file.type)) {
50
+ throw new Error(`Invalid image type: ${file.type}`);
51
+ }
52
+
53
+ if (!validationUtils.isValidImageSize(file.size, MAX_IMAGE_SIZE)) {
54
+ throw new Error(`Image size exceeds maximum: ${MAX_IMAGE_SIZE}`);
55
+ }
56
+ }
57
+
58
+ // Prepare form data
59
+ const formData = new FormData();
60
+ formData.append("file", file);
61
+
62
+ if (options?.metadata) {
63
+ for (const [key, value] of Object.entries(options.metadata)) {
64
+ formData.append(`metadata[${key}]`, value);
65
+ }
66
+ }
67
+
68
+ if (options?.requireSignedURLs !== undefined) {
69
+ formData.append("requireSignedURLs", options.requireSignedURLs.toString());
70
+ }
71
+
72
+ // Upload
73
+ const response = await fetch(`${this.getAPIBaseURL()}/direct_upload`, {
74
+ method: "POST",
75
+ headers: this.getAuthHeaders(),
76
+ body: formData,
77
+ });
78
+
79
+ if (!response.ok) {
80
+ const error = await response.text();
81
+ throw new Error(`Upload failed: ${error}`);
82
+ }
83
+
84
+ const data = await response.json();
85
+
86
+ return {
87
+ id: data.result.id,
88
+ filename: data.result.filename,
89
+ uploaded: new Date(data.result.uploaded),
90
+ variants: data.result.variants,
91
+ requireSignedURLs: data.result.requireSignedURLs,
92
+ };
93
+ }
94
+
95
+ async getSignedURL(imageId: string, expiresIn = 3600): Promise<SignedURL> {
96
+ const expiresAt = new Date(Date.now() + expiresIn * 1000);
97
+
98
+ const response = await fetch(`${this.getAPIBaseURL()}/${imageId}`, {
99
+ method: "POST",
100
+ headers: {
101
+ ...this.getAuthHeaders(),
102
+ "Content-Type": "application/json",
103
+ },
104
+ body: JSON.stringify({
105
+ expiry: expiresAt.toISOString(),
106
+ }),
107
+ });
108
+
109
+ if (!response.ok) {
110
+ const error = await response.text();
111
+ throw new Error(`Failed to get signed URL: ${error}`);
112
+ }
113
+
114
+ const data = await response.json();
115
+
116
+ return {
117
+ url: data.result.signedURLs?.[0] || data.result.variants?.[0],
118
+ expiresAt,
119
+ };
120
+ }
121
+
122
+ async getTransformedURL(imageId: string, transform: ImageTransformation): Promise<string> {
123
+ if (this.customDomain) {
124
+ const baseURL = `https://${this.customDomain}/${imageId}`;
125
+ return transformUtils.generateTransformURL(baseURL, transform);
126
+ }
127
+
128
+ // Fallback to Cloudflare CDN URL
129
+ const baseURL = `https://imagedelivery.net/${this.accountId}/${imageId}/public`;
130
+ return transformUtils.generateTransformURL(baseURL, transform);
131
+ }
132
+
133
+ async delete(imageId: string): Promise<boolean> {
134
+ const response = await fetch(`${this.getAPIBaseURL()}/${imageId}`, {
135
+ method: "DELETE",
136
+ headers: this.getAuthHeaders(),
137
+ });
138
+
139
+ if (!response.ok) {
140
+ const error = await response.text();
141
+ throw new Error(`Delete failed: ${error}`);
142
+ }
143
+
144
+ return true;
145
+ }
146
+
147
+ /**
148
+ * List helpers
149
+ */
150
+ async listImages(options?: { page?: number; perPage?: number }): Promise<{
151
+ images: readonly ImageUploadResult[];
152
+ totalCount: number;
153
+ }> {
154
+ const params = new URLSearchParams();
155
+ if (options?.page) params.set("page", options.page.toString());
156
+ if (options?.perPage) params.set("per_page", options.perPage.toString());
157
+
158
+ const response = await fetch(`${this.getAPIBaseURL()}?${params}`, {
159
+ headers: this.getAuthHeaders(),
160
+ });
161
+
162
+ if (!response.ok) {
163
+ const error = await response.text();
164
+ throw new Error(`List failed: ${error}`);
165
+ }
166
+
167
+ const data = await response.json();
168
+
169
+ return {
170
+ images: data.result.images,
171
+ totalCount: data.result.totalCount,
172
+ };
173
+ }
174
+
175
+ /**
176
+ * Bulk operations
177
+ */
178
+ async uploadMultiple(files: readonly (File | Blob)[], options?: ImageUploadOptions): Promise<ImageUploadResult[]> {
179
+ const uploads = files.map((file) => this.upload(file, options));
180
+
181
+ return Promise.all(uploads);
182
+ }
183
+
184
+ async deleteMultiple(imageIds: readonly string[]): Promise<boolean> {
185
+ const deletions = imageIds.map((id) => this.delete(id));
186
+
187
+ await Promise.all(deletions);
188
+
189
+ return true;
190
+ }
191
+
192
+ /**
193
+ * Variant helpers
194
+ */
195
+ generateVariantURL(imageId: string, variant: string): string {
196
+ if (this.customDomain) {
197
+ return `https://${this.customDomain}/${imageId}/${variant}`;
198
+ }
199
+
200
+ return `https://imagedelivery.net/${this.accountId}/${imageId}/${variant}`;
201
+ }
202
+
203
+ /**
204
+ * Upload from URL
205
+ */
206
+ async uploadFromURL(
207
+ url: string,
208
+ filename: string,
209
+ options?: ImageUploadOptions
210
+ ): Promise<ImageUploadResult> {
211
+ // Fetch the image
212
+ const response = await fetch(url);
213
+ if (!response.ok) {
214
+ throw new Error(`Failed to fetch image: ${response.statusText}`);
215
+ }
216
+
217
+ const blob = await response.blob();
218
+
219
+ // Upload the blob
220
+ return this.upload(
221
+ new File([blob], filename, { type: blob.type || "image/jpeg" }),
222
+ options
223
+ );
224
+ }
225
+ }
226
+
227
+ export const imagesService = new ImagesService();
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Cloudflare Images Service
3
+ * Subpath: @umituz/web-cloudflare/images
4
+ */
5
+
6
+ export { ImagesService, imagesService } from "./images.service";
7
+ export type { ImagesClientOptions } from "./images.service";
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Cloudflare KV Service
3
+ * Subpath: @umituz/web-cloudflare/kv
4
+ */
5
+
6
+ export { KVService, kvService } from "./kv.service";
7
+ export type { KVCacheOptions } from "./kv.service";
@@ -0,0 +1,116 @@
1
+ /**
2
+ * KV Service
3
+ * @description Cloudflare KV key-value storage operations
4
+ */
5
+
6
+ import type { KVEntry, KVListOptions, KVListResult } from "../../../domain/entities/kv.entity";
7
+ import type { IKVService } from "../../../domain/interfaces/services.interface";
8
+ import { validationUtils, cacheUtils } from "../../utils";
9
+
10
+ export interface KVCacheOptions {
11
+ readonly namespace: string;
12
+ readonly defaultTTL?: number;
13
+ }
14
+
15
+ class KVService implements IKVService {
16
+ private namespaces: Map<string, KVNamespace> = new Map();
17
+ private defaultTTL: number = 3600;
18
+
19
+ initialize(options: KVCacheOptions): void {
20
+ // Namespaces are bound by Cloudflare at runtime
21
+ // This is just a reference to the binding name
22
+ this.defaultTTL = options.defaultTTL ?? 3600;
23
+ }
24
+
25
+ bindNamespace(name: string, namespace: KVNamespace): void {
26
+ this.namespaces.set(name, namespace);
27
+ }
28
+
29
+ private getNamespace(binding?: string): KVNamespace {
30
+ const name = binding || "default";
31
+ const namespace = this.namespaces.get(name);
32
+ if (!namespace) {
33
+ throw new Error(`KV namespace "${name}" not bound`);
34
+ }
35
+ return namespace;
36
+ }
37
+
38
+ async get<T>(key: string, binding?: string): Promise<T | null> {
39
+ if (!validationUtils.isValidKVKey(key)) {
40
+ throw new Error(`Invalid KV key: ${key}`);
41
+ }
42
+
43
+ const namespace = this.getNamespace(binding);
44
+ const value = await namespace.get(key, "json");
45
+
46
+ return (value as T) ?? null;
47
+ }
48
+
49
+ async put<T>(key: string, value: T, options?: { ttl?: number; binding?: string }): Promise<void> {
50
+ if (!validationUtils.isValidKVKey(key)) {
51
+ throw new Error(`Invalid KV key: ${key}`);
52
+ }
53
+
54
+ const namespace = this.getNamespace(options?.binding);
55
+ const ttl = options?.ttl ?? this.defaultTTL;
56
+
57
+ await namespace.put(key, JSON.stringify(value), {
58
+ expirationTtl: ttl,
59
+ });
60
+ }
61
+
62
+ async delete(key: string, binding?: string): Promise<boolean> {
63
+ if (!validationUtils.isValidKVKey(key)) {
64
+ throw new Error(`Invalid KV key: ${key}`);
65
+ }
66
+
67
+ const namespace = this.getNamespace(binding);
68
+ await namespace.delete(key);
69
+
70
+ return true;
71
+ }
72
+
73
+ async list(options?: KVListOptions & { binding?: string }): Promise<KVListResult> {
74
+ const namespace = this.getNamespace(options?.binding);
75
+ const list = await namespace.list({
76
+ limit: options?.limit,
77
+ cursor: options?.cursor,
78
+ prefix: options?.prefix,
79
+ });
80
+
81
+ return {
82
+ keys: list.keys,
83
+ list_complete: list.list_complete,
84
+ cursor: list.cursor,
85
+ };
86
+ }
87
+
88
+ /**
89
+ * Cache helpers
90
+ */
91
+ async getOrSet<T>(
92
+ key: string,
93
+ factory: () => Promise<T>,
94
+ options?: { ttl?: number; binding?: string }
95
+ ): Promise<T> {
96
+ const cached = await this.get<T>(key, options?.binding);
97
+ if (cached !== null) return cached;
98
+
99
+ const value = await factory();
100
+ await this.put(key, value, options);
101
+
102
+ return value;
103
+ }
104
+
105
+ async invalidatePattern(prefix: string, binding?: string): Promise<void> {
106
+ const list = await this.list({ prefix, binding });
107
+
108
+ await Promise.all(
109
+ list.keys.map(async (key) => {
110
+ await this.delete(key.name, binding);
111
+ })
112
+ );
113
+ }
114
+ }
115
+
116
+ export const kvService = new KVService();
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Cloudflare R2 Service
3
+ * Subpath: @umituz/web-cloudflare/r2
4
+ */
5
+
6
+ export { R2Service, r2Service } from "./r2.service";
7
+ export type { R2UploadOptions, R2CacheOptions } from "./r2.service";