cloesce 0.0.4-unstable.9 → 0.0.5-unstable.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 DELETED
@@ -1,632 +0,0 @@
1
- # cloesce (unstable, `v0.0.4`)
2
-
3
- Cloesce is a full stack compiler for the Cloudflare developer platform, allowing class definitions in high level languages to serve as a metadata basis to create a database schema, backend REST API, frontend API client, and Cloudflare infrastructure (as of v0.0.4, D1 + Workers).
4
-
5
- Cloesce is working towards a stable alpha MVP (v0.1.0), with the general milestones being [here](https://cloesce.pages.dev/schreiber/v0.1.0_milestones/).
6
-
7
- Internal documentation going over design decisions and general thoughts for each milestone can be found [here](https://cloesce.pages.dev/).
8
-
9
- # Documentation
10
-
11
- ## Getting Started
12
-
13
- `v0.0.4` supports only Typescript-to-Typescript projects. An example project is shown [here](https://github.com/bens-schreiber/cloesce/tree/main/examples).
14
-
15
- ### 1) NPM
16
-
17
- Create an NPM project and install cloesce
18
-
19
- ```sh
20
- npm i cloesce@0.0.4-unstable.9
21
- ```
22
-
23
- ### 2) TypeScript
24
-
25
- Create a `tsconfig.json` with the following values:
26
-
27
- ```json
28
- {
29
- "compilerOptions": {
30
- // ...
31
- "resolveJsonModule": true,
32
- "strict": true,
33
- "strictPropertyInitialization": false,
34
- "experimentalDecorators": true,
35
- "emitDecoratorMetadata": true
36
- },
37
- "include": ["<your_src_dir>/**/*.ts", ".generated/*.ts"]
38
- }
39
- ```
40
-
41
- ### 3) Cloesce Config
42
-
43
- Create a `cloesce.config.json` with your desired configuration:
44
-
45
- ```json
46
- {
47
- "source": "./src",
48
- "workersUrl": "http://localhost:5000/api",
49
- "clientUrl": "http://localhost:5173/api"
50
- }
51
- ```
52
-
53
- ### 4) Vite
54
-
55
- To prevent CORS issues, a Vite proxy can be used for the frontend:
56
-
57
- ```ts
58
- import { defineConfig } from "vite";
59
-
60
- export default defineConfig({
61
- server: {
62
- port: 5173,
63
- proxy: {
64
- "/api": {
65
- target: "http://localhost:5000",
66
- changeOrigin: true,
67
- },
68
- },
69
- },
70
- });
71
- ```
72
-
73
- Middleware support for CORS is also supported (see Middleware section).
74
-
75
- ### 5) Wrangler Config
76
-
77
- Cloesce will generate any missing `wrangler.toml` values (or the file if missing). A minimal `wrangler.toml` looks like this:
78
-
79
- ```toml
80
- compatibility_date = "2025-10-02"
81
- main = ".generated/workers.ts"
82
- name = "example"
83
-
84
- [[d1_databases]]
85
- binding = "db"
86
- database_id = "..."
87
- database_name = "example"
88
- ```
89
-
90
- ## Cloesce Models
91
-
92
- A model is a type which represents:
93
-
94
- - a database table,
95
- - REST API
96
- - Client API
97
- - Cloudflare infrastructure (D1 + Workers)
98
-
99
- Suprisingly, it's pretty compact. A basic model looks like this:
100
-
101
- ```ts
102
- // horse.cloesce.ts
103
- import { D1, GET, POST, PrimaryKey } from "cloesce/backend";
104
-
105
- @D1
106
- export class Horse {
107
- @PrimaryKey
108
- id: number;
109
-
110
- name: string | null;
111
-
112
- @POST
113
- neigh(): string {
114
- return `i am ${this.name}, this is my horse noise`;
115
- }
116
- }
117
- ```
118
-
119
- - `@D1` denotes that this is a SQL Table
120
- - `@PrimaryKey` sets the SQL primary key. All models require a primary key.
121
- - `@POST` reveals the method as an API endpoint with the `POST` HTTP Verb.
122
- - All Cloesce models need to be under a `.cloesce.ts` file.
123
-
124
- To compile this model into a working full stack application, Cloesce must undergo both **compilation** and **migrations**.
125
-
126
- Compilation is the process of extracting the metadata language that powers Cloesce, ensuring it is a valid program, and then producing code to orchestrate the program across different domains (database, backend, frontend, cloud).
127
-
128
- Migrations utilize the history of validated metadata to create SQL code, translating the evolution of your models.
129
-
130
- ### Compiling
131
-
132
- - `npx cloesce compile`
133
- - `npx cloesce migrate <name>`.
134
-
135
- After running the above commands, you will have a full project capable of being ran with Wrangler:
136
-
137
- ```sh
138
- # Apply the generated migrations
139
- npx wrangler d1 migrations apply <db-name>
140
-
141
- # Run the backend
142
- npx wrangler dev
143
- ```
144
-
145
- ### Compiled Artifacts
146
-
147
- #### `.generated/`
148
-
149
- These values should not be committed to git, as they depend on the file system of the machine running it.
150
-
151
- - `client.ts` is an importable API with all of your backend types and endpoints
152
- - `workers.ts` is the workers entrypoint.
153
- - `cidl.json` is the working metadata for the project
154
-
155
- #### `migrations`
156
-
157
- After running `npx cloesce migrate <name>`, a new migration will be created in the `migrations/` folder. For example, after creating a migration called `Initial`, you will see:
158
-
159
- - `<date>_Initial.json` contains all model information necessary from SQL
160
- - `<date>_Initial.sql` contains the acual SQL migrations. In this early version of Cloesce, it's important to check migrations every time.
161
-
162
- #### Supported Column Types
163
-
164
- Model columns must directly map to SQLite columns. The supported TypeScript types are:
165
-
166
- - `number` => `Real` not null
167
- - `string` => `Text` not null
168
- - `boolean` and `Boolean` => `Integer` not null
169
- - `Integer` => `Integer` not null
170
- - `Date` => `Text` (ISO formatted) not null
171
- - `N | null` => `N` (nullable)
172
-
173
- Blob types will be added in v0.0.5 when R2 support is added.
174
-
175
- ## Features
176
-
177
- ### Wrangler Environment
178
-
179
- In order to interact with your database, you will need to define a WranglerEnv
180
-
181
- ```ts
182
- import { WranglerEnv } from "cloesce/backend";
183
-
184
- @WranglerEnv
185
- export class MyEnv {
186
- db: D1Database; // only one DB is supported for now-- make sure it matches the name in `wrangler.toml`
187
-
188
- // you can also define values in the toml under [[vars]]
189
- motd: string;
190
- }
191
- ```
192
-
193
- Your WranglerEnv can then be injected into any model method using the `@Inject` decorator:
194
-
195
- ```ts
196
- @D1
197
- export class Horse {
198
- @PrimaryKey
199
- id: number;
200
-
201
- @POST
202
- async neigh(@Inject env: MyEnv): Promise<string> {
203
- await env.db.prepare(...);
204
-
205
- return `i am ${this.name}, this is my horse noise`;
206
- }
207
- }
208
- ```
209
-
210
- ### Foreign Key Column
211
-
212
- Reference another model via a foreign key using the `@ForeignKey` decorator:
213
-
214
- ```ts
215
- @D1
216
- export class Dog {
217
- @PrimaryKey
218
- id: number;
219
- }
220
-
221
- @D1
222
- export class Person {
223
- @PrimaryKey
224
- id: number;
225
-
226
- @ForeignKey(Dog)
227
- dogId: number;
228
- }
229
- ```
230
-
231
- ### One to One
232
-
233
- Cloesce allows you to relate models via `1:1` relationships using the `@OneToOne` decorator. It requires that a foreign key already exists on the model.
234
-
235
- ```ts
236
- @D1
237
- export class Dog {
238
- @PrimaryKey
239
- id: number;
240
- }
241
-
242
- @D1
243
- export class Person {
244
- @PrimaryKey
245
- id: number;
246
-
247
- @ForeignKey(Dog)
248
- dogId: number;
249
-
250
- @OneToOne("dogId") // references Person.dogId
251
- dog: Dog | undefined; // This value is a "navigation property", which may or may not exist at runtime
252
- }
253
- ```
254
-
255
- In `v0.0.4`, there are no defaults, only very explicit decisons. Because of that, navigation properties won't exist at runtime unless you tell them to. Cloesce does this via a `Data Source`, which describes the foreign key dependencies you wish to include. All scalar properties are included by default and cannot be excluded.
256
-
257
- ```ts
258
- @D1
259
- export class Dog {
260
- @PrimaryKey
261
- id: number;
262
- }
263
-
264
- @D1
265
- export class Person {
266
- @PrimaryKey
267
- id: number;
268
-
269
- @ForeignKey(Dog)
270
- dogId: number;
271
-
272
- @OneToOne("dogId")
273
- dog: Dog | undefined;
274
-
275
- @DataSource
276
- static readonly default: IncludeTree<Person> = {
277
- dog: {}, // says: on model population, join Persons's Dog
278
- };
279
- }
280
- ```
281
-
282
- Data sources describe how foreign keys should be joined on model hydration (i.e. when invoking any instantiated method). They are composed of an `IncludeTree<T>`, a recursive type composed of the relationships you wish to include. All scalar properties are always included.
283
-
284
- Note that `DataSourceOf` is added implicitly to all instantiated methods if no data source parameter is defined.
285
-
286
- ### One to Many
287
-
288
- Cloesce supports models with `1:M` relationships:
289
-
290
- ```ts
291
- @D1
292
- export class Person {
293
- @PrimaryKey
294
- id: number;
295
-
296
- @OneToMany("personId") // directly references the FK on Dog
297
- dogs: Dog[];
298
-
299
- @DataSource
300
- static readonly default: IncludeTree<Person> = {
301
- dogs: {
302
- person: {
303
- dogs: {
304
- // essentially means: "When you get a person, get their dogs, and get all of those dog's Person, ..."
305
- // we could go on as long as we want
306
- },
307
- },
308
- },
309
- };
310
- }
311
-
312
- @D1
313
- export class Dog {
314
- @PrimaryKey
315
- id: number;
316
-
317
- @ForeignKey(Person)
318
- personId: number;
319
-
320
- // optional navigation property, not needed.
321
- @OneToOne("personId")
322
- person: Person | undefined;
323
- }
324
- ```
325
-
326
- ### Many to Many
327
-
328
- ```ts
329
- @D1
330
- export class Student {
331
- @PrimaryKey
332
- id: number;
333
-
334
- @ManyToMany("StudentsCourses") // unique ID for the generated junction table
335
- courses: Course[];
336
- }
337
-
338
- @D1
339
- export class Course {
340
- @PrimaryKey
341
- id: number;
342
-
343
- @ManyToMany("StudentsCourses") // same unique id => same jct table.
344
- students: Student[];
345
- }
346
- ```
347
-
348
- ### DataSourceOf<T>
349
-
350
- If it is important to determine what data source the frontend called the instantiated method with, the type `DataSourceOf<T>` allows explicit data source parameters:
351
-
352
- ```ts
353
- @D1
354
- class Foo {
355
- ...
356
-
357
- @POST
358
- bar(ds: DataSourceOf<Foo>) {
359
- // ds = "DataSource1" | "DataSource2" | ... | "none"
360
- }
361
- }
362
- ```
363
-
364
- Data sources are implicitly added to all instantiated methods if no data source parameter is defined.
365
-
366
- ### ORM Methods
367
-
368
- Cloesce provides a suite of ORM methods for getting, listing, updating and inserting models.
369
-
370
- #### Upsert
371
-
372
- ```ts
373
- @D1
374
- class Horse {
375
- // ...
376
-
377
- @POST
378
- static async post(@Inject { db }: Env, horse: Horse): Promise<Horse> {
379
- const orm = Orm.fromD1(db);
380
- await orm.upsert(Horse, horse, null);
381
- return (await orm.get(Horse, horse.id, null)).value;
382
- }
383
- }
384
- ```
385
-
386
- #### List, Get
387
-
388
- Both methods take an optional `IncludeTree<T>` parameter to specify what relationships in the generated CTE.
389
-
390
- ```ts
391
- @D1
392
- export class Person {
393
- @PrimaryKey
394
- id: number;
395
-
396
- @ForeignKey(Dog)
397
- dogId: number;
398
-
399
- @OneToOne("dogId")
400
- dog: Dog | undefined;
401
-
402
- @DataSource
403
- static readonly default: IncludeTree<Person> = {
404
- dog: {},
405
- };
406
- }
407
- ```
408
-
409
- Running `Orm.listQuery` with the data source `Person.default` would produce the CTE:
410
-
411
- ```sql
412
- WITH "Person_view" AS (
413
- SELECT
414
- "Person"."id" AS "id",
415
- "Person"."dogId" AS "dogId",
416
- "Dog"."id" AS "dog.id"
417
- FROM
418
- "Person"
419
- LEFT JOIN
420
- "Dog" ON "Person"."dogId" = "Dog"."id"
421
- ) SELECT * FROM "Person_view"
422
- ```
423
-
424
- Example usages:
425
-
426
- ```ts
427
- @D1
428
- class Horse {
429
- // ...
430
- @GET
431
- static async get(@Inject { db }: Env, id: number): Promise<Horse> {
432
- const orm = Orm.fromD1(db);
433
- return (await orm.get(Horse, id, Horse.default)).value;
434
- }
435
-
436
- @GET
437
- static async list(@Inject { db }: Env): Promise<Horse[]> {
438
- const orm = Orm.fromD1(db);
439
- return (await orm.list(Horse, {})).value;
440
- }
441
- }
442
- ```
443
-
444
- `list` takes an optional `from` parameter to modify the source of the list query. This is useful in filtering / limiting results.
445
-
446
- ```ts
447
- await orm.list(
448
- Horse,
449
- Horse.default,
450
- "SELECT * FROM Horse ORDER BY name LIMIT 10",
451
- );
452
- ```
453
-
454
- produces SQL
455
-
456
- ```sql
457
- WITH "Horse_view" AS (
458
- SELECT
459
- "Horse"."id" AS "id",
460
- "Horse"."name" AS "name"
461
- FROM
462
- (SELECT * FROM Horse ORDER BY name LIMIT 10) as "Horse"
463
- ) SELECT * FROM "Horse_view"
464
- ```
465
-
466
- ### CRUD Methods
467
-
468
- Generic `GET, POST, PATCH` (and in a future version, DEL) boilerplate methods do not need to be copied around. Cloesce supports CRUD generation, a syntactic sugar that adds the methods to the compiler output.
469
-
470
- The `SAVE` method is an `upsert`, meaning it both inserts and updates in the same query.
471
-
472
- ```ts
473
- @CRUD(["SAVE", "GET", "LIST"])
474
- @D1
475
- export class CrudHaver {
476
- @PrimaryKey
477
- id: number;
478
- name: string;
479
- }
480
- ```
481
-
482
- which will generate client API methods like:
483
-
484
- ```ts
485
- static async get(
486
- id: number,
487
- dataSource: "none" = "none",
488
- ): Promise<HttpResult<CrudHaver>>
489
- ```
490
-
491
- ### Middleware
492
-
493
- Cloesce supports middleware at the global level (before routing to a model+method), the model level (before validation) and the method level (before hydration). Middleware also exposes read/write access to the dependency injection instance that all models use.
494
-
495
- Middleware is capable of exiting from the Cloesce Router early with an HTTP Result.
496
-
497
- An example of all levels of middleware is below. All middleware must be defined in the file `app.cloesce.ts` which exports a `CloesceApp` instance as default.
498
-
499
- ```ts
500
- @PlainOldObject
501
- export class InjectedThing {
502
- value: string;
503
- }
504
-
505
- @WranglerEnv
506
- export class Env {
507
- db: D1Database;
508
- }
509
-
510
- @D1
511
- @CRUD(["POST"])
512
- export class Model {
513
- @PrimaryKey
514
- id: number;
515
-
516
- @GET
517
- static blockedMethod() {}
518
-
519
- @GET
520
- static getInjectedThing(@Inject thing: InjectedThing): InjectedThing {
521
- return thing;
522
- }
523
- }
524
-
525
- const app: CloesceApp = new CloesceApp();
526
-
527
- app.onRequest((request: Request, env, ir) => {
528
- if (request.method === "POST") {
529
- return { ok: false, status: 401, message: "POST methods aren't allowed." };
530
- }
531
- });
532
-
533
- app.onModel(Model, (request, env, ir) => {
534
- ir.set(InjectedThing.name, {
535
- value: "hello world",
536
- });
537
- });
538
-
539
- app.onMethod(Model, "blockedMethod", (request, env, ir) => {
540
- return { ok: false, status: 401, message: "Blocked method" };
541
- });
542
-
543
- app.onResponse(async (request, env, di, response: Response) => {
544
- // basic CORS, allow all origins
545
- response.headers.set("Access-Control-Allow-Origin", "*");
546
- response.headers.set(
547
- "Access-Control-Allow-Methods",
548
- "GET, POST, PUT, DELETE, OPTIONS",
549
- );
550
- response.headers.set(
551
- "Access-Control-Allow-Headers",
552
- "Content-Type, Authorization",
553
- );
554
- });
555
-
556
- export default app;
557
- ```
558
-
559
- With this middleware, all POST methods will be blocked, and all methods for the model `Model` will be able to inject `InjectedThing`,and `blockedMethod` will return a 401. Additionally, all responses will have CORS headers.
560
-
561
- ### Plain Old Objects
562
-
563
- Simple non-model objects can be returned and serialized from a model method:
564
-
565
- ```ts
566
- @PlainOldObject
567
- export class CatStuff {
568
- catFacts: string[],
569
- catNames: string[],
570
- }
571
-
572
- @D1
573
- export class Cat {
574
- @PrimaryKey
575
- id: number;
576
-
577
- @GET
578
- query(): CatStuff {
579
- return {
580
- catFacts: ["cats r cool"],
581
- catNames: ["reginald"]
582
- }
583
- }
584
- }
585
- ```
586
-
587
- ### HttpResult
588
-
589
- Methods can return any kind of status code via the `HttpResult` wrapper:
590
-
591
- ```ts
592
- @D1
593
- class Foo {
594
- ...
595
-
596
- @GET
597
- async foo(): Promise<HttpResult<number>> {
598
- return { ok: false, status: 500, message: "divided by 0"};
599
- }
600
- }
601
- ```
602
-
603
- # Testing the Compiler
604
-
605
- ## Unit Tests
606
-
607
- - `src/frontend/ts` run `npm test`
608
- - `src/generator` run `cargo test`
609
-
610
- ## Integration Tests
611
-
612
- - Regression tests: `cargo run --bin test regression`
613
-
614
- Optionally, pass `--check` if new snapshots should not be created.
615
-
616
- To target a specific fixture, pass `--fixture folder_name`
617
-
618
- To update integration snapshots, run:
619
-
620
- - `cargo run --bin update`
621
-
622
- To delete any generated snapshots run:
623
-
624
- - `cargo run --bin update -- -d`
625
-
626
- ## E2E
627
-
628
- - `tests/e2e` run `npm test`
629
-
630
- ## Code Formatting
631
-
632
- - `cargo fmt`, `cargo clippy`, `npm run format:fix`