create-cloesce 0.0.6 → 0.3.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.0.6",
4
+ "version": "0.3.0",
5
5
  "author": "Ben Schreiber <bpschreiber2003@gmail.com>",
6
6
  "repository": {
7
7
  "url": "https://github.com/bens-schreiber/create-cloesce-app"
@@ -0,0 +1,9 @@
1
+ import { CloesceConfigOptions } from "cloesce";
2
+
3
+ const config: CloesceConfigOptions = {
4
+ srcPaths: ["./src/schema"],
5
+ workersUrl: "http://localhost:5000/api",
6
+ wranglerConfigFormat: "jsonc",
7
+ };
8
+
9
+ export default config;
@@ -5,14 +5,14 @@
5
5
  "main": "index.js",
6
6
  "scripts": {
7
7
  "build": "cloesce compile && wrangler build",
8
- "migrate:cloesce": "cloesce migrate",
8
+ "migrate:cloesce": "cloesce migrate db",
9
9
  "migrate:wrangler": "wrangler d1 migrations apply db",
10
10
  "start:dev": "wrangler dev --port 5000",
11
11
  "start:web": "vite",
12
12
  "test": "vitest"
13
13
  },
14
14
  "dependencies": {
15
- "cloesce": ">=0.1.0",
15
+ "cloesce": ">=0.3.0",
16
16
  "wrangler": "^4.61.1"
17
17
  },
18
18
  "devDependencies": {
@@ -0,0 +1,62 @@
1
+ // Here, we import the generated backend code, which includes all the types
2
+ // defined in the `schema.clo` file
3
+ import * as Cloesce from "@cloesce/backend.js";
4
+ import { CfReadableStream } from "@cloesce/backend.js";
5
+
6
+ // The "cloesce" library provides basic types and utilities for building a Cloesce backend
7
+ import { HttpResult } from "cloesce";
8
+
9
+ // All API routes defined under a model in `schema.clo` are generated under their
10
+ // respective models namespace.
11
+ //
12
+ // To implement an API route, simply extend the generated Api class.
13
+ // If not implemented, the route will return a 501 Not Implemented error by default.
14
+ export class Weather extends Cloesce.Weather.Api {
15
+ async uploadPhoto(self: Cloesce.Weather.Self, e: Cloesce.Env, s: CfReadableStream): Promise<void> {
16
+ // All models have a `KeyFormat` namespace which provides utilities for generating
17
+ // KV and R2 keys for that model.
18
+ const key = Cloesce.Weather.KeyFormat.photo(self.id);
19
+ await e.bucket.put(key, s);
20
+ }
21
+
22
+ // Any method can return an HttpResult, which allows you to specify the
23
+ // Response status code, body, and headers.
24
+ downloadPhoto(self: Cloesce.Weather.Self): HttpResult<CfReadableStream> {
25
+ if (!self.photo) {
26
+ return HttpResult.fail(404, "Photo not found");
27
+ }
28
+ return HttpResult.ok(200, self.photo.body);
29
+ }
30
+ }
31
+
32
+ export default {
33
+ async fetch(request: Request, env: Cloesce.Env): Promise<Response> {
34
+ // preflight
35
+ if (request.method === "OPTIONS") {
36
+ return HttpResult.ok(200, undefined, {
37
+ "Access-Control-Allow-Origin": "*",
38
+ "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
39
+ "Access-Control-Allow-Headers": "Content-Type, Authorization",
40
+ }).toResponse();
41
+ }
42
+
43
+ // Run Cloesce app
44
+ const app = (await Cloesce.cloesce())
45
+ .register(new Weather());
46
+
47
+ const result = await app.run(request, env);
48
+
49
+ // attach CORS headers
50
+ result.headers.set("Access-Control-Allow-Origin", "*");
51
+ result.headers.set(
52
+ "Access-Control-Allow-Methods",
53
+ "GET, POST, PUT, DELETE, OPTIONS"
54
+ );
55
+ result.headers.set(
56
+ "Access-Control-Allow-Headers",
57
+ "Content-Type, Authorization"
58
+ );
59
+
60
+ return result;
61
+ }
62
+ };
@@ -0,0 +1,102 @@
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
14
+ }
15
+ }
16
+
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.
26
+ primary {
27
+ id: int
28
+ }
29
+
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.
33
+ //
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) {
40
+ weatherEntries
41
+ }
42
+
43
+ // All fields that are not apart of a block are just regular fields in the table.
44
+ title: string
45
+ description: string
46
+ }
47
+
48
+ [use db]
49
+ [use get, list, save]
50
+ model Weather {
51
+ primary {
52
+ id: int
53
+ }
54
+
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) {
58
+ weatherReportId
59
+
60
+ // A `nav` block inside a `foreign` block generates a navigation field,
61
+ // meaning the backend and client will have a WeatherReport object named `weatherReport` nested inside the Weather model.
62
+ nav { weatherReport }
63
+ }
64
+
65
+ r2 (bucket, "weather/photos/{id}.jpg") {
66
+ photo
67
+ }
68
+
69
+ dateTime: date
70
+ location: string
71
+ temperature: int
72
+ condition: string
73
+ }
74
+
75
+ // An `api` block describes what endpoints must be implemented in the backend
76
+ // and generated in the client. Many blocks can be defined, but they all together make up one API.
77
+ //
78
+ // All endpoints in Cloesce are HTTP endpoints.
79
+ api Weather {
80
+ // If `self` is passed as an argument, it means the endpoint requires an
81
+ // instantiated Weather object to be hydrated on the backend before the endpoint can be executed.
82
+ //
83
+ // In this case, `uploadPhoto` accepts the Wrangler environment (dependency injected),
84
+ // and a stream (the photo to be uploaded).
85
+ post uploadPhoto(self, e: env, s: stream) -> void
86
+
87
+ // By default, Cloesce will hydrate a `self` instance with all 1:1, KV, R2,
88
+ // and the near side of 1:M/M:M relationships.
89
+ //
90
+ // To avoid overfetching, a `source` tag can be used on `self`, which uses the
91
+ // specified data source instead. In this case, `downloadPhoto` will only hydrate the `photo` field.
92
+ // (NOTE: The Weather table is always queried to hydrate columns, regardless of the source.)
93
+ get downloadPhoto([source R2Only] self) -> stream
94
+ }
95
+
96
+ // A `source` block describes a data souce that can be used throughout
97
+ // a Cloesce program to hydrate and list models.
98
+ //
99
+ // In this case, the include tree has only the `photo` field of `Weather`.
100
+ source R2Only for Weather {
101
+ include { photo }
102
+ }
@@ -1,4 +1,6 @@
1
- import { Weather, WeatherReport } from '@generated/client';
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';
2
4
 
3
5
  declare global {
4
6
  interface Window {
@@ -21,7 +23,10 @@ const showResult = (outputId: string, result: any) => {
21
23
  };
22
24
 
23
25
  window.listReports = async () => {
24
- const result = await WeatherReport.LIST("withWeatherEntries");
26
+ const result = await WeatherReport.$list({
27
+ lastSeen_id: 0,
28
+ limit: 100
29
+ })
25
30
  showResult('list-output', result);
26
31
  };
27
32
 
@@ -34,7 +39,7 @@ window.saveReport = async () => {
34
39
  return;
35
40
  }
36
41
 
37
- const result = await WeatherReport.SAVE({ title, description });
42
+ const result = await WeatherReport.$save({ title, description });
38
43
  showResult('save-output', result);
39
44
  };
40
45
 
@@ -46,7 +51,9 @@ window.addWeatherEntry = async () => {
46
51
  return;
47
52
  }
48
53
 
49
- const getResult = await WeatherReport.GET(reportId, "withWeatherEntries");
54
+ const getResult = await WeatherReport.$get({
55
+ id: reportId
56
+ });
50
57
  if (!getResult.ok) {
51
58
  showResult('entry-output', getResult);
52
59
  return;
@@ -61,12 +68,12 @@ window.addWeatherEntry = async () => {
61
68
  dateTime: getValue('entry-datetime') ? new Date(getValue('entry-datetime')) : new Date()
62
69
  };
63
70
 
64
- const result = await WeatherReport.SAVE({
71
+ const result = await WeatherReport.$save({
65
72
  id: report.id,
66
73
  title: report.title,
67
74
  description: report.description,
68
75
  weatherEntries: [...(report.weatherEntries || []), newEntry]
69
- }, "withWeatherEntries");
76
+ });
70
77
 
71
78
  showResult('entry-output', result);
72
79
  };
@@ -90,7 +97,7 @@ window.uploadPhoto = async () => {
90
97
  const weather = new Weather();
91
98
  weather.id = id;
92
99
 
93
- const result = await weather.uploadPhoto(new Uint8Array(buffer), "withPhoto");
100
+ const result = await weather.uploadPhoto(new Uint8Array(buffer));
94
101
  showResult('upload-output', result);
95
102
  };
96
103
 
@@ -104,7 +111,7 @@ window.downloadPhoto = async () => {
104
111
 
105
112
  const weather = new Weather();
106
113
  weather.id = id;
107
- const result = await weather.downloadPhoto("withPhoto");
114
+ const result = await weather.downloadPhoto();
108
115
 
109
116
  if (result.ok && result.data) {
110
117
  const blob = await result.data.blob();
@@ -1,8 +1,8 @@
1
1
  import { Miniflare } from "miniflare";
2
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";
3
+ import * as Cloesce from "@cloesce/backend.js";
4
+ import { Weather } from "@api/main.js"
5
+ import { cloesce } from "@cloesce/backend.js";
6
6
 
7
7
  async function createTestEnv() {
8
8
  const mf = new Miniflare({
@@ -19,60 +19,59 @@ async function createTestEnv() {
19
19
  // Run any necessary migrations
20
20
  // TODO: Does Cloudflare have a way to do this automatically in tests?
21
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);
22
+ --- New Models
23
+ CREATE TABLE IF NOT EXISTS "WeatherReport" (
24
+ "id" integer PRIMARY KEY,
25
+ "title" text NOT NULL,
26
+ "description" text NOT NULL
27
+ );
28
+
29
+ CREATE TABLE IF NOT EXISTS "Weather" (
30
+ "id" integer PRIMARY KEY,
31
+ "weatherReportId" integer NOT NULL,
32
+ "dateTime" text NOT NULL,
33
+ "location" text NOT NULL,
34
+ "temperature" integer NOT NULL,
35
+ "condition" text NOT NULL,
36
+ FOREIGN KEY ("weatherReportId") REFERENCES "WeatherReport" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
37
+ );
38
+
39
+ --- Cloesce Temporary Table
40
+ CREATE TABLE IF NOT EXISTS "_cloesce_tmp" (
41
+ "path" text PRIMARY KEY,
42
+ "primary_key" text NOT NULL
43
+ );
37
44
  `).run();
38
45
 
39
- return { env, orm: Orm.fromEnv(env) };
46
+ return env;
40
47
  }
41
48
 
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));
49
+ beforeAll(() => cloesce());
47
50
 
48
51
  // Here we will test our Cloesce models against a Miniflare environment.
49
52
  // This does not use any client stubs; it interacts directly with the Miniflare instance.
50
53
  describe("Miniflare Integration Tests", () => {
51
54
  test("Download a thumbnail", async () => {
52
55
  // Arrange
53
- const { env, orm } = await createTestEnv();
56
+ const env = await createTestEnv();
54
57
  const testData = "test-data";
55
58
 
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
- );
59
+ const report = (await Cloesce.WeatherReport.save(env, {
60
+ title: "Test Report",
61
+ description: "This is a test weather report.",
62
+ weatherEntries: [{
63
+ dateTime: new Date(),
64
+ location: "Test Location",
65
+ temperature: 25,
66
+ condition: "Sunny"
67
+ }]
68
+ }))!;
70
69
 
71
- await report!.weatherEntries[0].uploadPhoto(env, testData as any);
70
+ await new Weather().uploadPhoto(report.weatherEntries[0], env, testData as any);
72
71
 
73
72
  // Act
74
- const weatherEntries = await orm.list(Weather, Weather.withPhoto);
75
- const photo = weatherEntries[0].downloadPhoto();
73
+ const weatherEntries = (await Cloesce.Weather.DataSources.Default.list(env, 0, 100))!;
74
+ const photo = new Weather().downloadPhoto(weatherEntries[0]);
76
75
 
77
76
  // Assert
78
77
  expect(photo.ok).toBe(true);
@@ -1,9 +1,9 @@
1
1
  {
2
2
  // Visit https://aka.ms/tsconfig to read more about this file
3
3
  "compilerOptions": {
4
- "module": "ESNext",
4
+ "module": "node20",
5
5
  "target": "ES2020",
6
- "moduleResolution": "node",
6
+ "moduleResolution": "node16",
7
7
  "types": [],
8
8
  "skipLibCheck": true,
9
9
 
@@ -15,10 +15,10 @@
15
15
  "emitDecoratorMetadata": true,
16
16
  "allowSyntheticDefaultImports": true,
17
17
  "paths": {
18
- "@data/*": ["./src/data/*"],
19
- "@generated/*": ["./.generated/*"],
18
+ "@api/*": ["./src/api/*"],
19
+ "@cloesce/*": ["./.cloesce/*"],
20
20
  },
21
21
  "outDir": "dist",
22
22
  },
23
- "include": [".generated/*.ts", "src/**/*.ts", "test/**/*.ts"],
23
+ "include": [".cloesce/*.ts", "src/**/*.ts", "test/**/*.ts"],
24
24
  }
@@ -0,0 +1,3 @@
1
+ {
2
+ "main": "src/api/main.ts"
3
+ }
@@ -1,7 +0,0 @@
1
- {
2
- "paths": [
3
- "./src/data"
4
- ],
5
- "workersUrl": "http://localhost:5000/api",
6
- "migrationsPath": "./migrations"
7
- }
@@ -1,46 +0,0 @@
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
- }
@@ -1,65 +0,0 @@
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
- }
@@ -1 +0,0 @@
1
- main = ".generated/workers.ts"