create-cloesce 0.3.4 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "create-cloesce",
3
3
  "description": "Create Cloesce App",
4
- "version": "0.3.4",
4
+ "version": "0.5.0",
5
5
  "author": "Ben Schreiber <bpschreiber2003@gmail.com>",
6
6
  "repository": {
7
7
  "url": "https://github.com/bens-schreiber/create-cloesce-app"
@@ -1,28 +1,27 @@
1
1
  {
2
- "name": "cloesce-proj",
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 db",
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.3.0",
17
+ "cloesce": ">=0.5.0",
14
18
  "wrangler": "^4.61.1"
15
19
  },
16
20
  "devDependencies": {
17
- "@cloudflare/workers-types": "^4.20260131.0",
18
- "miniflare": "^4.20260128.0",
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": "^7.3.1"
23
- },
24
- "keywords": [],
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
- // Here, we import the generated backend code, which includes all the types
2
- // defined in the `schema.clo` file
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, e, s: CfReadableStream) {
19
- // All models have a `Key` namespace which provides utilities for generating
20
- // KV and R2 keys for that model.
21
- const key = this.Key.photo(self.id);
22
- await e.bucket.put(key, s);
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, we can still create an implementation with `impl`
38
- // to avoid passing the generated code around the rest of our application.
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
- // preflight
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
- return HttpResult.ok(200, undefined, {
46
- "Access-Control-Allow-Origin": "*",
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
- // Run Cloesce app
53
- const app = (await clo.cloesce())
54
- .register(Weather);
55
-
56
- const result = await app.run(request, env);
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
- // attach CORS headers
59
- result.headers.set("Access-Control-Allow-Origin", "*");
60
- result.headers.set(
61
- "Access-Control-Allow-Methods",
62
- "GET, POST, PUT, DELETE, OPTIONS"
63
- );
64
- result.headers.set(
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,105 +1,117 @@
1
- // `env` describes the resources in a wrangler configuration that the API can access.
2
- // It does not replace the wrangler config, but does result in a generated version.
3
- env {
4
- // The `d1` block describes all D1 database bindings
5
- d1 {
6
- db
7
- // db2
8
- }
9
-
10
- // The `r2` block describes all R2 bucket bindings
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
- // If a model is to be stored in a D1 database as a table, it must be decorated with `[use <binding>]`
18
- // The `list`, `get`, and `save` operations are used to generate CRUD operations for the model.
19
- // Optionally, these tags can be combined, e.g. `[use db, list, get, save]`
20
- [use db]
21
- [use list, save, get]
22
- model WeatherReport {
23
- // The `primary` block describes the primary key of the table.
24
- // It can be composed of several fields. Any number of primary blocks can be defined,
25
- // which all together make up one primary key.
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.
31
+ [crud list, save, get]
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.
26
35
  primary {
27
36
  id: int
28
37
  }
29
38
 
30
- // A `nav` block describes a relationship between two models.
31
- // In this case, it is a one-to-many relationship between WeatherReport and Weather,
32
- // where WeatherReport has many Weathers.
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"
33
42
  //
34
- // This works because the field `weatherReportId` in the Weather model is a foreign key that
35
- // references the `id` field in the WeatherReport model.
36
- //
37
- // This will resolve to a navigation field named `weatherEntries` in the WeatherReport model,
38
- // which is an array of Weather objects.
39
- nav (Weather::weatherReportId) {
43
+ // This is an example of a 1:M relationship.
44
+ nav Weather::weatherReportId {
40
45
  weatherEntries
41
46
  }
42
47
 
43
- // All fields that are not apart of a block are just regular fields in the table.
44
- title: string
45
- description: string
48
+ column {
49
+ title: string
50
+ description: string
51
+ }
46
52
  }
47
53
 
48
- [use db]
49
- [use get, list, save]
50
- model Weather {
54
+ [crud get, list, save]
55
+ model Weather for Db {
51
56
  primary {
52
57
  id: int
53
58
  }
54
59
 
55
- // A `foreign` block describes a one to one relationship between two models.
56
- // It translates directly to a foreign key constraint in the database (`weatherReportId` references `id` in WeatherReport).
57
- foreign (WeatherReport::id) {
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 {
58
64
  weatherReportId
59
-
60
-
61
- // A `nav` block inside a `foreign` block generates a navigation field,
62
- // meaning the backend and client will have a WeatherReport object named `weatherReport` nested inside the Weather model.
63
- nav {
64
- weatherReport
65
- }
65
+ }
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
66
71
  }
67
72
 
68
- r2 (bucket, "weather/photos/{id}.jpg") {
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) {
69
78
  photo
70
79
  }
71
80
 
72
- dateTime: date
73
- location: string
74
- temperature: int
75
- condition: string
81
+ column {
82
+ dateTime: date
83
+ location: string
84
+ temperature: int
85
+ condition: string
86
+ }
76
87
  }
77
88
 
78
- // An `api` block describes what endpoints must be implemented in the backend
79
- // and generated in the client. Many blocks can be defined, but they all together make up one API.
80
- //
81
- // All endpoints in Cloesce are HTTP endpoints.
82
89
  api Weather {
83
- // If `self` is passed as an argument, it means the endpoint requires an
84
- // instantiated Weather object to be hydrated on the backend before the endpoint can be executed.
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.
85
95
  //
86
- // In this case, `uploadPhoto` accepts the Wrangler environment (dependency injected),
87
- // and a stream (the photo to be uploaded).
88
- post uploadPhoto(self, e: env, s: stream) -> void
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]
99
+ post uploadPhoto(self, s: stream)
89
100
 
90
- // By default, Cloesce will hydrate a `self` instance with all 1:1, KV, R2,
91
- // and the near side of 1:M/M:M relationships.
101
+ // This method translates to an HTTP GET endpoint at /Weather/:id/downloadPhoto.
92
102
  //
93
- // To avoid overfetching, a `source` tag can be used on `self`, which uses the
94
- // specified data source instead. In this case, `downloadPhoto` will only hydrate the `photo` field.
95
- // (NOTE: The Weather table is always queried to hydrate columns, regardless of the source.)
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"
96
107
  get downloadPhoto([source R2Only] self) -> stream
97
108
  }
98
109
 
99
- // A `source` block describes a data souce that can be used throughout
100
- // a Cloesce program to hydrate and list models.
110
+ // A Data Source block states:
111
+ // - "for this method, only hydrate the specified fields, and ignore the rest"
101
112
  //
102
- // In this case, the include tree has only the `photo` field of `Weather`.
113
+ // Only R2, KV, and Nav fields can be specified in the include tree. All scalar
114
+ // fields are brought by default.
103
115
  source R2Only for Weather {
104
116
  include {
105
117
  photo
@@ -1,203 +1,205 @@
1
- <!DOCTYPE html>
1
+ <!doctype html>
2
2
  <html lang="en">
3
-
4
- <head>
5
- <meta charset="UTF-8">
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
- margin: 0;
11
- padding: 0;
12
- box-sizing: border-box;
13
- }
14
-
15
- body {
16
- font-family: system-ui, sans-serif;
17
- background: #f5f5f5;
18
- padding: 20px;
19
- }
20
-
21
- .container {
22
- max-width: 1200px;
23
- margin: 0 auto;
24
- background: white;
25
- border-radius: 8px;
26
- overflow: hidden;
27
- }
28
-
29
- h1 {
30
- background: linear-gradient(135deg, #1e3a8a, #1e40af);
31
- color: white;
32
- padding: 24px;
33
- font-size: 24px;
34
- }
35
-
36
- .sections-wrapper {
37
- padding: 20px;
38
- }
39
-
40
- .section {
41
- border: 1px solid #e5e7eb;
42
- margin-bottom: 16px;
43
- border-radius: 6px;
44
- overflow: hidden;
45
- }
46
-
47
- h2 {
48
- color: #1f2937;
49
- font-size: 14px;
50
- padding: 12px 16px;
51
- background: #f9fafb;
52
- border-left: 4px solid #3b82f6;
53
- display: flex;
54
- align-items: center;
55
- }
56
-
57
- .section-content {
58
- padding: 20px;
59
- background: white;
60
- }
61
-
62
- .form-group {
63
- margin-bottom: 16px;
64
- }
65
-
66
- label {
67
- display: block;
68
- margin-bottom: 6px;
69
- color: #374151;
70
- font-size: 13px;
71
- font-weight: 600;
72
- }
73
-
74
- input,
75
- textarea {
76
- width: 100%;
77
- padding: 8px 10px;
78
- border: 1px solid #d1d5db;
79
- border-radius: 4px;
80
- font-size: 13px;
81
- }
82
-
83
- button {
84
- background: #3b82f6;
85
- color: white;
86
- border: none;
87
- padding: 10px 20px;
88
- border-radius: 4px;
89
- cursor: pointer;
90
- font-size: 13px;
91
- font-weight: 600;
92
- }
93
-
94
- .output {
95
- margin-top: 16px;
96
- padding: 12px;
97
- background: #1f2937;
98
- border-radius: 4px;
99
- min-height: 50px;
100
- font-family: monospace;
101
- font-size: 12px;
102
- color: #e5e7eb;
103
- white-space: pre-wrap;
104
- word-break: break-word;
105
- }
106
-
107
- .success {
108
- color: #10b981;
109
- font-weight: 600;
110
- }
111
-
112
- .error {
113
- color: #ef4444;
114
- font-weight: 600;
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
- <h1>⛅ Cloesce Project - Weather Report Manager</h1>
122
- <div class="sections-wrapper">
123
- <div class="section">
124
- <h2>List All Reports</h2>
125
- <div class="section-content">
126
- <button onclick="listReports()">Get All Reports</button>
127
- <div id="list-output" class="output"></div>
128
- </div>
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="section">
131
- <h2>Create/Update Report</h2>
132
- <div class="section-content">
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="section">
146
- <h2>Add Weather Entry to Report</h2>
147
- <div class="section-content">
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="section">
173
- <h2>Upload Photo to Weather Entry</h2>
174
- <div class="section-content">
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="section">
188
- <h2>Download Photo from Weather Entry</h2>
189
- <div class="section-content">
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,127 +1,123 @@
1
- // All client code is generated under `@cloesce/client.js`.
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
- interface Window {
7
- listReports: () => Promise<void>;
8
- saveReport: () => Promise<void>;
9
- addWeatherEntry: () => Promise<void>;
10
- uploadPhoto: () => Promise<void>;
11
- downloadPhoto: () => Promise<void>;
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
- const output = document.getElementById(outputId)!;
19
- const status = result.ok ? 'success' : 'error';
20
- const icon = result.ok ? '' : '';
21
- const data = result.ok && result.data ? `\n${JSON.stringify(result.data, null, 2)}` : '';
22
- output.innerHTML = `<div class="${status}">${icon} ${result.ok ? 'Success' : 'Error'} (${result.status})</div>${result.message || data}`;
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
- const result = await WeatherReport.$list({
27
- lastSeen_id: 0,
28
- limit: 100
29
- })
30
- showResult('list-output', result);
24
+ const result = await WeatherReport.$list(0, 100);
25
+ showResult("list-output", result);
31
26
  };
32
27
 
33
28
  window.saveReport = async () => {
34
- const title = getValue('save-title');
35
- const description = getValue('save-desc');
29
+ const title = getValue("save-title");
30
+ const description = getValue("save-desc");
36
31
 
37
- if (!title) {
38
- document.getElementById('save-output')!.innerHTML = '<div class="error">Please enter a title</div>';
39
- return;
40
- }
32
+ if (!title) {
33
+ document.getElementById("save-output")!.innerHTML =
34
+ '<div class="error">Please enter a title</div>';
35
+ return;
36
+ }
41
37
 
42
- const result = await WeatherReport.$save({ title, description });
43
- showResult('save-output', result);
38
+ const result = await WeatherReport.$save({ title, description });
39
+ showResult("save-output", result);
44
40
  };
45
41
 
46
42
  window.addWeatherEntry = async () => {
47
- const reportId = parseInt(getValue('entry-report-id'));
48
-
49
- if (!reportId) {
50
- document.getElementById('entry-output')!.innerHTML = '<div class="error">Please enter a report ID</div>';
51
- return;
52
- }
53
-
54
- const getResult = await WeatherReport.$get({
55
- id: reportId
56
- });
57
- if (!getResult.ok) {
58
- showResult('entry-output', getResult);
59
- return;
60
- }
61
-
62
- const report = getResult.data!;
63
- const newEntry = {
64
- weatherReportId: reportId,
65
- location: getValue('entry-location') || '',
66
- temperature: parseFloat(getValue('entry-temp')) || 0,
67
- condition: getValue('entry-condition') || '',
68
- dateTime: getValue('entry-datetime') ? new Date(getValue('entry-datetime')) : new Date()
69
- };
70
-
71
- const result = await WeatherReport.$save({
72
- id: report.id,
73
- title: report.title,
74
- description: report.description,
75
- weatherEntries: [...(report.weatherEntries || []), newEntry]
76
- });
77
-
78
- showResult('entry-output', result);
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);
79
74
  };
80
75
 
81
76
  window.uploadPhoto = async () => {
82
- const id = parseInt(getValue('upload-id'));
83
- const fileInput = document.getElementById('upload-file') as HTMLInputElement;
84
- const output = document.getElementById('upload-output')!;
85
-
86
- if (!id) {
87
- output.innerHTML = '<div class="error">Please enter a weather entry ID</div>';
88
- return;
89
- }
90
-
91
- if (!fileInput.files?.[0]) {
92
- output.innerHTML = '<div class="error">Please select a file</div>';
93
- return;
94
- }
95
-
96
- const buffer = await fileInput.files[0].arrayBuffer();
97
- const weather = new Weather();
98
- weather.id = id;
99
-
100
- const result = await weather.uploadPhoto(new Uint8Array(buffer));
101
- showResult('upload-output', result);
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);
102
97
  };
103
98
 
104
99
  window.downloadPhoto = async () => {
105
- const id = parseInt(getValue('download-id'));
106
-
107
- if (!id) {
108
- document.getElementById('download-output')!.innerHTML = '<div class="error">Please enter a weather entry ID</div>';
109
- return;
110
- }
111
-
112
- const weather = new Weather();
113
- weather.id = id;
114
- const result = await weather.downloadPhoto();
115
-
116
- if (result.ok && result.data) {
117
- const blob = await result.data.blob();
118
- const url = URL.createObjectURL(blob);
119
- const a = document.createElement('a');
120
- a.href = url;
121
- a.download = `weather-photo-${id}.jpg`;
122
- a.click();
123
- URL.revokeObjectURL(url);
124
- }
125
-
126
- showResult('download-output', result);
127
- };
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 { Miniflare } from "miniflare";
2
- import { describe, test, expect, beforeAll } from "vitest";
3
- import { Weather, WeatherReport } from "@api/main.js"
4
- import { cloesce } from "@cloesce/backend.js";
5
-
6
- async function createTestEnv() {
7
- const mf = new Miniflare({
8
- modules: true,
9
- script: `export default { async fetch() { return new Response("Hello!"); } }`,
10
- d1Databases: ["db"],
11
- r2Buckets: ["bucket"],
12
- });
13
-
14
- const db = await mf.getD1Database("db");
15
- const bucket = await mf.getR2Bucket("bucket");
16
- const env = { db, bucket } as any;
17
-
18
- // Run any necessary migrations
19
- // TODO: Does Cloudflare have a way to do this automatically in tests?
20
- await db.prepare(`
21
- --- New Models
22
- CREATE TABLE IF NOT EXISTS "WeatherReport" (
23
- "id" integer PRIMARY KEY,
24
- "title" text NOT NULL,
25
- "description" text NOT NULL
26
- );
27
-
28
- CREATE TABLE IF NOT EXISTS "Weather" (
29
- "id" integer PRIMARY KEY,
30
- "weatherReportId" integer NOT NULL,
31
- "dateTime" text NOT NULL,
32
- "location" text NOT NULL,
33
- "temperature" integer NOT NULL,
34
- "condition" text NOT NULL,
35
- FOREIGN KEY ("weatherReportId") REFERENCES "WeatherReport" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
36
- );
37
-
38
- --- Cloesce Temporary Table
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
- }))!;
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))!;
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": "node20",
3
+ "module": "NodeNext",
5
4
  "target": "ES2020",
6
- "moduleResolution": "node16",
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", "test/**/*.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 tsconfigPaths from "vite-tsconfig-paths";
2
+ import { fileURLToPath } from "url";
3
3
 
4
4
  export default defineConfig({
5
- plugins: [tsconfigPaths()],
6
- root: "./src/web"
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 tsconfigPaths from "vite-tsconfig-paths";
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
- plugins: [tsconfigPaths()]
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
+ });