@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.
- package/LICENSE +21 -0
- package/README.md +621 -0
- package/package.json +87 -0
- package/src/config/patterns.ts +469 -0
- package/src/config/types.ts +648 -0
- package/src/domain/entities/analytics.entity.ts +47 -0
- package/src/domain/entities/d1.entity.ts +37 -0
- package/src/domain/entities/image.entity.ts +48 -0
- package/src/domain/entities/index.ts +11 -0
- package/src/domain/entities/kv.entity.ts +34 -0
- package/src/domain/entities/r2.entity.ts +55 -0
- package/src/domain/entities/worker.entity.ts +35 -0
- package/src/domain/index.ts +7 -0
- package/src/domain/interfaces/index.ts +6 -0
- package/src/domain/interfaces/services.interface.ts +82 -0
- package/src/index.ts +53 -0
- package/src/infrastructure/constants/index.ts +13 -0
- package/src/infrastructure/domain/ai-gateway.entity.ts +169 -0
- package/src/infrastructure/domain/workflows.entity.ts +108 -0
- package/src/infrastructure/middleware/index.ts +405 -0
- package/src/infrastructure/router/index.ts +549 -0
- package/src/infrastructure/services/ai-gateway/index.ts +416 -0
- package/src/infrastructure/services/analytics/analytics.service.ts +189 -0
- package/src/infrastructure/services/analytics/index.ts +7 -0
- package/src/infrastructure/services/d1/d1.service.ts +191 -0
- package/src/infrastructure/services/d1/index.ts +7 -0
- package/src/infrastructure/services/images/images.service.ts +227 -0
- package/src/infrastructure/services/images/index.ts +7 -0
- package/src/infrastructure/services/kv/index.ts +7 -0
- package/src/infrastructure/services/kv/kv.service.ts +116 -0
- package/src/infrastructure/services/r2/index.ts +7 -0
- package/src/infrastructure/services/r2/r2.service.ts +164 -0
- package/src/infrastructure/services/workers/index.ts +7 -0
- package/src/infrastructure/services/workers/workers.service.ts +164 -0
- package/src/infrastructure/services/workflows/index.ts +437 -0
- package/src/infrastructure/utils/helpers.ts +732 -0
- package/src/infrastructure/utils/index.ts +6 -0
- package/src/infrastructure/utils/utils.util.ts +150 -0
- package/src/presentation/hooks/cloudflare.hooks.ts +314 -0
- package/src/presentation/hooks/index.ts +6 -0
- package/src/worker.example.ts +41 -0
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* R2 Service
|
|
3
|
+
* @description Cloudflare R2 object storage operations
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { R2Object, R2ListOptions, R2ListResult, R2PutOptions, R2PresignedURL } from "../../../domain/entities/r2.entity";
|
|
7
|
+
import type { IR2Service } from "../../../domain/interfaces/services.interface";
|
|
8
|
+
import { validationUtils } from "../../utils";
|
|
9
|
+
|
|
10
|
+
export interface R2UploadOptions extends R2PutOptions {
|
|
11
|
+
readonly binding?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface R2CacheOptions {
|
|
15
|
+
readonly bucket: string;
|
|
16
|
+
readonly customDomain?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
class R2Service implements IR2Service {
|
|
20
|
+
private buckets: Map<string, R2Bucket> = new Map();
|
|
21
|
+
private customDomain: string | null = null;
|
|
22
|
+
|
|
23
|
+
initialize(options: R2CacheOptions): void {
|
|
24
|
+
this.customDomain = options.customDomain ?? null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
bindBucket(name: string, bucket: R2Bucket): void {
|
|
28
|
+
this.buckets.set(name, bucket);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
private getBucket(binding?: string): R2Bucket {
|
|
32
|
+
const name = binding || "default";
|
|
33
|
+
const bucket = this.buckets.get(name);
|
|
34
|
+
if (!bucket) {
|
|
35
|
+
throw new Error(`R2 bucket "${name}" not bound`);
|
|
36
|
+
}
|
|
37
|
+
return bucket;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async get(key: string, binding?: string): Promise<R2Object | null> {
|
|
41
|
+
if (!validationUtils.isValidR2Key(key)) {
|
|
42
|
+
throw new Error(`Invalid R2 key: ${key}`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const bucket = this.getBucket(binding);
|
|
46
|
+
const object = await bucket.get(key);
|
|
47
|
+
|
|
48
|
+
if (!object) return null;
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
key: object.key,
|
|
52
|
+
size: object.size,
|
|
53
|
+
uploaded: object.uploaded,
|
|
54
|
+
httpMetadata: object.httpMetadata,
|
|
55
|
+
customMetadata: object.customMetadata,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async put(
|
|
60
|
+
key: string,
|
|
61
|
+
data: ReadableStream | ArrayBuffer | string,
|
|
62
|
+
options?: R2UploadOptions
|
|
63
|
+
): Promise<void> {
|
|
64
|
+
if (!validationUtils.isValidR2Key(key)) {
|
|
65
|
+
throw new Error(`Invalid R2 key: ${key}`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const bucket = this.getBucket(options?.binding);
|
|
69
|
+
|
|
70
|
+
await bucket.put(key, data, {
|
|
71
|
+
httpMetadata: options?.httpMetadata,
|
|
72
|
+
customMetadata: options?.customMetadata,
|
|
73
|
+
checksum: options?.checksum,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async delete(key: string, binding?: string): Promise<boolean> {
|
|
78
|
+
if (!validationUtils.isValidR2Key(key)) {
|
|
79
|
+
throw new Error(`Invalid R2 key: ${key}`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const bucket = this.getBucket(binding);
|
|
83
|
+
await bucket.delete(key);
|
|
84
|
+
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async list(options?: R2ListOptions & { binding?: string }): Promise<R2ListResult> {
|
|
89
|
+
const bucket = this.getBucket(options?.binding);
|
|
90
|
+
const listed = await bucket.list({
|
|
91
|
+
limit: options?.limit,
|
|
92
|
+
prefix: options?.prefix,
|
|
93
|
+
cursor: options?.cursor,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
objects: listed.objects,
|
|
98
|
+
truncated: listed.truncated,
|
|
99
|
+
cursor: listed.cursor,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async getPresignedURL(key: string, expiresIn = 3600, binding?: string): Promise<R2PresignedURL> {
|
|
104
|
+
// Note: R2 presigned URLs require AWS S3 signature
|
|
105
|
+
// This would typically use the AWS SDK or custom signing logic
|
|
106
|
+
// For now, return a placeholder
|
|
107
|
+
|
|
108
|
+
const expiresAt = new Date(Date.now() + expiresIn * 1000);
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
url: `https://presigned-url-placeholder/${key}?expires=${expiresAt.getTime()}`,
|
|
112
|
+
expiresAt,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Upload helpers
|
|
118
|
+
*/
|
|
119
|
+
async uploadFile(file: File, key?: string, options?: R2UploadOptions, binding?: string): Promise<void> {
|
|
120
|
+
const objectKey = key || `uploads/${Date.now()}-${file.name}`;
|
|
121
|
+
|
|
122
|
+
await this.put(objectKey, file.stream(), {
|
|
123
|
+
...options,
|
|
124
|
+
httpMetadata: {
|
|
125
|
+
contentType: file.type,
|
|
126
|
+
...options?.httpMetadata,
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async uploadFromURL(url: string, key: string, binding?: string): Promise<void> {
|
|
132
|
+
const response = await fetch(url);
|
|
133
|
+
if (!response.ok) {
|
|
134
|
+
throw new Error(`Failed to fetch URL: ${response.statusText}`);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
await this.put(key, response.body, {
|
|
138
|
+
httpMetadata: {
|
|
139
|
+
contentType: response.headers.get("Content-Type") || undefined,
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Delete helpers
|
|
146
|
+
*/
|
|
147
|
+
async deleteMultiple(keys: readonly string[], binding?: string): Promise<void> {
|
|
148
|
+
const bucket = this.getBucket(binding);
|
|
149
|
+
|
|
150
|
+
await Promise.all(keys.map((key) => bucket.delete(key)));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async deletePrefix(prefix: string, binding?: string): Promise<void> {
|
|
154
|
+
const list = await this.list({ prefix, binding });
|
|
155
|
+
|
|
156
|
+
await this.deleteMultiple(list.objects.map((obj) => obj.key), binding);
|
|
157
|
+
|
|
158
|
+
if (list.truncated && list.cursor) {
|
|
159
|
+
await this.deletePrefix(prefix, binding);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export const r2Service = new R2Service();
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workers Service
|
|
3
|
+
* @description Cloudflare Workers HTTP handler and routing
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { WorkerRequest, WorkerResponse, WorkerConfig } from "../../../domain/entities/worker.entity";
|
|
7
|
+
import type { Env } from "../../../domain/interfaces/services.interface";
|
|
8
|
+
|
|
9
|
+
export interface WorkerFetchOptions {
|
|
10
|
+
readonly cache?: CacheControls;
|
|
11
|
+
readonly cors?: CorsOptions;
|
|
12
|
+
readonly timeout?: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface CacheControls {
|
|
16
|
+
readonly maxAge?: number;
|
|
17
|
+
readonly immutable?: boolean;
|
|
18
|
+
readonly noCache?: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface CorsOptions {
|
|
22
|
+
readonly origins?: readonly string[];
|
|
23
|
+
readonly methods?: readonly string[];
|
|
24
|
+
readonly headers?: readonly string[];
|
|
25
|
+
readonly credentials?: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface WorkerRouteConfig {
|
|
29
|
+
readonly pattern: URLPattern;
|
|
30
|
+
readonly handler: (request: WorkerRequest, env?: Env) => Promise<WorkerResponse>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export type WorkersMiddleware = (
|
|
34
|
+
request: WorkerRequest,
|
|
35
|
+
env?: Env
|
|
36
|
+
) => Promise<WorkerResponse | null>;
|
|
37
|
+
|
|
38
|
+
class WorkersService {
|
|
39
|
+
private routes: WorkerRouteConfig[] = [];
|
|
40
|
+
private middleware: WorkersMiddleware[] = [];
|
|
41
|
+
private cache: Cache | null = null;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Register a route
|
|
45
|
+
*/
|
|
46
|
+
route(pattern: string, handler: WorkerRouteConfig["handler"]): void {
|
|
47
|
+
this.routes.push({
|
|
48
|
+
pattern: new URLPattern({ pathname: pattern }),
|
|
49
|
+
handler,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Add middleware
|
|
55
|
+
*/
|
|
56
|
+
use(middleware: WorkersMiddleware): void {
|
|
57
|
+
this.middleware.push(middleware);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Fetch handler
|
|
62
|
+
*/
|
|
63
|
+
async fetch(request: WorkerRequest, env?: Env, ctx?: ExecutionContext): Promise<WorkerResponse> {
|
|
64
|
+
// Initialize cache
|
|
65
|
+
if (!this.cache && env) {
|
|
66
|
+
this.cache = caches.default;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Try middleware
|
|
70
|
+
for (const mw of this.middleware) {
|
|
71
|
+
const response = await mw(request, env);
|
|
72
|
+
if (response) return response;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Match route
|
|
76
|
+
const url = new URL(request.url);
|
|
77
|
+
for (const route of this.routes) {
|
|
78
|
+
if (route.pattern.test(url)) {
|
|
79
|
+
return route.handler(request, env);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// 404
|
|
84
|
+
return new Response("Not Found", { status: 404 });
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* JSON response helper
|
|
89
|
+
*/
|
|
90
|
+
json(data: unknown, status = 200): WorkerResponse {
|
|
91
|
+
return new Response(JSON.stringify(data), {
|
|
92
|
+
status,
|
|
93
|
+
headers: {
|
|
94
|
+
"Content-Type": "application/json",
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Error response helper
|
|
101
|
+
*/
|
|
102
|
+
error(message: string, status = 500): WorkerResponse {
|
|
103
|
+
return this.json({ error: message }, status);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* CORS response helper
|
|
108
|
+
*/
|
|
109
|
+
cors(options: CorsOptions = {}): WorkerResponse {
|
|
110
|
+
const headers = new Headers();
|
|
111
|
+
|
|
112
|
+
if (options.origins) {
|
|
113
|
+
const origin = options.origins.join(", ");
|
|
114
|
+
headers.set("Access-Control-Allow-Origin", origin);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (options.methods) {
|
|
118
|
+
headers.set("Access-Control-Allow-Methods", options.methods.join(", "));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (options.headers) {
|
|
122
|
+
headers.set("Access-Control-Allow-Headers", options.headers.join(", "));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (options.credentials) {
|
|
126
|
+
headers.set("Access-Control-Allow-Credentials", "true");
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return new Response(null, { headers });
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Cache response helper
|
|
134
|
+
*/
|
|
135
|
+
async cached(request: Request, ttl: number, fetcher: () => Promise<Response>): Promise<Response> {
|
|
136
|
+
if (!this.cache) return fetcher();
|
|
137
|
+
|
|
138
|
+
const cacheKey = new Request(request.url, request);
|
|
139
|
+
let response = await this.cache.match(cacheKey);
|
|
140
|
+
|
|
141
|
+
if (!response) {
|
|
142
|
+
response = await fetcher();
|
|
143
|
+
response = new Response(response.body, response);
|
|
144
|
+
response.headers.set("Cache-Control", `public, max-age=${ttl}`);
|
|
145
|
+
await this.cache.put(cacheKey, response.clone());
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return response;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Redirect response helper
|
|
153
|
+
*/
|
|
154
|
+
redirect(url: string, status = 302): WorkerResponse {
|
|
155
|
+
return new Response(null, {
|
|
156
|
+
status,
|
|
157
|
+
headers: {
|
|
158
|
+
Location: url,
|
|
159
|
+
},
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export const workersService = new WorkersService();
|