create-cloesce 0.4.0 → 0.5.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/templates/default/package.json +12 -13
- package/templates/default/src/api/main.ts +43 -35
- package/templates/default/src/schema/schema.clo +79 -70
- package/templates/default/src/web/index.html +191 -189
- package/templates/default/src/web/index.ts +99 -98
- package/templates/default/src/web/tsconfig.json +17 -0
- package/templates/default/test/setup.ts +28 -0
- package/templates/default/test/weather.test.ts +38 -80
- package/templates/default/tsconfig.json +5 -11
- package/templates/default/vite.config.ts +5 -3
- package/templates/default/vitest.config.ts +22 -3
package/package.json
CHANGED
|
@@ -1,28 +1,27 @@
|
|
|
1
1
|
{
|
|
2
|
-
"name": "
|
|
2
|
+
"name": "examples-weather",
|
|
3
3
|
"version": "1.0.0",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
7
|
-
"migrate:wrangler": "wrangler d1 migrations apply
|
|
7
|
+
"migrate:wrangler": "wrangler d1 migrations apply Db",
|
|
8
8
|
"start:dev": "wrangler dev --port 5000",
|
|
9
9
|
"start:web": "vite",
|
|
10
10
|
"test": "vitest"
|
|
11
11
|
},
|
|
12
|
+
"keywords": [],
|
|
13
|
+
"author": "",
|
|
14
|
+
"license": "ISC",
|
|
15
|
+
"type": "module",
|
|
12
16
|
"dependencies": {
|
|
13
|
-
"cloesce": ">=0.
|
|
17
|
+
"cloesce": ">=0.5.0",
|
|
14
18
|
"wrangler": "^4.61.1"
|
|
15
19
|
},
|
|
16
20
|
"devDependencies": {
|
|
17
|
-
"@cloudflare/workers-types": "^4.
|
|
18
|
-
"
|
|
19
|
-
"vite-tsconfig-paths": "^6.0.5",
|
|
20
|
-
"vitest": "^4.0.18",
|
|
21
|
+
"@cloudflare/workers-types": "^4.20250906.0",
|
|
22
|
+
"@cloudflare/vitest-pool-workers": "^0.16.15",
|
|
21
23
|
"typescript": "^5.9.3",
|
|
22
|
-
"vite": "^
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
"author": "",
|
|
26
|
-
"license": "ISC",
|
|
27
|
-
"type": "module"
|
|
24
|
+
"vite": "^8.0.10",
|
|
25
|
+
"vitest": "^4.0.18"
|
|
26
|
+
}
|
|
28
27
|
}
|
|
@@ -1,12 +1,10 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
1
|
+
// Import the generated backend code, which includes all the types defined in the
|
|
2
|
+
// `schema.clo` file.
|
|
3
3
|
import * as clo from "@cloesce/backend.js";
|
|
4
|
-
import { CfReadableStream } from "@cloesce/backend.js";
|
|
5
4
|
|
|
6
5
|
// The "cloesce" library provides basic types and utilities for building a Cloesce backend
|
|
7
6
|
import { HttpResult } from "cloesce";
|
|
8
7
|
|
|
9
|
-
|
|
10
8
|
// To implement the API routes of a model or service defined in `schema.clo`,
|
|
11
9
|
// we can use the `impl` method on it's respective generated namespace.
|
|
12
10
|
//
|
|
@@ -15,11 +13,14 @@ import { HttpResult } from "cloesce";
|
|
|
15
13
|
//
|
|
16
14
|
// The only place where generated code should be directly used is in `impl` blocks like this.
|
|
17
15
|
export const Weather = clo.Weather.impl({
|
|
18
|
-
async uploadPhoto(self,
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
|
|
22
|
-
|
|
16
|
+
async uploadPhoto(self, env, stream) {
|
|
17
|
+
// At runtime, Cloesce "upgrades" the Cloudflare Environment (clo.CfEnv)
|
|
18
|
+
// to a "Cloesce Environment" (clo.Env) which includes helper methods for
|
|
19
|
+
// every binding defined in our schema.
|
|
20
|
+
//
|
|
21
|
+
// For example, the "photos" template in the "Bucket" R2 binding generates
|
|
22
|
+
// a helper for uploading files to R2, which we can call like this:
|
|
23
|
+
await env.Bucket.photos.put(self.id, stream);
|
|
23
24
|
},
|
|
24
25
|
|
|
25
26
|
downloadPhoto(self) {
|
|
@@ -29,43 +30,50 @@ export const Weather = clo.Weather.impl({
|
|
|
29
30
|
return HttpResult.fail(404, "Photo not found");
|
|
30
31
|
}
|
|
31
32
|
return HttpResult.ok(200, self.photo.body);
|
|
32
|
-
}
|
|
33
|
+
},
|
|
33
34
|
});
|
|
34
35
|
|
|
35
36
|
// `WeatherReport` has no API routes defined.
|
|
36
37
|
//
|
|
37
|
-
// Instead of using the generated namespace directly,
|
|
38
|
-
//
|
|
38
|
+
// Instead of using the generated namespace directly, (clo.X) create
|
|
39
|
+
// an implementation with an empty object, which provides a cleaner interface for
|
|
40
|
+
// the rest of the codebase.
|
|
39
41
|
export const WeatherReport = clo.WeatherReport.impl({});
|
|
40
42
|
|
|
43
|
+
|
|
44
|
+
|
|
41
45
|
export default {
|
|
42
46
|
async fetch(request: Request, env: clo.Env): Promise<Response> {
|
|
43
|
-
|
|
47
|
+
const cors = {
|
|
48
|
+
"Access-Control-Allow-Origin": "*",
|
|
49
|
+
"Access-Control-Allow-Methods": "GET, POST, PUT, OPTIONS",
|
|
50
|
+
"Access-Control-Allow-Headers": "Content-Type, Authorization",
|
|
51
|
+
};
|
|
52
|
+
|
|
44
53
|
if (request.method === "OPTIONS") {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
|
|
48
|
-
"Access-Control-Allow-Headers": "Content-Type, Authorization",
|
|
49
|
-
}).toResponse();
|
|
54
|
+
// A basic CORS preflight handler
|
|
55
|
+
return new Response(null, { headers: cors });
|
|
50
56
|
}
|
|
51
57
|
|
|
52
|
-
//
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
const
|
|
58
|
+
// The `cloesce` function returns a `CloesceApp` instance,
|
|
59
|
+
// capable of routing an HTTP request to a model implementation.
|
|
60
|
+
//
|
|
61
|
+
// We register any implementations we want to use with `app.register()`.
|
|
62
|
+
const app = clo.cloesce(env);
|
|
63
|
+
app.register(Weather, WeatherReport);
|
|
57
64
|
|
|
58
|
-
//
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
result.
|
|
65
|
-
"Access-Control-Allow-Headers",
|
|
66
|
-
"Content-Type, Authorization"
|
|
67
|
-
);
|
|
65
|
+
// The `app.run()` method will:
|
|
66
|
+
// 1. Parses the incoming request URL and matches it to a models method
|
|
67
|
+
// 2. Deserializes and validates the request body and parameters against the schema
|
|
68
|
+
// 3. Hydrates the model instance (if applicable)
|
|
69
|
+
// 4. Dispatches to the respective implementation method (e.g. `Weather.uploadPhoto()`)
|
|
70
|
+
// 5. Returns a Response with the result of the method, serialized according to the schema
|
|
71
|
+
const result = await app.run(request);
|
|
68
72
|
|
|
73
|
+
// Set CORS headers on the response
|
|
74
|
+
for (const [key, value] of Object.entries(cors)) {
|
|
75
|
+
result.headers.set(key, value);
|
|
76
|
+
}
|
|
69
77
|
return result;
|
|
70
|
-
}
|
|
71
|
-
};
|
|
78
|
+
},
|
|
79
|
+
};
|
|
@@ -1,108 +1,117 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
r2 {
|
|
12
|
-
bucket
|
|
13
|
-
// bucket2
|
|
1
|
+
// We are able to define any number of R2 bindings in our schema,
|
|
2
|
+
// which are Cloudflare Workers S3-compatible object storage buckets.
|
|
3
|
+
//
|
|
4
|
+
// Each R2 binding compiles to both a Wrangler binding definition as well as
|
|
5
|
+
// helper methods for interacting with the bucket at runtime.
|
|
6
|
+
r2 Bucket {
|
|
7
|
+
// The "photos" template directly states:
|
|
8
|
+
// - "with this parameter, at this key, store a value"
|
|
9
|
+
photos(id: int) {
|
|
10
|
+
"weather/photos/{id}.jpg"
|
|
14
11
|
}
|
|
15
12
|
}
|
|
16
13
|
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
|
|
14
|
+
// D1 is Cloudflares serverless SQL database. Define any number
|
|
15
|
+
// of bindings in the schema under a `d1` block, which compiles to Wrangler bindings
|
|
16
|
+
// and provides a basis for a Model to generate SQL tables for.
|
|
17
|
+
d1 {
|
|
18
|
+
Db
|
|
19
|
+
// db2
|
|
20
|
+
// ...
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// `WeatherReport` is specifically denoted as `for Db`, which means it can source
|
|
24
|
+
// data from the `WeatherReport` table in the `Db` D1 database.
|
|
25
|
+
//
|
|
26
|
+
// Any Model can be annotated with a `crud` tag, which generates basic API
|
|
27
|
+
// functionality for the specified methods.
|
|
28
|
+
//
|
|
29
|
+
// CRUD implementations always exist on the backend, but with the `crud` tag,
|
|
30
|
+
// they are exposed to the client as well.
|
|
20
31
|
[crud list, save, get]
|
|
21
|
-
model WeatherReport {
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
// which all together make up one primary key.
|
|
32
|
+
model WeatherReport for Db {
|
|
33
|
+
// Every SQLite database backed Model must have a
|
|
34
|
+
// `primary` key block, directly translating to a PRIMARY KEY in the generated SQL table.
|
|
25
35
|
primary {
|
|
26
36
|
id: int
|
|
27
37
|
}
|
|
28
|
-
|
|
29
|
-
// Define regular SQL columns under the `column` block.
|
|
30
|
-
column {
|
|
31
|
-
title: string
|
|
32
|
-
description: string
|
|
33
|
-
}
|
|
34
38
|
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
// This works because the field `weatherReportId` in the Weather model is a foreign key that
|
|
40
|
-
// references the `id` field in the WeatherReport model.
|
|
39
|
+
// Cloesce can model relationships between Models with a `nav` block,
|
|
40
|
+
// which states:
|
|
41
|
+
// - "`Weather` has `weatherReportId` which is an FK to myself, JOIN to me at runtime"
|
|
41
42
|
//
|
|
42
|
-
// This
|
|
43
|
-
|
|
44
|
-
nav (Weather::weatherReportId) {
|
|
43
|
+
// This is an example of a 1:M relationship.
|
|
44
|
+
nav Weather::weatherReportId {
|
|
45
45
|
weatherEntries
|
|
46
46
|
}
|
|
47
|
+
|
|
48
|
+
column {
|
|
49
|
+
title: string
|
|
50
|
+
description: string
|
|
51
|
+
}
|
|
47
52
|
}
|
|
48
53
|
|
|
49
|
-
[use db]
|
|
50
54
|
[crud get, list, save]
|
|
51
|
-
model Weather {
|
|
55
|
+
model Weather for Db {
|
|
52
56
|
primary {
|
|
53
57
|
id: int
|
|
54
58
|
}
|
|
59
|
+
|
|
60
|
+
// A Foreign Key block directly translates
|
|
61
|
+
// to a SQLite FOREIGN KEY constraint to the referenced table,
|
|
62
|
+
// and inherits the type of the referenced column (in this case, int).
|
|
63
|
+
foreign WeatherReport::id {
|
|
64
|
+
weatherReportId
|
|
65
|
+
}
|
|
55
66
|
|
|
67
|
+
// A navigation block can also be 1:1. This states:
|
|
68
|
+
// - "`Weather` has `weatherReportId` which is an FK to `WeatherReport`, JOIN to `WeatherReport` at runtime"
|
|
69
|
+
nav WeatherReport::id(weatherReportId) {
|
|
70
|
+
weatherReport
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// SQL isnt the only source of data for a Model. Reference an R2 binding
|
|
74
|
+
// with an `r2` block, which states:
|
|
75
|
+
// - "at runtime, this field is hydrated from the `photos` template of the `Bucket` R2 binding,
|
|
76
|
+
// using my column `id` as the parameter"
|
|
77
|
+
r2 Bucket::photos(id) {
|
|
78
|
+
photo
|
|
79
|
+
}
|
|
80
|
+
|
|
56
81
|
column {
|
|
57
82
|
dateTime: date
|
|
58
83
|
location: string
|
|
59
84
|
temperature: int
|
|
60
85
|
condition: string
|
|
61
86
|
}
|
|
62
|
-
|
|
63
|
-
// A `foreign` block describes a one to one relationship between two models.
|
|
64
|
-
// It translates directly to a foreign key constraint in the database (`weatherReportId` references `id` in WeatherReport).
|
|
65
|
-
foreign (WeatherReport::id) {
|
|
66
|
-
weatherReportId
|
|
67
|
-
|
|
68
|
-
// A `nav` block inside a `foreign` block generates a navigation field,
|
|
69
|
-
// meaning the backend and client will have a WeatherReport object named `weatherReport` nested inside the Weather model.
|
|
70
|
-
nav {
|
|
71
|
-
weatherReport
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
r2 (bucket, "weather/photos/{id}.jpg") {
|
|
76
|
-
photo
|
|
77
|
-
}
|
|
78
87
|
}
|
|
79
88
|
|
|
80
|
-
// An `api` block describes what endpoints must be implemented in the backend
|
|
81
|
-
// and generated in the client. Many blocks can be defined, but they all together make up one API.
|
|
82
|
-
//
|
|
83
|
-
// All endpoints in Cloesce are HTTP endpoints.
|
|
84
89
|
api Weather {
|
|
85
|
-
//
|
|
86
|
-
//
|
|
90
|
+
// This method translates to an HTTP POST endpoint at /Weather/:id/uploadPhoto, accepting a
|
|
91
|
+
// stream as the body of the request.
|
|
92
|
+
//
|
|
93
|
+
// Because this method is marked as `self`, Cloesce will hydrate the respective `Weather` instance
|
|
94
|
+
// at runtime, allowing us to access all fields of the model.
|
|
87
95
|
//
|
|
88
|
-
//
|
|
89
|
-
//
|
|
90
|
-
[inject
|
|
96
|
+
// The `inject` tag allows us to specify any bindings we want access to in the implementation of this method.
|
|
97
|
+
// No other parameters will be injected at runtime, so we must specify all bindings we want to use.
|
|
98
|
+
[inject Bucket]
|
|
91
99
|
post uploadPhoto(self, s: stream)
|
|
92
100
|
|
|
93
|
-
//
|
|
94
|
-
// and the near side of 1:M/M:M relationships.
|
|
101
|
+
// This method translates to an HTTP GET endpoint at /Weather/:id/downloadPhoto.
|
|
95
102
|
//
|
|
96
|
-
//
|
|
97
|
-
//
|
|
98
|
-
//
|
|
103
|
+
// Because this method is marked as `self`, Cloesce will hydrate the respective `Weather` instance.
|
|
104
|
+
// However, we don't need the entire Weather instance to download the photo-- just the `id`.
|
|
105
|
+
// To avoid unnecessary hydration, we can specify a `source` for this method, which states:
|
|
106
|
+
// - "for this method, hydrate `self` using this source instead of the default"
|
|
99
107
|
get downloadPhoto([source R2Only] self) -> stream
|
|
100
108
|
}
|
|
101
109
|
|
|
102
|
-
// A
|
|
103
|
-
//
|
|
110
|
+
// A Data Source block states:
|
|
111
|
+
// - "for this method, only hydrate the specified fields, and ignore the rest"
|
|
104
112
|
//
|
|
105
|
-
//
|
|
113
|
+
// Only R2, KV, and Nav fields can be specified in the include tree. All scalar
|
|
114
|
+
// fields are brought by default.
|
|
106
115
|
source R2Only for Weather {
|
|
107
116
|
include {
|
|
108
117
|
photo
|
|
@@ -1,203 +1,205 @@
|
|
|
1
|
-
<!
|
|
1
|
+
<!doctype html>
|
|
2
2
|
<html lang="en">
|
|
3
|
-
|
|
4
|
-
<
|
|
5
|
-
<meta
|
|
6
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
6
|
<title>Cloesce Project</title>
|
|
8
7
|
<style>
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
8
|
+
* {
|
|
9
|
+
margin: 0;
|
|
10
|
+
padding: 0;
|
|
11
|
+
box-sizing: border-box;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
body {
|
|
15
|
+
font-family: system-ui, sans-serif;
|
|
16
|
+
background: #f5f5f5;
|
|
17
|
+
padding: 20px;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.container {
|
|
21
|
+
max-width: 1200px;
|
|
22
|
+
margin: 0 auto;
|
|
23
|
+
background: white;
|
|
24
|
+
border-radius: 8px;
|
|
25
|
+
overflow: hidden;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
h1 {
|
|
29
|
+
background: linear-gradient(135deg, #1e3a8a, #1e40af);
|
|
30
|
+
color: white;
|
|
31
|
+
padding: 24px;
|
|
32
|
+
font-size: 24px;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.sections-wrapper {
|
|
36
|
+
padding: 20px;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.section {
|
|
40
|
+
border: 1px solid #e5e7eb;
|
|
41
|
+
margin-bottom: 16px;
|
|
42
|
+
border-radius: 6px;
|
|
43
|
+
overflow: hidden;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
h2 {
|
|
47
|
+
color: #1f2937;
|
|
48
|
+
font-size: 14px;
|
|
49
|
+
padding: 12px 16px;
|
|
50
|
+
background: #f9fafb;
|
|
51
|
+
border-left: 4px solid #3b82f6;
|
|
52
|
+
display: flex;
|
|
53
|
+
align-items: center;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.section-content {
|
|
57
|
+
padding: 20px;
|
|
58
|
+
background: white;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.form-group {
|
|
62
|
+
margin-bottom: 16px;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
label {
|
|
66
|
+
display: block;
|
|
67
|
+
margin-bottom: 6px;
|
|
68
|
+
color: #374151;
|
|
69
|
+
font-size: 13px;
|
|
70
|
+
font-weight: 600;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
input,
|
|
74
|
+
textarea {
|
|
75
|
+
width: 100%;
|
|
76
|
+
padding: 8px 10px;
|
|
77
|
+
border: 1px solid #d1d5db;
|
|
78
|
+
border-radius: 4px;
|
|
79
|
+
font-size: 13px;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
button {
|
|
83
|
+
background: #3b82f6;
|
|
84
|
+
color: white;
|
|
85
|
+
border: none;
|
|
86
|
+
padding: 10px 20px;
|
|
87
|
+
border-radius: 4px;
|
|
88
|
+
cursor: pointer;
|
|
89
|
+
font-size: 13px;
|
|
90
|
+
font-weight: 600;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.output {
|
|
94
|
+
margin-top: 16px;
|
|
95
|
+
padding: 12px;
|
|
96
|
+
background: #1f2937;
|
|
97
|
+
border-radius: 4px;
|
|
98
|
+
min-height: 50px;
|
|
99
|
+
font-family: monospace;
|
|
100
|
+
font-size: 12px;
|
|
101
|
+
color: #e5e7eb;
|
|
102
|
+
white-space: pre-wrap;
|
|
103
|
+
word-break: break-word;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.success {
|
|
107
|
+
color: #10b981;
|
|
108
|
+
font-weight: 600;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.error {
|
|
112
|
+
color: #ef4444;
|
|
113
|
+
font-weight: 600;
|
|
114
|
+
}
|
|
116
115
|
</style>
|
|
117
|
-
</head>
|
|
116
|
+
</head>
|
|
118
117
|
|
|
119
|
-
<body>
|
|
118
|
+
<body>
|
|
120
119
|
<div class="container">
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
120
|
+
<h1>⛅ Cloesce Project - Weather Report Manager</h1>
|
|
121
|
+
<div class="sections-wrapper">
|
|
122
|
+
<div class="section">
|
|
123
|
+
<h2>List All Reports</h2>
|
|
124
|
+
<div class="section-content">
|
|
125
|
+
<button onclick="listReports()">Get All Reports</button>
|
|
126
|
+
<div id="list-output" class="output"></div>
|
|
127
|
+
</div>
|
|
128
|
+
</div>
|
|
129
|
+
<div class="section">
|
|
130
|
+
<h2>Create/Update Report</h2>
|
|
131
|
+
<div class="section-content">
|
|
132
|
+
<div class="form-group">
|
|
133
|
+
<label>Title:</label>
|
|
134
|
+
<input type="text" id="save-title" placeholder="e.g., Weekly Weather Summary" />
|
|
135
|
+
</div>
|
|
136
|
+
<div class="form-group">
|
|
137
|
+
<label>Description:</label>
|
|
138
|
+
<textarea
|
|
139
|
+
id="save-desc"
|
|
140
|
+
rows="3"
|
|
141
|
+
placeholder="e.g., Weather report for the week"
|
|
142
|
+
></textarea>
|
|
143
|
+
</div>
|
|
144
|
+
<button onclick="saveReport()">Save Report</button>
|
|
145
|
+
<div id="save-output" class="output"></div>
|
|
146
|
+
</div>
|
|
147
|
+
</div>
|
|
148
|
+
<div class="section">
|
|
149
|
+
<h2>Add Weather Entry to Report</h2>
|
|
150
|
+
<div class="section-content">
|
|
151
|
+
<div class="form-group">
|
|
152
|
+
<label>Report ID:</label>
|
|
153
|
+
<input type="number" id="entry-report-id" placeholder="Enter report ID" required />
|
|
129
154
|
</div>
|
|
130
|
-
<div class="
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
<div class="form-group">
|
|
134
|
-
<label>Title:</label>
|
|
135
|
-
<input type="text" id="save-title" placeholder="e.g., Weekly Weather Summary">
|
|
136
|
-
</div>
|
|
137
|
-
<div class="form-group">
|
|
138
|
-
<label>Description:</label>
|
|
139
|
-
<textarea id="save-desc" rows="3" placeholder="e.g., Weather report for the week"></textarea>
|
|
140
|
-
</div>
|
|
141
|
-
<button onclick="saveReport()">Save Report</button>
|
|
142
|
-
<div id="save-output" class="output"></div>
|
|
143
|
-
</div>
|
|
155
|
+
<div class="form-group">
|
|
156
|
+
<label>Location:</label>
|
|
157
|
+
<input type="text" id="entry-location" placeholder="e.g., Seattle, WA" />
|
|
144
158
|
</div>
|
|
145
|
-
<div class="
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
<div class="form-group">
|
|
149
|
-
<label>Report ID:</label>
|
|
150
|
-
<input type="number" id="entry-report-id" placeholder="Enter report ID" required>
|
|
151
|
-
</div>
|
|
152
|
-
<div class="form-group">
|
|
153
|
-
<label>Location:</label>
|
|
154
|
-
<input type="text" id="entry-location" placeholder="e.g., Seattle, WA">
|
|
155
|
-
</div>
|
|
156
|
-
<div class="form-group">
|
|
157
|
-
<label>Temperature (°F):</label>
|
|
158
|
-
<input type="number" id="entry-temp" placeholder="e.g., 72">
|
|
159
|
-
</div>
|
|
160
|
-
<div class="form-group">
|
|
161
|
-
<label>Condition:</label>
|
|
162
|
-
<input type="text" id="entry-condition" placeholder="e.g., Sunny, Cloudy, Rainy">
|
|
163
|
-
</div>
|
|
164
|
-
<div class="form-group">
|
|
165
|
-
<label>Date/Time:</label>
|
|
166
|
-
<input type="datetime-local" id="entry-datetime">
|
|
167
|
-
</div>
|
|
168
|
-
<button onclick="addWeatherEntry()">Add Weather Entry</button>
|
|
169
|
-
<div id="entry-output" class="output"></div>
|
|
170
|
-
</div>
|
|
159
|
+
<div class="form-group">
|
|
160
|
+
<label>Temperature (°F):</label>
|
|
161
|
+
<input type="number" id="entry-temp" placeholder="e.g., 72" />
|
|
171
162
|
</div>
|
|
172
|
-
<div class="
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
<div class="form-group">
|
|
176
|
-
<label>Weather Entry ID:</label>
|
|
177
|
-
<input type="number" id="upload-id" placeholder="Enter weather entry ID">
|
|
178
|
-
</div>
|
|
179
|
-
<div class="form-group">
|
|
180
|
-
<label>Photo File:</label>
|
|
181
|
-
<input type="file" id="upload-file" accept="image/*">
|
|
182
|
-
</div>
|
|
183
|
-
<button onclick="uploadPhoto()">Upload Photo</button>
|
|
184
|
-
<div id="upload-output" class="output"></div>
|
|
185
|
-
</div>
|
|
163
|
+
<div class="form-group">
|
|
164
|
+
<label>Condition:</label>
|
|
165
|
+
<input type="text" id="entry-condition" placeholder="e.g., Sunny, Cloudy, Rainy" />
|
|
186
166
|
</div>
|
|
187
|
-
<div class="
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
<div class="form-group">
|
|
191
|
-
<label>Weather Entry ID:</label>
|
|
192
|
-
<input type="number" id="download-id" placeholder="Enter weather entry ID">
|
|
193
|
-
</div>
|
|
194
|
-
<button onclick="downloadPhoto()">Download Photo</button>
|
|
195
|
-
<div id="download-output" class="output"></div>
|
|
196
|
-
</div>
|
|
167
|
+
<div class="form-group">
|
|
168
|
+
<label>Date/Time:</label>
|
|
169
|
+
<input type="datetime-local" id="entry-datetime" />
|
|
197
170
|
</div>
|
|
171
|
+
<button onclick="addWeatherEntry()">Add Weather Entry</button>
|
|
172
|
+
<div id="entry-output" class="output"></div>
|
|
173
|
+
</div>
|
|
198
174
|
</div>
|
|
175
|
+
<div class="section">
|
|
176
|
+
<h2>Upload Photo to Weather Entry</h2>
|
|
177
|
+
<div class="section-content">
|
|
178
|
+
<div class="form-group">
|
|
179
|
+
<label>Weather Entry ID:</label>
|
|
180
|
+
<input type="number" id="upload-id" placeholder="Enter weather entry ID" />
|
|
181
|
+
</div>
|
|
182
|
+
<div class="form-group">
|
|
183
|
+
<label>Photo File:</label>
|
|
184
|
+
<input type="file" id="upload-file" accept="image/*" />
|
|
185
|
+
</div>
|
|
186
|
+
<button onclick="uploadPhoto()">Upload Photo</button>
|
|
187
|
+
<div id="upload-output" class="output"></div>
|
|
188
|
+
</div>
|
|
189
|
+
</div>
|
|
190
|
+
<div class="section">
|
|
191
|
+
<h2>Download Photo from Weather Entry</h2>
|
|
192
|
+
<div class="section-content">
|
|
193
|
+
<div class="form-group">
|
|
194
|
+
<label>Weather Entry ID:</label>
|
|
195
|
+
<input type="number" id="download-id" placeholder="Enter weather entry ID" />
|
|
196
|
+
</div>
|
|
197
|
+
<button onclick="downloadPhoto()">Download Photo</button>
|
|
198
|
+
<div id="download-output" class="output"></div>
|
|
199
|
+
</div>
|
|
200
|
+
</div>
|
|
201
|
+
</div>
|
|
199
202
|
</div>
|
|
200
203
|
<script type="module" src="index.ts"></script>
|
|
201
|
-
</body>
|
|
202
|
-
|
|
203
|
-
</html>
|
|
204
|
+
</body>
|
|
205
|
+
</html>
|
|
@@ -1,122 +1,123 @@
|
|
|
1
|
-
|
|
2
|
-
// Be careful to not use any imports under `@cloesce/backend.js` in the client.
|
|
3
|
-
import { Weather, WeatherReport } from '@cloesce/client.js';
|
|
1
|
+
import { Weather, WeatherReport } from "@cloesce/client.js";
|
|
4
2
|
|
|
5
3
|
declare global {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
4
|
+
interface Window {
|
|
5
|
+
listReports: () => Promise<void>;
|
|
6
|
+
saveReport: () => Promise<void>;
|
|
7
|
+
addWeatherEntry: () => Promise<void>;
|
|
8
|
+
uploadPhoto: () => Promise<void>;
|
|
9
|
+
downloadPhoto: () => Promise<void>;
|
|
10
|
+
}
|
|
13
11
|
}
|
|
14
12
|
|
|
15
13
|
const getValue = (id: string) => (document.getElementById(id) as HTMLInputElement).value;
|
|
16
14
|
|
|
17
15
|
const showResult = (outputId: string, result: any) => {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
16
|
+
const output = document.getElementById(outputId)!;
|
|
17
|
+
const status = result.ok ? "success" : "error";
|
|
18
|
+
const icon = result.ok ? "✓" : "✗";
|
|
19
|
+
const data = result.ok && result.data ? `\n${JSON.stringify(result.data, null, 2)}` : "";
|
|
20
|
+
output.innerHTML = `<div class="${status}">${icon} ${result.ok ? "Success" : "Error"} (${result.status})</div>${result.message || data}`;
|
|
23
21
|
};
|
|
24
22
|
|
|
25
23
|
window.listReports = async () => {
|
|
26
|
-
|
|
27
|
-
|
|
24
|
+
const result = await WeatherReport.$list(0, 100);
|
|
25
|
+
showResult("list-output", result);
|
|
28
26
|
};
|
|
29
27
|
|
|
30
28
|
window.saveReport = async () => {
|
|
31
|
-
|
|
32
|
-
|
|
29
|
+
const title = getValue("save-title");
|
|
30
|
+
const description = getValue("save-desc");
|
|
33
31
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
32
|
+
if (!title) {
|
|
33
|
+
document.getElementById("save-output")!.innerHTML =
|
|
34
|
+
'<div class="error">Please enter a title</div>';
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
38
37
|
|
|
39
|
-
|
|
40
|
-
|
|
38
|
+
const result = await WeatherReport.$save({ title, description });
|
|
39
|
+
showResult("save-output", result);
|
|
41
40
|
};
|
|
42
41
|
|
|
43
42
|
window.addWeatherEntry = async () => {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
43
|
+
const reportId = parseInt(getValue("entry-report-id"));
|
|
44
|
+
|
|
45
|
+
if (!reportId) {
|
|
46
|
+
document.getElementById("entry-output")!.innerHTML =
|
|
47
|
+
'<div class="error">Please enter a report ID</div>';
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const getResult = await WeatherReport.$get(reportId);
|
|
52
|
+
if (!getResult.ok) {
|
|
53
|
+
showResult("entry-output", getResult);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const report = getResult.data!;
|
|
58
|
+
const newEntry = {
|
|
59
|
+
weatherReportId: reportId,
|
|
60
|
+
location: getValue("entry-location") || "",
|
|
61
|
+
temperature: parseFloat(getValue("entry-temp")) || 0,
|
|
62
|
+
condition: getValue("entry-condition") || "",
|
|
63
|
+
dateTime: getValue("entry-datetime") ? new Date(getValue("entry-datetime")) : new Date(),
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const result = await WeatherReport.$save({
|
|
67
|
+
id: report.id,
|
|
68
|
+
title: report.title,
|
|
69
|
+
description: report.description,
|
|
70
|
+
weatherEntries: [...(report.weatherEntries || []), newEntry],
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
showResult("entry-output", result);
|
|
74
74
|
};
|
|
75
75
|
|
|
76
76
|
window.uploadPhoto = async () => {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
77
|
+
const id = parseInt(getValue("upload-id"));
|
|
78
|
+
const fileInput = document.getElementById("upload-file") as HTMLInputElement;
|
|
79
|
+
const output = document.getElementById("upload-output")!;
|
|
80
|
+
|
|
81
|
+
if (!id) {
|
|
82
|
+
output.innerHTML = '<div class="error">Please enter a weather entry ID</div>';
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (!fileInput.files?.[0]) {
|
|
87
|
+
output.innerHTML = '<div class="error">Please select a file</div>';
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const buffer = await fileInput.files[0].arrayBuffer();
|
|
92
|
+
const weather = new Weather();
|
|
93
|
+
weather.id = id;
|
|
94
|
+
|
|
95
|
+
const result = await weather.uploadPhoto(new Uint8Array(buffer));
|
|
96
|
+
showResult("upload-output", result);
|
|
97
97
|
};
|
|
98
98
|
|
|
99
99
|
window.downloadPhoto = async () => {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
100
|
+
const id = parseInt(getValue("download-id"));
|
|
101
|
+
|
|
102
|
+
if (!id) {
|
|
103
|
+
document.getElementById("download-output")!.innerHTML =
|
|
104
|
+
'<div class="error">Please enter a weather entry ID</div>';
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const weather = new Weather();
|
|
109
|
+
weather.id = id;
|
|
110
|
+
const result = await weather.downloadPhoto();
|
|
111
|
+
|
|
112
|
+
if (result.ok && result.data) {
|
|
113
|
+
const blob = await result.data.blob();
|
|
114
|
+
const url = URL.createObjectURL(blob);
|
|
115
|
+
const a = document.createElement("a");
|
|
116
|
+
a.href = url;
|
|
117
|
+
a.download = `weather-photo-${id}.jpg`;
|
|
118
|
+
a.click();
|
|
119
|
+
URL.revokeObjectURL(url);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
showResult("download-output", result);
|
|
123
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"module": "ESNext",
|
|
4
|
+
"target": "ES2020",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
|
7
|
+
"types": [],
|
|
8
|
+
"skipLibCheck": true,
|
|
9
|
+
"allowSyntheticDefaultImports": true,
|
|
10
|
+
"resolveJsonModule": true,
|
|
11
|
+
"paths": {
|
|
12
|
+
"@cloesce/*": ["../../.cloesce/*"],
|
|
13
|
+
},
|
|
14
|
+
"noEmit": true,
|
|
15
|
+
},
|
|
16
|
+
"include": ["*.ts"],
|
|
17
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { env } from "cloudflare:workers";
|
|
2
|
+
import { applyD1Migrations, type D1Migration } from "cloudflare:test";
|
|
3
|
+
import { beforeAll, inject } from "vitest";
|
|
4
|
+
import * as clo from "../.cloesce/backend.js";
|
|
5
|
+
|
|
6
|
+
declare module "vitest" {
|
|
7
|
+
interface ProvidedContext {
|
|
8
|
+
migrations: D1Migration[];
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
declare global {
|
|
13
|
+
namespace Cloudflare {
|
|
14
|
+
interface Env extends clo.CfEnv {}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
beforeAll(async () => {
|
|
19
|
+
const migrations = inject("migrations");
|
|
20
|
+
await applyD1Migrations(env.db, migrations);
|
|
21
|
+
await clo.cloesce(env).forceLoad();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
export function upgraded() {
|
|
25
|
+
return clo.upgradeEnv(env);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export { env };
|
|
@@ -1,80 +1,38 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
);
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
CREATE TABLE IF NOT EXISTS "_cloesce_tmp" (
|
|
40
|
-
"path" text PRIMARY KEY,
|
|
41
|
-
"primary_key" text NOT NULL
|
|
42
|
-
);
|
|
43
|
-
`).run();
|
|
44
|
-
|
|
45
|
-
return env;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
beforeAll(() => cloesce());
|
|
49
|
-
|
|
50
|
-
// Here we will test our Cloesce models against a Miniflare environment.
|
|
51
|
-
// This does not use any client stubs; it interacts directly with the Miniflare instance.
|
|
52
|
-
describe("Miniflare Integration Tests", () => {
|
|
53
|
-
test("Download a thumbnail", async () => {
|
|
54
|
-
// Arrange
|
|
55
|
-
const env = await createTestEnv();
|
|
56
|
-
const testData = "test-data";
|
|
57
|
-
|
|
58
|
-
const report = (await WeatherReport.Orm.save(env, {
|
|
59
|
-
title: "Test Report",
|
|
60
|
-
description: "This is a test weather report.",
|
|
61
|
-
weatherEntries: [{
|
|
62
|
-
dateTime: new Date(),
|
|
63
|
-
location: "Test Location",
|
|
64
|
-
temperature: 25,
|
|
65
|
-
condition: "Sunny"
|
|
66
|
-
}]
|
|
67
|
-
})).value!;
|
|
68
|
-
|
|
69
|
-
await Weather.uploadPhoto(report.weatherEntries[0], env, testData as any);
|
|
70
|
-
|
|
71
|
-
// Act
|
|
72
|
-
const weatherEntries = (await Weather.Default.list(env, 0, 100)).value!;
|
|
73
|
-
const photo = Weather.downloadPhoto(weatherEntries[0]);
|
|
74
|
-
|
|
75
|
-
// Assert
|
|
76
|
-
expect(photo.ok).toBe(true);
|
|
77
|
-
const downloadedText = await new Response(photo.data as any).text();
|
|
78
|
-
expect(downloadedText).toBe(testData);
|
|
79
|
-
});
|
|
80
|
-
});
|
|
1
|
+
import { describe, test, expect } from "vitest";
|
|
2
|
+
import { Weather, WeatherReport } from "@api/main.js";
|
|
3
|
+
import { upgraded } from "./setup.js";
|
|
4
|
+
|
|
5
|
+
// This does not use any client stubs; it interacts directly with the bound D1/R2 instances.
|
|
6
|
+
describe("Cloudflare Workers Integration Tests", () => {
|
|
7
|
+
test("Download a thumbnail", async () => {
|
|
8
|
+
// Arrange
|
|
9
|
+
const env = upgraded();
|
|
10
|
+
const testData = "test-data";
|
|
11
|
+
|
|
12
|
+
const report = (
|
|
13
|
+
await WeatherReport.Orm.save(env, {
|
|
14
|
+
title: "Test Report",
|
|
15
|
+
description: "This is a test weather report.",
|
|
16
|
+
weatherEntries: [
|
|
17
|
+
{
|
|
18
|
+
dateTime: new Date(),
|
|
19
|
+
location: "Test Location",
|
|
20
|
+
temperature: 25,
|
|
21
|
+
condition: "Sunny",
|
|
22
|
+
},
|
|
23
|
+
],
|
|
24
|
+
})
|
|
25
|
+
).value!;
|
|
26
|
+
|
|
27
|
+
await Weather.uploadPhoto(report.weatherEntries[0], env, testData as any);
|
|
28
|
+
|
|
29
|
+
// Act
|
|
30
|
+
const weatherEntries = (await Weather.Default.list(env, 0, 100)).data!;
|
|
31
|
+
const photo = Weather.downloadPhoto(weatherEntries[0]);
|
|
32
|
+
|
|
33
|
+
// Assert
|
|
34
|
+
expect(photo.ok).toBe(true);
|
|
35
|
+
const downloadedText = await new Response(photo.data as any).text();
|
|
36
|
+
expect(downloadedText).toBe(testData);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
@@ -1,24 +1,18 @@
|
|
|
1
1
|
{
|
|
2
|
-
// Visit https://aka.ms/tsconfig to read more about this file
|
|
3
2
|
"compilerOptions": {
|
|
4
|
-
"module": "
|
|
3
|
+
"module": "NodeNext",
|
|
5
4
|
"target": "ES2020",
|
|
6
|
-
"moduleResolution": "
|
|
7
|
-
"types": [],
|
|
5
|
+
"moduleResolution": "nodenext",
|
|
6
|
+
"types": ["@cloudflare/workers-types", "@cloudflare/vitest-pool-workers/types"],
|
|
8
7
|
"skipLibCheck": true,
|
|
9
8
|
|
|
10
|
-
// Cloesce required options
|
|
11
|
-
"resolveJsonModule": true,
|
|
12
|
-
"strict": true,
|
|
13
|
-
"strictPropertyInitialization": false,
|
|
14
|
-
"experimentalDecorators": true,
|
|
15
|
-
"emitDecoratorMetadata": true,
|
|
16
9
|
"allowSyntheticDefaultImports": true,
|
|
10
|
+
"resolveJsonModule": true,
|
|
17
11
|
"paths": {
|
|
18
12
|
"@api/*": ["./src/api/*"],
|
|
19
13
|
"@cloesce/*": ["./.cloesce/*"],
|
|
20
14
|
},
|
|
21
15
|
"outDir": "dist",
|
|
22
16
|
},
|
|
23
|
-
"include": [".cloesce/*.ts", "src/**/*.ts", "
|
|
17
|
+
"include": [".cloesce/*.ts", "src/api/**/*.ts", "tests/**/*.ts", "migrations/**/*.ts"],
|
|
24
18
|
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { defineConfig } from "vite";
|
|
2
|
-
import
|
|
2
|
+
import { fileURLToPath } from "url";
|
|
3
3
|
|
|
4
4
|
export default defineConfig({
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
root: "./src/web",
|
|
6
|
+
resolve: {
|
|
7
|
+
alias: { "@cloesce": fileURLToPath(new URL("./.cloesce", import.meta.url)) },
|
|
8
|
+
},
|
|
7
9
|
});
|
|
@@ -1,6 +1,25 @@
|
|
|
1
1
|
import { defineConfig } from "vitest/config";
|
|
2
|
-
import
|
|
2
|
+
import { cloudflareTest, readD1Migrations } from "@cloudflare/vitest-pool-workers";
|
|
3
|
+
import { fileURLToPath } from "url";
|
|
4
|
+
|
|
5
|
+
const resolve = (p: string) => fileURLToPath(new URL(p, import.meta.url));
|
|
3
6
|
|
|
4
7
|
export default defineConfig({
|
|
5
|
-
|
|
6
|
-
|
|
8
|
+
resolve: {
|
|
9
|
+
alias: {
|
|
10
|
+
"@cloesce": resolve("./.cloesce"),
|
|
11
|
+
"@api": resolve("./src/api"),
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
plugins: [
|
|
15
|
+
cloudflareTest({
|
|
16
|
+
main: "./src/api/main.ts",
|
|
17
|
+
wrangler: { configPath: "./wrangler.jsonc" },
|
|
18
|
+
}),
|
|
19
|
+
],
|
|
20
|
+
test: {
|
|
21
|
+
provide: {
|
|
22
|
+
migrations: await readD1Migrations("./migrations/db"),
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
});
|