create-cloesce 0.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/README.md ADDED
@@ -0,0 +1,3 @@
1
+ # Create Cloesce App
2
+
3
+ Creates a Cloesce application from a template.
package/dist/cli.js ADDED
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env node
2
+ "use strict";var m=Object.create;var a=Object.defineProperty;var l=Object.getOwnPropertyDescriptor;var c=Object.getOwnPropertyNames;var u=Object.getPrototypeOf,f=Object.prototype.hasOwnProperty;var d=(e,r,t,s)=>{if(r&&typeof r=="object"||typeof r=="function")for(let o of c(r))!f.call(e,o)&&o!==t&&a(e,o,{get:()=>r[o],enumerable:!(s=l(r,o))||s.enumerable});return e};var v=(e,r,t)=>(t=e!=null?m(u(e)):{},d(r||!e||!e.__esModule?a(t,"default",{value:e,enumerable:!0}):t,e));var i=require("create-create-app"),n=require("path"),p=v(require("fs")),w=(0,n.resolve)(__dirname,"..","templates"),y=`
3
+ To build your Cloesce project, run:
4
+ - npm run build
5
+ - npm run migrate:cloesce Initial
6
+ - npm run migrate:wrangler
7
+
8
+ To start your Cloesce project in development mode, in seperate terminals, run:
9
+ - npm run start:dev
10
+ - npm run start:web
11
+ `;(0,i.create)("create-cloesce",{templateRoot:w,promptForLicense:!1,promptForDescription:!1,promptForEmail:!1,promptForAuthor:!1,skipNpmInstall:!0,defaultLicense:"UNLICENSED",after:async({installNpmPackage:e,packageDir:r})=>{p.default.mkdirSync((0,n.resolve)(r,"migrations")),await e("cloesce wrangler"),await e("@cloudflare/workers-types miniflare vite-tsconfig-paths vitest typescript",!0)},caveat:y});
package/package.json ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "create-cloesce",
3
+ "description": "Create Cloesce App",
4
+ "version": "0.0.1",
5
+ "author": "Ben Schreiber <bpschreiber2003@gmail.com>",
6
+ "scripts": {
7
+ "build": "tsup src/cli.ts --minify",
8
+ "dev": "tsup src/cli.ts --watch",
9
+ "prepublishOnly": "npm run build"
10
+ },
11
+ "bin": "dist/cli.js",
12
+ "files": [
13
+ "dist",
14
+ "templates"
15
+ ],
16
+ "devDependencies": {
17
+ "@types/node": "^17.0.29",
18
+ "tsup": "^5.12.1",
19
+ "typescript": "^4.6.3"
20
+ },
21
+ "license": "Apache-2.0",
22
+ "dependencies": {
23
+ "create-create-app": "^7.3.0"
24
+ }
25
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "paths": [
3
+ "./src/data"
4
+ ],
5
+ "workersUrl": "http://localhost:5000/api",
6
+ "migrationsPath": "./migrations"
7
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "cloesce-proj",
3
+ "version": "1.0.0",
4
+ "description": "",
5
+ "main": "index.js",
6
+ "scripts": {
7
+ "build": "cloesce compile && wrangler build",
8
+ "migrate:cloesce": "cloesce migrate",
9
+ "migrate:wrangler": "wrangler d1 migrations apply db",
10
+ "start:dev": "wrangler dev --port 5000",
11
+ "start:web": "vite",
12
+ "test": "vitest"
13
+ },
14
+ "keywords": [],
15
+ "author": "",
16
+ "license": "ISC",
17
+ "type": "commonjs"
18
+ }
@@ -0,0 +1,46 @@
1
+ import { WranglerEnv, CloesceApp, HttpResult } from "cloesce/backend";
2
+ import { D1Database, R2Bucket, ExecutionContext } from "@cloudflare/workers-types";
3
+
4
+ /**
5
+ * Compiles to the Wrangler configuration file, defining bindings
6
+ * for the Cloudflare Worker environment.
7
+ */
8
+ @WranglerEnv
9
+ export class Env {
10
+ db: D1Database;
11
+ bucket: R2Bucket;
12
+ myVariable: string;
13
+ }
14
+
15
+ // Basic main entry point for a Cloesce App.
16
+ // Does not need to be defined if no customizations are required.
17
+ export default async function main(
18
+ request: Request,
19
+ env: Env,
20
+ app: CloesceApp,
21
+ _ctx: ExecutionContext): Promise<Response> {
22
+ // preflight
23
+ if (request.method === "OPTIONS") {
24
+ return HttpResult.ok(200, undefined, {
25
+ "Access-Control-Allow-Origin": "*",
26
+ "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
27
+ "Access-Control-Allow-Headers": "Content-Type, Authorization",
28
+ }).toResponse();
29
+ }
30
+
31
+ // Run Cloesce app
32
+ const result = await app.run(request, env);
33
+
34
+ // attach CORS headers
35
+ result.headers.set("Access-Control-Allow-Origin", "*");
36
+ result.headers.set(
37
+ "Access-Control-Allow-Methods",
38
+ "GET, POST, PUT, DELETE, OPTIONS"
39
+ );
40
+ result.headers.set(
41
+ "Access-Control-Allow-Headers",
42
+ "Content-Type, Authorization"
43
+ );
44
+
45
+ return result;
46
+ }
@@ -0,0 +1,65 @@
1
+ import { GET, POST, HttpResult, IncludeTree, Integer, Model, R2, Inject } from "cloesce/backend";
2
+ import { R2ObjectBody, ReadableStream } from "@cloudflare/workers-types";
3
+ import { Env } from "./main.cloesce";
4
+
5
+ @Model()
6
+ export class Weather {
7
+ // Cloesce interprets this is a primary key.
8
+ // Optionally, decorate with @PrimaryKey
9
+ id: Integer;
10
+
11
+ // Foreign key to WeatherReport
12
+ // Optionally, decorate with @ForeignKey
13
+ weatherReportId: Integer;
14
+
15
+ // Navigation property to weatherReportId
16
+ // Optionally, decorate with @OneToOne<Weather>(w => w.weatherReportId)
17
+ weatherReport: WeatherReport | undefined;
18
+
19
+ dateTime: Date;
20
+ location: string;
21
+ temperature: number;
22
+ condition: string;
23
+
24
+ @R2("weather/photo/{id}", "bucket")
25
+ photo: R2ObjectBody | undefined;
26
+
27
+ // Hydrates the photo when the client calls "withPhoto"
28
+ static readonly withPhoto: IncludeTree<Weather> = {
29
+ photo: {}
30
+ }
31
+
32
+ @POST
33
+ async uploadPhoto(@Inject env: Env, stream: ReadableStream): Promise<HttpResult<void>> {
34
+ await env.bucket.put(`weather/photo/${this.id}`, stream);
35
+ return HttpResult.ok(200);
36
+ }
37
+
38
+ @GET
39
+ downloadPhoto(): HttpResult<ReadableStream> {
40
+ if (!this.photo) {
41
+ return HttpResult.fail(404, "Photo not found");
42
+ }
43
+ return HttpResult.ok(200, this.photo.body);
44
+ }
45
+ }
46
+
47
+ @Model(["GET", "LIST", "SAVE"])
48
+ export class WeatherReport {
49
+ // Cloesce assumes this is a primary key.
50
+ // Optionally, decorate with @PrimaryKey
51
+ id: Integer;
52
+
53
+ title: string;
54
+ description: string;
55
+
56
+ // Cloesce assumes this is a foreign key to Weather.weatherReportId
57
+ // Optionally, or if multiple FKs exist, decorate with
58
+ // @OneToMany<Weather>(w => w.weatherReportId)
59
+ weatherEntries: Weather[];
60
+
61
+ // Hydrates the weatherEntries when the client calls "withWeatherEntries"
62
+ static readonly withWeatherEntries: IncludeTree<WeatherReport> = {
63
+ weatherEntries: {}
64
+ }
65
+ }
@@ -0,0 +1,203 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>Cloesce Project</title>
8
+ <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
+ }
116
+ </style>
117
+ </head>
118
+
119
+ <body>
120
+ <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>
129
+ </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>
144
+ </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>
171
+ </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>
186
+ </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>
197
+ </div>
198
+ </div>
199
+ </div>
200
+ <script type="module" src="index.ts"></script>
201
+ </body>
202
+
203
+ </html>
@@ -0,0 +1,120 @@
1
+ import { Weather, WeatherReport } from '@generated/client';
2
+
3
+ declare global {
4
+ interface Window {
5
+ listReports: () => Promise<void>;
6
+ saveReport: () => Promise<void>;
7
+ addWeatherEntry: () => Promise<void>;
8
+ uploadPhoto: () => Promise<void>;
9
+ downloadPhoto: () => Promise<void>;
10
+ }
11
+ }
12
+
13
+ const getValue = (id: string) => (document.getElementById(id) as HTMLInputElement).value;
14
+
15
+ const showResult = (outputId: string, result: any) => {
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}`;
21
+ };
22
+
23
+ window.listReports = async () => {
24
+ const result = await WeatherReport.LIST("withWeatherEntries");
25
+ showResult('list-output', result);
26
+ };
27
+
28
+ window.saveReport = async () => {
29
+ const title = getValue('save-title');
30
+ const description = getValue('save-desc');
31
+
32
+ if (!title) {
33
+ document.getElementById('save-output')!.innerHTML = '<div class="error">Please enter a title</div>';
34
+ return;
35
+ }
36
+
37
+ const result = await WeatherReport.SAVE({ title, description });
38
+ showResult('save-output', result);
39
+ };
40
+
41
+ window.addWeatherEntry = async () => {
42
+ const reportId = parseInt(getValue('entry-report-id'));
43
+
44
+ if (!reportId) {
45
+ document.getElementById('entry-output')!.innerHTML = '<div class="error">Please enter a report ID</div>';
46
+ return;
47
+ }
48
+
49
+ const getResult = await WeatherReport.GET(reportId, "withWeatherEntries");
50
+ if (!getResult.ok) {
51
+ showResult('entry-output', getResult);
52
+ return;
53
+ }
54
+
55
+ const report = getResult.data!;
56
+ const newEntry = {
57
+ weatherReportId: reportId,
58
+ location: getValue('entry-location') || '',
59
+ temperature: parseFloat(getValue('entry-temp')) || 0,
60
+ condition: getValue('entry-condition') || '',
61
+ dateTime: getValue('entry-datetime') ? new Date(getValue('entry-datetime')) : new Date()
62
+ };
63
+
64
+ const result = await WeatherReport.SAVE({
65
+ id: report.id,
66
+ title: report.title,
67
+ description: report.description,
68
+ weatherEntries: [...(report.weatherEntries || []), newEntry]
69
+ }, "withWeatherEntries");
70
+
71
+ showResult('entry-output', result);
72
+ };
73
+
74
+ window.uploadPhoto = async () => {
75
+ const id = parseInt(getValue('upload-id'));
76
+ const fileInput = document.getElementById('upload-file') as HTMLInputElement;
77
+ const output = document.getElementById('upload-output')!;
78
+
79
+ if (!id) {
80
+ output.innerHTML = '<div class="error">Please enter a weather entry ID</div>';
81
+ return;
82
+ }
83
+
84
+ if (!fileInput.files?.[0]) {
85
+ output.innerHTML = '<div class="error">Please select a file</div>';
86
+ return;
87
+ }
88
+
89
+ const buffer = await fileInput.files[0].arrayBuffer();
90
+ const weather = new Weather();
91
+ weather.id = id;
92
+
93
+ const result = await weather.uploadPhoto(new Uint8Array(buffer), "withPhoto");
94
+ showResult('upload-output', result);
95
+ };
96
+
97
+ window.downloadPhoto = async () => {
98
+ const id = parseInt(getValue('download-id'));
99
+
100
+ if (!id) {
101
+ document.getElementById('download-output')!.innerHTML = '<div class="error">Please enter a weather entry ID</div>';
102
+ return;
103
+ }
104
+
105
+ const weather = new Weather();
106
+ weather.id = id;
107
+ const result = await weather.downloadPhoto("withPhoto");
108
+
109
+ if (result.ok && result.data) {
110
+ const blob = await result.data.blob();
111
+ const url = URL.createObjectURL(blob);
112
+ const a = document.createElement('a');
113
+ a.href = url;
114
+ a.download = `weather-photo-${id}.jpg`;
115
+ a.click();
116
+ URL.revokeObjectURL(url);
117
+ }
118
+
119
+ showResult('download-output', result);
120
+ };
@@ -0,0 +1,82 @@
1
+ import { Miniflare } from "miniflare";
2
+ import { describe, test, expect, beforeAll } from "vitest";
3
+ import { Orm, CloesceApp } from "cloesce/backend";
4
+ import { cidl, constructorRegistry } from "@generated/workers";
5
+ import { Weather, WeatherReport } from "@data/models.cloesce";
6
+
7
+ async function createTestEnv() {
8
+ const mf = new Miniflare({
9
+ modules: true,
10
+ script: `export default { async fetch() { return new Response("Hello!"); } }`,
11
+ d1Databases: ["db"],
12
+ r2Buckets: ["bucket"],
13
+ });
14
+
15
+ const db = await mf.getD1Database("db");
16
+ const bucket = await mf.getR2Bucket("bucket");
17
+ const env = { db, bucket } as any;
18
+
19
+ // Run any necessary migrations
20
+ // TODO: Does Cloudflare have a way to do this automatically in tests?
21
+ await db.prepare(`
22
+ CREATE TABLE IF NOT EXISTS "WeatherReport" (
23
+ "id" integer PRIMARY KEY,
24
+ "title" text NOT NULL,
25
+ "description" text NOT NULL
26
+ );
27
+ CREATE TABLE IF NOT EXISTS "Weather" (
28
+ "id" integer PRIMARY KEY,
29
+ "weatherReportId" integer NOT NULL,
30
+ "dateTime" text NOT NULL,
31
+ "location" text NOT NULL,
32
+ "temperature" real NOT NULL,
33
+ "condition" text NOT NULL,
34
+ FOREIGN KEY ("weatherReportId") REFERENCES "WeatherReport" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
35
+ );
36
+ CREATE TABLE IF NOT EXISTS "_cloesce_tmp" ("path" text PRIMARY KEY, "id" integer NOT NULL);
37
+ `).run();
38
+
39
+ return { env, orm: Orm.fromEnv(env) };
40
+ }
41
+
42
+ // Cloesce must be initialized before utilizing any ORM features.
43
+ // It takes in the generated Cloesce Interface Definition Language (CIDL)
44
+ // and the generated constructor registry. Both may be imported from
45
+ // "@generated/workers" as shown above.
46
+ beforeAll(() => CloesceApp.init(cidl as any, constructorRegistry));
47
+
48
+ // Here we will test our Cloesce models against a Miniflare environment.
49
+ // This does not use any client stubs; it interacts directly with the Miniflare instance.
50
+ describe("Miniflare Integration Tests", () => {
51
+ test("Download a thumbnail", async () => {
52
+ // Arrange
53
+ const { env, orm } = await createTestEnv();
54
+ const testData = "test-data";
55
+
56
+ const report = await orm.upsert(
57
+ WeatherReport,
58
+ {
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
+ WeatherReport.withWeatherEntries
69
+ );
70
+
71
+ await report!.weatherEntries[0].uploadPhoto(env, testData as any);
72
+
73
+ // Act
74
+ const weatherEntries = await orm.list(Weather, Weather.withPhoto);
75
+ const photo = weatherEntries[0].downloadPhoto();
76
+
77
+ // Assert
78
+ expect(photo.ok).toBe(true);
79
+ const downloadedText = await new Response(photo.data as any).text();
80
+ expect(downloadedText).toBe(testData);
81
+ });
82
+ });
@@ -0,0 +1,24 @@
1
+ {
2
+ // Visit https://aka.ms/tsconfig to read more about this file
3
+ "compilerOptions": {
4
+ "module": "ESNext",
5
+ "target": "ES2020",
6
+ "moduleResolution": "node",
7
+ "types": [],
8
+ "skipLibCheck": true,
9
+
10
+ // Cloesce required options
11
+ "resolveJsonModule": true,
12
+ "strict": true,
13
+ "strictPropertyInitialization": false,
14
+ "experimentalDecorators": true,
15
+ "emitDecoratorMetadata": true,
16
+ "allowSyntheticDefaultImports": true,
17
+ "paths": {
18
+ "@data/*": ["./src/data/*"],
19
+ "@generated/*": ["./.generated/*"],
20
+ },
21
+ "outDir": "dist",
22
+ },
23
+ "include": [".generated/*.ts", "src/**/*.ts", "test/**/*.ts"],
24
+ }
@@ -0,0 +1,7 @@
1
+ import { defineConfig } from "vite";
2
+ import tsconfigPaths from "vite-tsconfig-paths";
3
+
4
+ export default defineConfig({
5
+ plugins: [tsconfigPaths()],
6
+ root: "./src/web"
7
+ });
@@ -0,0 +1 @@
1
+ main = ".generated/workers.ts"