# Summary [Introduction](./ch0-0-introduction.md) [LLMs and TypeDoc](./ch0-1-typedoc.md) - [Getting Started](./ch1-0-getting-started.md) - [Installation](./ch1-1-installation.md) - [Exploring the Template](./ch1-2-exploring-the-template.md) - [Building and Migrating](./ch1-3-building-and-migrating.md) - [Deploying](./ch1-4-deploying.md) - [Models](./ch2-0-models.md) - [Basic D1 Backed Model](./ch2-1-d1-backed-model.md) - [D1 Navigation Properties](./ch2-2-navigation-properties.md) - [Data Sources](./ch2-3-data-sources.md) - [KV and R2 Properties](./ch2-4-kv-r2-properties.md) - [Model Methods](./ch2-5-model-methods.md) - [Cloesce ORM](./ch2-6-cloesce-orm.md) - [Wrangler Environment](./ch2-7-wrangler-environment.md) - [Services](./ch3-0-services.md) - [Middleware](./ch4-0-middleware.md) - [Testing](./ch5-0-testing.md) - [Compiler Reference](./ch6-0-compiler-reference.md) - [Future Vision](./ch6-1-future-vision.md) - [Architecture Overview](./ch6-2-architecture-overview.md) \# Introduction > \[!WARNING\] Cloesce is under active development, expanding its > feature set as it pushes toward [full Cloudflare support across any > language](./ch6-1-future-vision.md). In this alpha, breaking changes > can occur between releases. *Cloesce* converts class definitions into a full stack Cloudflare application. Inspired by - [Entity Framework](https://learn.microsoft.com/en-us/ef/) - [NestJS](https://nestjs.com/) - [ASP.NET](https://dotnet.microsoft.com/en-us/apps/aspnet) - [Swagger Codegen](https://swagger.io/tools/swagger-codegen/) - [gRPC](https://grpc.io/) - and Infrastructure as Code (IaC) Cloesce is not just an ORM, migration engine, web framework, runtime validation library, IaC tool, or API Generator. It is **all of these things and more**, wrapped in a clean paradigm that makes building Cloudflare applications a breeze. ```{=html} ``` ``` typescript @Crud("GET", "SAVE", "LIST") @Model("db") class User { id: Integer; name: String; posts: Post[]; @KV("user/settings/{id}", namespace) settings: KValue; @R2("user/avatars/{id}.png", bucket) avatar: R2Object; @Post() hello(): User { return this; } } ``` ``` python # Coming in a later release! ``` ``` rs // Coming in a later release! ``` ```{=html} ``` *How easy can full stack development get?* ## Contributing Contributions are welcome at all levels. Join our [Discord](https://discord.gg/saVTbcGHwF) to discuss ideas, report issues, or get help getting started. [Create an issue](https://github.com/bens-schreiber/cloesce/issues/new) on GitHub if you find a bug or have a feature request. ## Coalesce Check out [Coalesce](https://coalesce.intellitect.com), an accelerated web app framework for Vue.js and Entity Framework by [IntelliTect](https://intellitect.com). Many core concepts of Cloesce come directly from Coalesce (Cloesce = Cloudflare + Coalesce).# LLMs Interact with this documentation with an LLM by utilizing the [llms-full.txt found here](https://cloesce.pages.dev/llms-full.txt). Download from the terminal using `curl`: curl https://cloesce.pages.dev/llms-full.txt -o llms-full.txt # TypeDoc An API reference generated using [TypeDoc](https://typedoc.org/) for the Cloesce TypeScript library can be found [here](https://cloesce-ts.pages.dev/).# Getting Started > \[!TIP\] Cloesce runs entirely on Cloudflare Workers, so familiarity > with Workers and Wrangler is recommended. If you are new to Workers, > check out the [Cloudflare Workers > documentation](https://developers.cloudflare.com/workers/). Welcome to the Getting Started guide. This document will help you set up a basic Cloesce project using the `create-cloesce` template. This guide covers: - Installing Cloesce - A basic project structure - Building, running, and deploying your Cloesce application # Installation > \[!NOTE\] Cloesce supports only TypeScript to TypeScript compilation > as of Alpha v0.2.0. Support for additional languages will be added in > future releases. The simplest way to get a Cloesce project up and running is to use the `create-cloesce` template. This template sets up a basic Cloesce project structure with all the necessary dependencies, configurations, example Models, and example tests to help get you started quickly. The template includes a sample HTML frontend with Vite which should be replaced with your frontend of choice. ## Prerequisites 1. Sign up for a [Cloudflare account](https://dash.cloudflare.com/sign-up/workers-and-pages) (*not necessary for local development*) 2. Install [Node.js](https://nodejs.org/) (version `16.17.0` or later) ## create-cloesce To create a new Cloesce project using the `create-cloesce` template, run the following command in your terminal: ``` bash npx create-cloesce my-cloesce-app ``` After running this command, navigate into your new project directory: ``` bash cd my-cloesce-app ``` A simple project structure is created for you. ├── src/ │ ├── data/ # Example Cloesce Models │ └── web/ # Frontend web assets ├── test/ # Unit tests for example Models ├── migrations/ # Database migration files ├── cloesce.config.ts # Cloesce configuration └── package.json # Project dependencies and scripts ```# Exploring the Template After creating your project with `create-cloesce`, several example files are included to help you get started. Below is an overview of those files and their purpose. ## Wrangler Environment All Cloudflare Workers define [a set of bindings](https://developers.cloudflare.com/workers/configuration/environment-variables/) that provision resources such as [D1 databases](https://developers.cloudflare.com/d1/), [R2 buckets](https://developers.cloudflare.com/r2/), [KV namespaces](https://developers.cloudflare.com/kv/concepts/kv-namespaces/), and miscellaneous environment variables. Cloesce uses a class decorated with `@WranglerEnv` to define the Wrangler Environment for your application, tailored to the resources you need. In `src/data/main.ts`, a basic Wrangler Environment has been defined. ```typescript @WranglerEnv export class Env { db: D1Database; bucket: R2Bucket; myVariable: string; } The above implementation of `Env` defines a Wrangler environment with a D1 database binding named `db`, an R2 bucket named `bucket`, and a string environment variable named `myVariable`. A typical Cloudflare Worker defines these bindings in a `wrangler.jsonc` file, but Cloesce generates this file for you during compilation based on the `@WranglerEnv` class. Read more about the Wrangler Environment in the [Wrangler Environment](./ch2-7-wrangler-environment.md) chapter. ## Custom Main Function Cloudflare Workers are serverless functions that run at Cloudflare's edge and respond to HTTP requests. Each Worker defines an entry point function through which all requests are routed. Cloesce allows this same functionality through a custom `main` definition (seen in `src/data/main.ts`) ``` typescript export default async function main( request: Request, env: Env, app: CloesceApp, ctx: ExecutionContext ): Promise {...} ``` Just like the standard Workers entrypoint, this function receives the inbound `Request`, the Wrangler Environment defined by the decorated `@WranglerEnv` class, and an `ExecutionContext` for managing background tasks. Additionally, it receives a `CloesceApp` instance that you can use to handle routing and Model operations. Read more about custom main functions in the [Middleware](./ch4-0-middleware.md) chapter. ## Example Models Models are the core building blocks of a Cloesce application. They define exactly how your data is structured, what relationships exist between different data entities, and what API endpoints will be generated to interact with that data. Unlike other ORMs, Cloesce Models are not limited to just relational data stored in a SQL database. Models can also include data stored in R2 buckets, KV namespaces, or inject external services. In `src/data/Models.ts` you will find two example Models, `Weather` and `WeatherReport`. ```{=html}
``` ```{=html} ``` Weather Code Snippet ```{=html} ``` ``` typescript @Model("db") export class Weather { id: Integer; weatherReportId: Integer; weatherReport: WeatherReport | undefined; dateTime: Date; location: string; temperature: number; condition: string; @R2("weather/photo/{id}", "bucket") photo: R2ObjectBody | undefined; @Post() async uploadPhoto(@Inject env: Env, stream: ReadableStream) { ... } @Get() downloadPhoto() { ... } } ``` ```{=html}
``` ```{=html}
``` ```{=html} ``` WeatherReport Code Snippet ```{=html} ``` ``` typescript @Crud("GET", "LIST", "SAVE") @Model("db") export class WeatherReport { id: Integer; title: string; description: string; weatherEntries: Weather[]; } ``` ```{=html}
``` The `Weather` Model consists of: Feature Type / Description Source / Layer ----------------- -------------------------------------------- ---------------- `id` Primary Key D1 `weatherReport` One-to-One relationship D1 `dateTime` Scalar column D1 `temperature` Scalar column D1 `condition` Scalar column D1 `photo` R2 object, key format `weather/photo/{id}` R2 `uploadPhoto` API endpoint Workers `downloadPhoto` API endpoint Workers The `WeatherReport` Model consists of: \| Feature \| Type / Description \| Source / Layer \| \|---------\|-----------------\|----------------\| \| `id` \| Primary Key \| D1 \| \| `title` \| Scalar column \| D1 \| \| `summary` \| Scalar column \| D1 \| \| `weatherEntries` \| One-to-Many relationship with `Weather` \| D1 \| \| `GET` \| Generated CRUD operation \| Workers \| \| `SAVE` \| Generated CRUD operation \| Workers \| \| `LIST` \| Generated CRUD operation \| Workers \| Read more about how Models work in the [Models](./ch2-0-models.md) chapter.# Building and Migrating Building a Cloesce project generally consists of three steps: 1. Compilation 2. Running database migrations 3. Building your frontend code ## Compiling In your project directory, run the following command to compile your Cloesce Models: ``` bash npx cloesce compile ``` This command looks for a `cloesce.config.ts` file in your project root, which contains configuration settings for Cloesce. If the file is not found, or settings are omitted, default values will be used. After compilation, a `.generated` folder is created in your project root. This should **not** be committed to source control, as it is regenerated on each build. The folder contains: - `cidl.json`: The Cloesce Interface Definition Language file, representing your data Models and their relationships. This file is used internally by Cloesce for generating client code, migrations, and running the Cloudflare Worker runtime. - `client.ts`: The generated client code for accessing your Models from the frontend. Import this file in your frontend code to interact with your Cloesce Models over HTTP. - `workers.ts`: The generated Cloudflare Worker code with all linked dependencies (including your custom `main` function if defined). This file is the entry point for your Cloudflare Worker and is referenced in the generated `wrangler.jsonc`. ## Generating Migrations To generate database migration files based on changes to your Cloesce Models, run the following command: ``` bash npx cloesce migrate # Or to generate a migration for all D1 bindings: npx cloesce migrate --all ``` This command compares your current Cloesce Models against the last applied migration and generates a new migration file in the `migrations/` folder with the specified ``. The migration file contains SQL statements to update your D1 database schema to match your Models. You must apply the generated migrations to your D1 database using the Wrangler CLI: ``` bash npx wrangler d1 migrations apply ``` ## Running After compiling and applying migrations, you can build and run your application locally using Wrangler: ``` bash npx wrangler dev --port ```# Deploying With your application built and your database migrated, you're ready to deploy your Cloesce application to Cloudflare Workers. Deployment is done through the Wrangler CLI. 1. **Modify `cloesce.config.ts`** Ensure your `cloesce.config.ts` file is correctly configured for production, including the production Worker URL. 2. **Configure Wrangler bindings** Open your `wrangler.jsonc` and set all required binding IDs (e.g., `kv_namespaces`, `d1_databases`, `r2_buckets`) to their production values. ```jsonc { "r2_buckets": [ { "binding": "bucket", "bucket_name": "xxxxxxxx" } ], } ``` 3. **Build your application** Run the compile command to generate the necessary files for deployment: ``` bash npx cloesce compile ``` 4. **Deploy using Wrangler** Publish your application to Cloudflare Workers: ``` bash npx wrangler deploy ``` 5. **Deploy your frontend** If you have a frontend application (e.g., built with Vite), build and deploy it to your preferred hosting service. For example, with [Cloudflare Pages](https://pages.cloudflare.com): ``` bash npx wrangler pages deploy ./dist ``` # Models Models are used to define the: - Source of data (D1, KV, R2) - Structure of data (properties and relationships) - REST operations that can be performed on the data (CRUD methods, custom methods) - Generated client API A single Cloesce Model encapsulates an entity that exists across the stack. Surprisingly, they're only a couple of lines of code! In this chapter, we will explore the various features of Cloesce Models, including defining D1, KV, and R2 backed properties, relationships between Models, generated CRUD methods, and custom methods.# Basic D1 Backed Model In this section, we will explore the basic properties of a D1 backed Model in Cloesce. [Cloudflare D1]((https://developers.cloudflare.com/d1/)) is a serverless SQL database built on SQLite for Workers. ## Defining a Model > \[!NOTE\] Models do not have constructors as they should not be > manually instantiated. Instead, use the [ORM > functions](./ch2-6-cloesce-orm.md) to create, retrieve, and update > Model instances. For tests, consider using > [`Object.assign()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign) > to create instances of Models with specific property values. > \[!TIP\] Using the `@PrimaryKey` decorator is optional if your primary > key property is named `id` or `Id` (in any casing, i.e., > snake case, camel case, etc). Cloesce will automatically treat a > property named `id` as the primary key. Compilation in Cloesce consists of three phases: Extraction, Analysis, and Code Generation. During Extraction, Cloesce scans your source files (designated with `*.cloesce.ts`) for Model definitions. Models are defined using the `@Model()` decorator. ``` typescript import { Model, Integer, PrimaryKey } from "cloesce/backend"; @Model("db") export class User { @PrimaryKey id: Integer; name: string; } ``` The above code defines a Model "User" stored in the D1 database `db`, with several properties: \| Property \| Description \| \|--------\|-------------\| \| `User` \| Cloesce infers from the class attributes that this Model is backed by a D1 table `User` \| \| `id` \| Integer property decorated with `@PrimaryKey`, indicating it is the Model's primary key. \| \| `name` \| String property representing the user's name; stored as a regular column in the D1 database. \| ## Supported D1 Column Types Cloesce supports a variety of column types for D1 Models. These are the supported TypeScript types and their corresponding SQLite types: TypeScript Type SQLite Type Notes ----------------- ------------- ------------------------------------ `Integer` `INTEGER` Represents an integer value `string` `TEXT` Represents a string value `boolean` `INTEGER` 0 for false, 1 for true `Date` `TEXT` Stored in ISO 8601 format `number` `REAL` Represents a floating-point number `Uint8Array` `BLOB` Represents binary data All of these types by themselves are `NOT NULL` by default. To make a property nullable, you can use a union with `null`, e.g., `property: string | null;`. `undefined` is reserved for [Navigation Properties](./ch2-2-navigation-properties.md) and cannot be used to indicate nullability. Notably, an `Integer` primary key is automatically set to `AUTOINCREMENT` in D1, so you don't need to manually assign values to it when creating new records (useful for the [ORM functions](./ch2-6-cloesce-orm.md)). ## Fluent API Some column configurations cannot be cleanly expressed through TypeScript decorators alone. For these cases, Cloesce provides a Fluent API that can called in `cloesce.config.ts` to further customize the D1 schema. For example, to make a column unique: ``` ts import { defineConfig } from "cloesce/config"; import { Weather } from "./src/data/models.cloesce"; const config = defineConfig({ // ... }); config.model(Weather, builder => { builder.unique("dateTime", "location"); }); ``` Additionally, Cloesce exposes a method to modify the AST after extraction: ``` ts config.rawAst((ast) => { // modify the raw AST here }); ``` ## Migrating the Database > \[!IMPORTANT\] Any change in a D1 backed Model definition (adding, > removing, or modifying properties; renaming Models) requires a new > migration to be created. > > The migration command will generate a new migration file in the > `migrations/` directory. The standard Cloesce compilation command does not perform database migrations. To create or update the D1 database schema based on your Model definitions, you need to run the migration command: ``` bash npx cloesce compile # load the latest Model definitions npx cloesce migrate ``` Finally, these generated migrations must be applied to the actual D1 database using the Wrangler CLI: ``` bash npx wrangler d1 migrations apply ```# Navigation Properties In the previous section, we built a basic D1 backed Model with scalar properties. However, relational databases like Cloudflare D1 often involve more complex relationships between tables. In this section, we will explore *Navigation Properties* which allow us to define relationships between different Models. ## Foreign Keys > [!NOTE] > A Model can only have a foreign key to another Model if it is: > 1. D1 backed > 2. Part of the same database as the Model it references (lifted in future releases!) Before diving into Navigation Properties, it's essential to understand their source: foreign keys. Foreign keys are scalar properties that reference the Primary Key of another Model, establishing a relationship between two Models. Foreign keys directly translate to SQLite `FOREIGN KEY` constraints in the underlying D1 database. For example, let's say we want to create a relationship between `Person` and `Dog`, where a Person can have one Dog. ```typescript import { Model, Integer, ForeignKey } from "cloesce/backend"; @Model("db") export class Dog { id: Integer; } @Model("db") export class Person { id: Integer; @ForeignKey(d => d.id) dogId: Integer; } ``` The `Person` Model has a foreign key property `dogId`, which references the primary key of the `Dog` Model. This establishes a relationship where each person can be associated with one dog. > \[!NOTE\] Cloesce does not allow circular foreign key relationships > (and neither does SQLite!). > > If you need to model such a relationship, consider marking a foreign > key as nullable and managing the relationship at the application > level. ## Navigation Properties Inspired by [Entity Framework relationship navigations](https://learn.microsoft.com/en-us/ef/core/Modeling/relationships/navigations), Cloesce allows you to effortlessly define One to One, One to Many, and Many to Many relationships between your Models using Navigation Properties. All navigation properties are D1 backed Models themselves, or arrays of D1 backed Models. Let's revisit our `Person` and `Dog` Models and add navigation properties to them: ``` typescript import { Model, Integer } from "cloesce/backend"; @Model("db") export class Dog { id: Integer; } @Model("db") export class Person { id: Integer; dogId: Integer; dog: Dog | undefined; } ``` In this example, Cloesce infers that `dog` is a navigation property to `Dog`, with `dogId` as the foreign key. This allows us to access the associated `Dog` instance directly from a `Person` instance. Cloesce has a simple inference engine that finds navigation properties, then searches for a property in the Model with the name `` (in any casing) to use as the foreign key. This relationship can be explicitly expressed using the Fluent API in `cloesce.config.ts`: ``` typescript config.model(Person, builder => { builder // Property "dogId" is a foreign key referencing the model Dog, // using Dog's primary key "id" .foreignKey("dogId") .references(Dog, "id") // Property "dog" is one to one referencing the model Dog, // using the foreign key "dogId" .oneToOne("dog") .references(Dog, "dogId"); }); ``` ```{=html} ``` ## One to Many Let's modify our Models to allow a Person to have multiple Dogs: ``` typescript import { Model, Integer } from "cloesce/backend"; @Model("db") export class Dog { id: Integer; ownerId: Integer; owner: Person | undefined; } @Model("db") export class Person { id: Integer; dogs: Dog[]; } ``` In this example, we added a foreign key `ownerId` to the `Dog` Model, referencing the `Person` Model. The `Person` Model now has a navigation property `dogs`, which is an array of `Dog` instances, representing all dogs owned by that person. Cloesce can infer this relationship by finding the first property in `Dog` that references `Person` as a foreign key, and using that as the basis for the one to many relationship. If many properties reference `Person`, it will need to be explicitly stated in the Fluent API: ``` typescript config.model(Person, builder => { builder .oneToMany("dogs") .references(Dog, "ownerId"); .oneToMany("otherDogs") .references(Dog, "otherOwnerId"); }); ``` ## Many to Many Many to Many relationships have an intermediate junction table that holds foreign keys to both related Models. ``` typescript import { Model, Integer } from "cloesce/backend"; @Model("db") export class Student { id: Integer; courses: Course[]; } @Model("db") export class Course { id: Integer; students: Student[]; } ``` An underlying junction table will be automatically created by Cloesce during migration: ``` sql CREATE TABLE IF NOT EXISTS "CourseStudent" ( "left" integer NOT NULL, "right" integer NOT NULL, PRIMARY KEY ("left", "right"), FOREIGN KEY ("left") REFERENCES "Course" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, FOREIGN KEY ("right") REFERENCES "Student" ("id") ON DELETE RESTRICT ON UPDATE CASCADE ); ``` > \[!NOTE\] The left column lists the Model name that comes first > alphabetically; the right column lists the one that comes after. ## Composite Keys A Model can have a composite primary key by using the `@PrimaryKey` decorator on multiple properties. A primary key may also be a foreign key. ``` typescript import { Model, Integer, PrimaryKey } from "cloesce/backend"; @Model("db") class Enrollment { @PrimaryKey @ForeignKey(s => s.id) studentId: Integer; @PrimaryKey courseId: Integer; student: Student | undefined; course: Course | undefined; } @Model("db") class Student { id: Integer; enrollments: Enrollment[]; } ``` Here, Cloesce is able to infer that Student has many Enrollments through `enrollments` because the Enrollment Model has a foreign key to Student. ``` typescript import { Model, Integer, PrimaryKey } from "cloesce/backend"; @Model("db") class Person { @PrimaryKey firstName: string; @PrimaryKey lastName: string; dog: Dog | undefined; } @Model("db") class Dog { id: Integer; ownerFirstName: string; ownerLastName: string; owner: Person | undefined; } ``` Because a navigation property `owner` is defined on `Dog`, Cloesce can infer that `ownerFirstName` and `ownerLastName` together form a composite foreign key to `Person`. Without a navigation property, Cloesce Models can only decorate a single foreign key, so the Fluent API must be used to explicitly define the composite foreign key: ``` typescript config.model(Dog, builder => { builder .foreignKey("ownerFirstName", "ownerLastName") .references(Person, "firstName", "lastName"); }); ```# Data Sources If you fetch a Model you may notice that Cloesce will leave `undefined` or empty arrays in deeply nested composition with other Models. This is intentional, and is handled by Data Sources. ## What are Data Sources? > [!IMPORTANT] > All scalar properties (e.g., `string`, `number`, `boolean`, etc.) are always included in query results. Include Trees are only necessary for Navigation Properties. Data Sources are Cloesce's response to the overfetching and recursive relationship challenges when modeling relational databases with object-oriented paradigms. For example, in the Model definition below, how should Cloesce know how deep to go when fetching a Person and their associated Dog? ```typescript import { Model, Integer } from "cloesce/backend"; @Model("db") export class Dog { id: Integer; ownerId: Integer; owner: Person | undefined; } @Model("db") export class Person { id: Integer; dogs: Dog[]; } // => { id: 1, dogs: [ { id: 1, owner: { id: 1, dogs: [ ... ] } } ] } ad infinitum ``` If we were to follow this structure naively, fetching a `Person` would lead to fetching their `Dog`, which would lead to fetching the same `Person` again, and so on, resulting in an infinite loop of data retrieval. Data Sources, through their `includeTree` configuration, allow developers to explicitly state which related Navigation Properties should be included in the query results, preventing overfetching. If a Navigation Property is not included in the `includeTree`, it will remain `undefined` (for singular relationships) or an empty array (for collections). A common convention to follow when writing singular Navigation Properties is to define them as `Type | undefined`, indicating that they may not be populated unless explicitly included. ## Default Data Source Cloesce will create a default Data Source for each model called "default". This Data Source will include all KV, R2, and D1 properties, but will avoid both circular references and nested relationships in arrays. For example, in the `Person` and `Dog` Models above, the default Data Source for `Person` would be: ``` typescript { includeTree: { dogs: { // No more includes, preventing infinite recursion } } } ``` The default Data Source for `Dog` would be: ``` typescript { includeTree: { owner: { dogs: { // No more includes, preventing infinite recursion } } } } ``` The default Data Source can be generated on demand using the Cloesce ORM (see [Cloesce ORM chapter](./ch2-6-cloesce-orm.md) for more details), or it can be overridden with a custom Data Source definition (see next section). ## Custom Data Sources In addition to the default Data Source, you can define custom Data Sources on your Models, or even override the default Data Source. Each Data Source you define on a Model will be accessible by the client for querying that Model, and you can have as many Data Sources as you want. ``` typescript import { Model, Integer, DataSource } from "cloesce/backend"; @Model("db") export class Dog { id: Integer; ownerId: Integer; owner: Person | undefined; } @Model("db") export class Person { id: Integer; dogs: Dog[]; static readonly withDogsOwnersDogs: DataSource = { includeTree: { dogs: { owner: { dogs: { // ... could keep going! } } } } }; static readonly default: DataSource = { includeTree: {} }; } ``` In this example, we defined a custom Data Source called `withDogsOwnersDogs` on the `Person` Model. This Data Source specifies that when fetching a `Person`, we want to include their `dogs`, and for each `Dog`, we want to include their `owner`, and for each `owner`, we want to include their `dogs` again. This allows for a much deeper fetch than the default Data Source, but it is still explicitly defined to prevent infinite recursion. We also overrode the default Data Source for `Person` to be an empty include tree, meaning that by default, fetching a `Person` will not include any related Navigation Properties unless some other Data Source is specified in the query. ## Custom Data Source Queries On top of creating the structure of hydrated data, Data Sources are also responsible for the underlying SQL queries to fetch that data. Each Data Source comes with two default implementations for the methods: `get` and `list`. `get` is responsible for fetching a single instance of the Model, while `list` is responsible for fetching multiple instances. `get` can take only the primary key(s) as arguments, while `list` can take `lastSeen`, `limit` and `offset` arguments for pagination. Each method accepts an argument `joined` which generates `SELECT * FROM ... JOIN ...` query based off the `includeTree` structure of the Data Source. ``` typescript // Cloesce will generate a data source like this by default. const customDs: DataSource = { includeTree: { dogs: {} }, // NOTE: This is equivalent to the default `get` implementation get: (joined) => ` WITH joined AS (${joined()}) SELECT * FROM joined WHERE id = ? `, // NOTE: This is equivalent to the default `list` implementation list: (joined) => ` WITH joined AS (${joined()}) SELECT * FROM joined WHERE id > ? ORDER BY id LIMIT ? `, // Array of parameters available for the list method. Also defines // the order of those parameters. `lastSeen` can be multiple primary keys // for composite key models, defined in the same order as the primary keys. listParams: ["LastSeen", "Limit"] } @Model("db") export class Person { id: Integer; dogs: Dog[]; static readonly default: DataSource = customDs; } ``` See the [Cloesce ORM chapter](./ch2-6-cloesce-orm.md) and [Model Methods chapter](./ch2-5-model-methods.md) for more details on how to use custom Data Sources in queries.# KV and R2 Properties D1 is a powerful relational database solution, but sometimes developers need to work with other types of storage for specific use cases. Cloesce supports integrating [Cloudflare KV](https://developers.cloudflare.com/kv/) and [Cloudflare R2](https://developers.cloudflare.com/r2/) storage directly into your Models, allowing you to leverage these storage solutions alongside D1 databases. ## Defining a Model with KV > \[!IMPORTANT\] `unknown` is a special type to Cloesce, designating > that no validation should be performed on the data, but it is still > stored and retrieved as JSON. > \[!IMPORTANT\] KV Models do not yet support cache control directives > and expiration times. This feature is planned for a future release. > \[!NOTE\] KV properties on a Model consider a missing key as a valid > state, and will not return 404 errors. Instead, the value inside of > the `KValue` will be set to `null`. [Cloudflare KV](https://developers.cloudflare.com/kv/) is a globally distributed key-value storage system. Along with a key and value, KV entries can also have associated metadata. Cloesce respects the design constraints of KV storage. For Models backed purely by KV or R2, the following are not supported: - Relationships\ - Navigation properties\ - Migrations ``` typescript import { Model, KV, KValue, KeyParam } from "cloesce/backend"; @Model() // no database specified export class Settings { @KeyParam settingsId: string; @KV("settings/{settingsId}", "myNamespace") data: KValue | undefined; @KV("settings/", "myNamespace") allSettings: KValue[]; } ``` The above Model uses only KV attributes. The `@KeyParam` decorator indicates that the `settingsId` property is used to construct the KV key for the `data` property, using string interpolation. The `@KV` decorator specifies the key pattern and the KV namespace to use. The `data` property is of type `KValue`, which represents a value stored in KV. You can replace `unknown` with any serializable type, but Cloesce will not validate or instantiate the data when fetching it. [Data Sources](./ch2-3-data-sources.md) can be used with KV Models as well to specify which properties to include when fetching data. ## Defining a Model with R2 > \[!NOTE\] R2 properties on a Model consider a missing object as a > valid state, and will not return 404 errors. Instead, the property > will be set to `undefined`. [Cloudflare R2](https://developers.cloudflare.com/r2/) is an object storage solution similar to [Amazon S3](https://aws.amazon.com/pm/serv-s3/). It allows you to store and retrieve large binary objects. Just like in KV Models, Cloesce does not support relationships, Navigation Properties, or migrations for purely R2 backed Models. Since R2 is used for storing large objects, the actual data of an R2 object is not fetched automatically when accessing an R2 property to avoid hitting [Worker memory limits](https://developers.cloudflare.com/workers/platform/limits/). Instead, only the metadata of the [`R2Object`](https://developers.cloudflare.com/r2/api/workers/workers-api-reference/#r2object-definition) is retrieved. To fetch the full object data, you can use Model Methods as described in the chapter [Model Methods](./ch2-5-Model-methods.md). ``` typescript import { Model, R2, R2Object, KeyParam, IncludeTree } from "cloesce/backend"; @Model() // no database specified export class MediaFile { @KeyParam fileName: string; @R2("media/{fileName}.png", "myBucket") file: R2Object | undefined; } ``` The `MediaFile` Model above is purely R2 backed. The `@KeyParam` decorator indicates that the `fileName` property is used to construct the R2 object key for the `file` property. The `@R2` decorator specifies the key pattern and the R2 bucket to use. The `file` property is of type `R2Object`, which represents an object stored in R2. This type provides access to metadata about the object, such as its size and content type. [Data Sources](./ch2-3-data-sources.md) can also be used with R2 Models to specify which properties to include when fetching data. ## Mixing Data Together Cloesce allows you to combine D1, KV, and R2 properties into a single Model. This provides flexibility in how you structure your data and choose the appropriate storage mechanism for each property. ``` typescript import { Model, Integer, KV, KValue, R2, R2Object, KeyParam, DataSource } from "cloesce/backend"; @Model("db") export class DataCentaur { id: Integer; @R2("centaurPhotos/{id}.jpg", "myBucket") photo: R2Object; } @Model("db") export class DataChimera { id: Integer; favoriteSettingsId: string; dataCentaurId: Integer; dataCentaur: DataCentaur | undefined; @KV("settings/{favoriteSettingsId}", "myNamespace") settings: KValue; @R2("media/{id}.png", "myBucket") mediaFile: R2Object | undefined; } ``` In the `DataChimera` Model above, we have a mix of D1, KV, and R2 properties. The `id` property is stored in a D1 database, while the `settings` property is stored in KV and the `mediaFile` property is stored in R2. Mixing these storage mechanisms introduces some caveats. Whenever D1 is used in a Model, it is treated as the source of truth for that Model. This means that if the primary key does not exist in D1, the entire Model is considered non-existent, even if KV or R2 entries exist for that key. However, if a primary key exists and the KV and R2 entries do not, Cloesce considers this a valid state and will place `null` or `undefined` in those properties respectively. Furthermore, using `KeyParam`s in a Model with D1 limits the capabilities of the ORM, discussed [later in this chapter](./ch2-6-cloesce-orm.md). It is recommended to avoid using `KeyParam`s in Models that also use D1 Navigation Properties.# Model Methods Models do not define just the source, structure, and infrastructure of your data, but also the API endpoints to operate on that data. In this section, we will explore how to define methods on Models that are exposed as API endpoints, CRUD methods generated by Cloesce, and the runtime validation that occurs when these methods are invoked. ## Static and Instance Methods > \[!WARNING\] GET methods in alpha v0.2.0 do not support complex types > as parameters (such as other Models, arrays, etc). Only primitive > types like `string`, `number`, `boolean` are supported. This > limitation will be lifted in future releases. A Model class in Cloesce may have both static and instance methods. A static method simply exists on the same namespace as the Model class. An instance method exists on an actual instance of the Model. Methods must be decorated with an HTTP Verb such as `@Get`, `@Post`, etc. to be exposed as API endpoints. Each endpoint may take a [Data Source](./ch2-3-data-sources.md) as an optional query parameter to specify which related data to include in the response. It may also define a Data Source inline if it should not be shared with the client. ``` typescript import { Model, Integer, HttpResult } from "cloesce/backend"; @Model("db") export class User { id: Integer; name: string; @Get() static echo(input: string): HttpResult { if (isBadWord(input)) { return HttpResult.fail(400, "I'm not saying that!"); } return HttpResult.ok(`Echo: ${input}`); } @Get() greet(): string { return `Hello, my name is ${this.name}.`; } foo() { // Not exposed as an API method } } ``` After compilation via `npx cloesce compile`, the above Model will have two API endpoints: - `GET /User/echo?input=yourInput` Calls the static `echo` method. - `GET /User/{id}/greet` Calls the instance `greet` method on the `User` instance with the specified `id`. To query these endpoints, a full generated client that matches the exact structure defined in the Model is available after compilation in `.generated/client.ts`. ## CRUD Methods > \[!IMPORTANT\] Delete is not yet supported as a generated CRUD method > in alpha v0.2.0, but it is planned for a future release. > \[!NOTE\] R2 does not support CRUD methods for streaming the object > body, instead it only sends the metadata. When creating Models, you will find yourself writing the same CRUD (Create, Read, Update, Delete) boilerplate. To save this effort, Cloesce automatically generates standard CRUD methods if included in the Model decorator. These methods are exposed as API endpoints. Internally, they simply run the [Cloesce ORM](./ch2-6-cloesce-orm.md) operations available via the `Orm` class. ``` typescript import { Model, Integer, Crud } from "cloesce/backend"; @Crud("GET", "SAVE", "LIST") @Model("db") export class User { id: Integer; name: string; } ``` The above `User` Model will have the following API endpoints generated automatically: - `GET /User/{id}/GET?__dataSource=` Fetch a `User` by its primary key. - `POST /User/SAVE?__dataSource=` Create or update a `User`. The `User` data is passed in the request body as JSON. - `GET /User/LIST?__dataSource=` List all `User` instances. All CRUD methods take an optional `DataSource` in the request to specify which navigation properties to include in the response. This defaults to the default Data Source. ## Private Data Sources A Model may be composed of several other Models, KV entries and R2 objects, but one method may only need to fetch a subset of that data. In this case, you can define a Data Source for that specific method: ``` typescript @Model("db") export class User { id: Integer; name: string; @R2("profilePictures/{id}.png", "myBucket") profilePicture: R2Object | undefined; @Get({ includeTree: {} }) // can also define as a `const` outside the method and reuse it if needed getComputedField(): string { return `User: ${this.name}`; } } ``` ## Runtime Validation When a Model method is invoked via an API call, the Cloesce runtime automatically performs validation on the input parameters and the return value based on what it has extracted from the Model definition during compilation. This ensures that the data being passed to and from the method adheres to the expected types and constraints defined in the Model. There are many valid types for method parameters in Cloesce, such as: ----------------------------------------------------------------------- Type Description ---------------------- ------------------------------------------------ `string` String values `number` Floating-point numbers `Integer` Integer values `boolean` Boolean values (true/false) `Date` Date and time values `Uint8Array` Binary data `DataSourceOf` Any data source for Model type `T` `unknown` JSON data of unknown structure `DeepPartial` Partial version of Model type `T` where anything can be missing Plain Old Objects Objects with properties of supported types Model types Custom Models (e.g., `User`, `Post`) Arrays Arrays of any supported type (e.g., `string[]`, `User[]`) Nullable unions Nullable versions of any type (e.g., `string \| null`, `User \| null`) `HttpResult` HTTP result wrapping any supported type `T` `ReadableStream` Stream of data ----------------------------------------------------------------------- ## Plain Old Objects > \[!NOTE\] Plain old objects can only consist of serializable > properties supported by Cloesce. They must be exported so that they > can be linked. They cannot contain streams. Cloesce supports the use of Plain Old Objects (POOs) as method parameters and return types. A POO is an object with properties that are of supported types. This allows developers to return or accept complex data structures without needing to define a full Model for them. ``` typescript import { Model, Integer, HttpResult } from "cloesce/backend"; export class Profile { bio: string; age: Integer; interests: string[]; } @Model("db") export class User { id: Integer; name: string; @Post() updateProfile(profile: Profile): HttpResult { // Update the user's profile with the provided data return HttpResult.ok("Profile updated successfully."); } } ``` ## HttpResult Every method response in Cloesce is converted to an `HttpResult` internally. This allows methods to have fine-grained control over the HTTP response, including status codes and headers. ## DeepPartial Cloesce provides a special utility type called `DeepPartial`, which allows for the creation of objects where all properties of type `T` are optional, and this optionality is applied recursively to nested objects. This is particularly useful for update operations where you may only want to provide a subset of the properties of a Model. ## Stream Input and Output Cloesce supports streaming data both as input parameters and return values in Model methods. This is particularly useful for handling large files or data streams without loading everything into memory at once. Streams can be hinted to Cloesce using the `ReadableStream` type. If a method parameter is of type `ReadableStream`, no other validation is performed on the input data. Additionally, no other parameters are allowed in the method signature when using a stream input (aside from injected dependencies, which are discussed later). When a method returns a `ReadableStream`, Cloesce will return a plain `Response` on the client side, allowing for efficient streaming of data back to the client. Cloesce allows a `HttpResult` to be returned as well, which provides the ability to set custom status codes and headers while still streaming data. For example, to return a R2 object's body as a stream, you can define a method like this: ``` typescript import { Model, Integer, R2Object, Get, HttpResult } from "cloesce/backend"; @Model("db") export class MediaFile { id: Integer; @R2("media/{id}.png", "myBucket") r2Object: R2Object | undefined; @Get() downloadFile(): HttpResult { if (!this.r2Object) { return HttpResult.fail(404, "File not found."); } // Body is never loaded into memory, just streamed! return HttpResult.ok(this.r2Object.body); } } ``` # Cloesce ORM > \[!CAUTION\] The ORM is subject to change as new features are added. During the hydration step of the Cloesce runtime, all of a Model's data is fetched from its various defined sources (D1, KV, R2) and combined into a single object instance. This unified object can then be used seamlessly within your application code. This functionality is exposed through the `Orm` class in the `cloesce/backend` package. ## Data Sources A `DataSource` describes how a Model should be fetched and hydrated. It pairs an optional `IncludeTree` (which relationships to join) with optional custom SQL for `get` and `list` queries. ``` typescript interface DataSource { includeTree?: IncludeTree; get?: (joined: (from?: string) => string) => string; list?: (joined: (from?: string) => string) => string; listParams?: ("LastSeen" | "Limit" | "Offset")[]; } ``` - `includeTree` --- which relationships to include (KV, R2, 1:1, 1:M, M:M). - `get` --- custom SQL for `orm.get`. Receives a helper that generates the joined SELECT. Primary key columns are always bound in order via `?`. - `list` --- custom SQL for `orm.list`. Receives the same helper. Bind parameters are declared in `listParams`. - `listParams` --- which parameters to bind when executing the custom `list` query. Defaults to empty. All ORM methods that accept an include accept either a `DataSource` or a plain `IncludeTree` interchangeably. ### Default Data Source Cloesce generates a default `DataSource` for every Model at compile time. It includes all near relationships (KV, R2, 1:1) and the shallow side of 1:M and M:M relationships. This is used whenever no explicit Data Source is provided to an ORM method or instance method. You can access it at runtime with: ``` typescript const defaultDs = Orm.defaultDataSource(User); ``` Defining a `static readonly default` property on your Model that is a `DataSource` with an `includeTree` overrides the compiler-generated default. ## Getting and Listing Models ``` typescript import { Orm } from "cloesce/backend"; import { User } from "@data" const orm = Orm.fromEnv(env); const user = await orm.get(User, { primaryKey: { id: 1 }, keyParams: { myParam: "value" }, include: User.withFriends }); // => User | null const users = await orm.list(User, { include: User.withFriends }); // => User[] ``` `get` requires the primary key via `primaryKey`. For composite primary keys, supply all key columns: `primaryKey: { professorId: 1, courseId: 2 }`. Any `keyParams` needed to construct KV or R2 keys are passed alongside it. Returns `null` when no matching row is found. `list` takes an optional args object and cannot be used with Models that require key parameters for KV or R2 properties. Use prefix queries for those instead. ### Pagination `orm.list` uses seek-based pagination by default. Pass `lastSeen`, `limit`, and `offset` to page through results: ``` typescript const page1 = await orm.list(User, { limit: 50 }); const page2 = await orm.list(User, { lastSeen: { id: page1[page1.length - 1].id }, limit: 50 }); ``` The default query is `WHERE (primaryKey) > (lastSeen) ORDER BY primaryKey LIMIT ?`, which stays consistent under concurrent inserts. For `LIMIT`/`OFFSET` pagination or custom ordering, provide a `list` function on a custom Data Source. ### Paginated KV and R2 Fields KV and R2 list fields are declared with `Paginated`: ``` typescript @Model("db") class User { id: Integer; @KV("settings/{id}", namespace) settings: Paginated>; @R2("files/{id}", bucket) files: Paginated; } ``` ``` typescript interface Paginated { results: T[]; // first page, up to 1,000 entries cursor: string | null; complete: boolean; } ``` To retrieve the next page, use the `cursor` from the previous result with a custom method on your Model. ## Select, Map and Hydrate When you need filtering, ordering, or aggregation beyond what `get` and `list` provide, write the SQL directly. The ORM gives you three methods to bridge raw SQL results back to hydrated Model instances. `Orm.select` generates the appropriate `SELECT` with `LEFT JOIN`s and column aliases for a given Data Source. For example, given: ``` typescript @Model() export class Boss { id: Integer; persons: Person[]; static readonly withAll: DataSource = { includeTree: { persons: { dogs: {}, cats: {} } } }; } ``` `Orm.select(Boss, { include: Boss.withAll })` produces: ``` sql SELECT "Boss"."id" AS "id", "Person_1"."id" AS "persons.id", "Person_1"."bossId" AS "persons.bossId", "Dog_2"."id" AS "persons.dogs.id", "Dog_2"."personId" AS "persons.dogs.personId", "Cat_3"."id" AS "persons.cats.id", "Cat_3"."personId" AS "persons.cats.personId" FROM "Boss" LEFT JOIN "Person" AS "Person_1" ON "Boss"."id" = "Person_1"."bossId" LEFT JOIN "Dog" AS "Dog_2" ON "Person_1"."id" = "Dog_2"."personId" LEFT JOIN "Cat" AS "Cat_3" ON "Person_1"."id" = "Cat_3"."personId" ``` The aliased columns make it straightforward to filter on nested relationships via a CTE: ``` typescript const query = ` WITH BossCte AS ( ${Orm.select(Boss, { include: Boss.withAll })} ) SELECT * FROM BossCte WHERE [persons.dogs.id] = 5 AND [persons.cats.id] = 10 AND [persons.id] = 15 `; ``` An optional `from` string wraps a subquery as the base table: ``` typescript Orm.select(Boss, { from: "SELECT * FROM Boss WHERE name = 'Alice'", include: Boss.withAll }); ``` Pass the D1 results to `Orm.map` to reconstruct the object graph: ``` typescript const results = await d1.prepare(query).all(); const bosses = Orm.map(Boss, results, Boss.withAll); // => Boss[] ``` Then `orm.hydrate` fetches any KV and R2 properties and returns fully populated Model instances: ``` typescript const orm = Orm.fromEnv(env); const hydratedBosses = await orm.hydrate(Boss, { base: bosses, keyParams: {...}, include: Boss.withAll }); // => Boss[] ``` > \[!NOTE\] `Orm.map` requires results in the exact aliased format > produced by `Orm.select`. Mixing in results from other queries may > fail. ## Saving a Model `orm.upsert` handles both creating and updating a Model, including nested D1 and KV relationships. R2 properties are not supported; large binary data is better handled separately. ``` typescript import { Orm } from "cloesce/backend"; import { User } from "@data" const orm = Orm.fromEnv(env); const result = await orm.upsert(User, { // id: 1, omit to auto-increment name: "New User", friends: [ { name: "Friend 1" }, { id: 1, name: "My Best Friend" } // update existing ] }, User.withFriends); ``` The returned instance has all primary keys assigned and any navigation properties specified by the third argument (`DataSource` or `IncludeTree`) populated.# Wrangler Environment > \[!NOTE\] Unlike some Infrastructure as Code tools for Cloudflare, > Cloesce does not try to fully replace using a Wrangler configuration > file. Account specific configuration settings such as binding IDs must > still be managed manually. Cloesce will search your project for a class decorated with `@WranglerEnv` to define the [Cloudflare Workers environment](https://developers.cloudflare.com/workers/configuration/environment-variables/) tailored to your application. A `wrangler.jsonc` or `wrangler.toml` file is generated during compilation based on this class. Configure your preference in the `cloesce.config.ts` file: ``` typescript import { defineConfig } from "cloesce/config"; const config = defineConfig({ // ... wranglerConfigFormat: "jsonc", // or "toml" }); ``` Currently, only D1 databases, R2 buckets, KV namespaces, and string environment variables are supported. Cloesce will not overwrite an existing wrangler file or any unique configurations you may have added to it. It will append any missing bindings and variables defined in the `@WranglerEnv` class. An instance of the `WranglerEnv` is always available as a dependency to inject. See the [Services](./ch3-0-services.md) chapter for more information on dependency injection. An example `WranglerEnv` class is shown below: ``` typescript @WranglerEnv export class Env { db: D1Database; bucket: R2Bucket; kv: KVNamespace; someVariable: string; } ``` This will compile to the following `wrangler.jsonc` file: ``` jsonc { "compatibility_date": "2025-10-02", "main": ".generated/workers.ts", "name": "cloesce", "d1_databases": [ { "binding": "db", "database_id": "replace_with_db_id", "database_name": "replace_with_db_name", "migrations_dir": "./migrations/db" } ], "r2_buckets": [ { "binding": "bucket", "bucket_name": "replace-with-r2-bucket-name" } ], "kv_namespaces": [ { "binding": "kv", "namespace_id": "replace_with_kv_namespace_id" } ], "vars": { "someVariable": "default_string" } } ```# Services > [!TIP] > A clean design pattern for Cloesce is to use Services to encapsulate significant business logic and have Models act as > thin wrappers around data storage and retrieval. > > This separation of concerns can lead to more maintainable and testable code. Models are not the only way to write API logic in Cloesce. Services are another core concept that allows you to encapsulate business logic and share it across your application. Services are similar to Models in that they can define methods that can be called from your API routes, but they do not have any associated data storage or schema. Instead, Services are used to group related, complex functionality together and can be injected into other parts of your application using Cloesce's dependency injection system. ## Hello World Service Let's create a simple Service that returns a "Hello, World!" message. ```typescript import { Service, Get, HttpResult } from 'cloesce/backend'; @Service export class HelloWorldService { init(): HttpResult | undefined { // Optional initialization logic can go here } @Get() hello(): string { return "Hello, World!"; } } ``` After running `npx cloesce compile`, this Service will be available at the endpoint `HelloWorldService/hello`, and a client method will be generated for you to call it from the frontend. ## Dependency Injection To share dependencies across your Cloesce application methods, Cloesce utilizes dependency injection. By default, Cloesce provides two dependencies: - Wrangler Environment: Access to your Cloudflare Workers environment variables. - Request: The incoming HTTP request object. You can access these dependencies by decorating your method parameters with the `@Inject` decorator on any Cloesce Model or Service method: ``` typescript import { Service, Get, WranglerEnv } from 'cloesce/backend'; @WranglerEnv class Env { d1: D1Database; } @Service export class HelloWorldService { @Get() async hello(@Inject env: Env, @Inject request: Request): Promise { console.log("Request URL:", request.url); const res = await env.d1.prepare("SELECT 'Hello, World!' AS message").first<{ message: string }>(); return res.message; } } ``` Unlike Models, which require all attributes to be SQL columns, KV keys, or R2 objects, Services allow attributes to be any arbitrary value, searching for them in the dependency injection context. This means you can easily inject custom Services, utilities, or configurations into your Service methods as needed. ``` typescript @Service export class HelloWorldService { env: Env; request: Request; foo: string; init(): void { this.foo = "bar"; } @Get() async hello(): Promise { console.log("Request URL:", this.request.url); const res = await this.env.d1.prepare("SELECT 'Hello, World!' AS message").first<{ message: string }>(); return res.message + " and foo is " + this.foo; } } ``` ## Services as Dependencies Services can also be injected into other Services. They cannot be circularly dependent, but otherwise, you can freely compose Services together. ``` typescript @Service export class GreetingService { greet(name: string): string { return `Hello, ${name}!`; } } @Service export class HelloWorldService { greetingService: GreetingService; @Get() hello(name: string): string { return this.greetingService.greet(name); } } ``` # Middleware Middleware functions are events called between states of the Cloesce Router processing pipeline. They can be used to modify requests, exit early, or perform actions before and after operations. It is important to note that the Cloesce client expects results to come exactly as described in API methods. Therefore, middleware should not modify the structure of a response or change the expected output of an API method unless you are fully aware of the implications. ## Custom Main Entrypoint > \[!TIP\] The Cloesce Router will never throw an unhandled exception. > All errors are converted into `HttpResult` responses. > > Therefore, there is no need to wrap `app.run` in a try/catch block. > HTTP 500 errors are logged by default. The most basic form of middleware is a custom main entrypoint function, which will be called for every request to your Cloesce application. Cloesce will search your project for an exported `main` entrypoint. If none is found, a default main will be generated that simply initializes the Cloesce application. The main entrypoint allows you to intercept a request before it reaches the Cloesce Router, and handle the output of the Cloesce Router as you see fit. Below is an example of using the main entrypoint to attach CORS headers to every response: ``` typescript import { CloesceApp } from "cloesce/backend"; export default async function main( request: Request, env: Env, app: CloesceApp, _ctx: ExecutionContext): Promise { // preflight if (request.method === "OPTIONS") { return HttpResult.ok(200, undefined, { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS", "Access-Control-Allow-Headers": "Content-Type, Authorization", }).toResponse(); } // Run Cloesce router const result = await app.run(request, env); // attach CORS headers result.headers.set("Access-Control-Allow-Origin", "*"); result.headers.set( "Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS" ); result.headers.set( "Access-Control-Allow-Headers", "Content-Type, Authorization" ); return result; } ``` ## Middleware Hooks > \[!WARNING\] Middleware hooks are likely to change significantly > before a stable release. > \[!WARNING\] Middleware can only inject classes into the DI container > at this time. Injecting primitive values (strings, numbers, etc) is > not yet supported, but support is planned for a future release. > \[!TIP\] Many hooks can be registered. Hooks are called in the order > they are registered, per hook. Middleware hooks can be registered to run at specific points in the Cloesce Router processing pipeline. The available middleware hooks are: ----------------------------------------------------------------------- Hook Description ----------------------------------- ----------------------------------- `onRoute` Called when a request hits a valid route with the correct HTTP method. Service initialization occurs directly after this point, therefore services will not be available. `onNamespace` Called when a request hits a specific namespace (Model or Service). Occurs after service initialization but before request body validation. `onMethod` Called when a request is about to invoke a specific method. Occurs after request body validation but before hydration and method execution. ----------------------------------------------------------------------- Each hook has access to the dependency injection container for the current request, allowing you to modify it as needed. ``` typescript export class InjectedThing { value: string; } export default async function main( request: Request, env: Env, app: CloesceApp, _ctx: ExecutionContext ): Promise { app.onNamespace(Foo, (di) => { di.set(InjectedThing, { value: "hello world", }); }); app.onMethod(Foo, "blockedMethod", (_di) => { return HttpResult.fail(401, "Blocked method"); }); return await app.run(request, env); } ``` Middleware is capable of short-circuiting the request processing by returning an `HttpResult` directly. This is useful for implementing features like authentication. Middleware can also modify the dependency injection container for the current request, allowing you to inject custom services or data.# Testing Cloesce Models and Services live as isolated units with no inherent connection to an incoming Worker request, making them ideal for unit testing. To write tests for Cloesce that utilize any ORM features, ensure you have: - Ran `npx cloesce compile` to generate the necessary files - Applied migrations for the Models being tested - Invoked `CloesceApp.init` to initialize the Cloesce runtime ``` typescript import { CloesceApp } from "cloesce/backend"; import { cidl, constructorRegistry } from "@generated/workers"; // Cloesce must be initialized before utilizing any ORM features. // It takes the generated Cloesce Interface Definition Language (CIDL) // and the generated constructor registry. Both may be imported from // "@generated/workers" as shown above. beforeAll(() => CloesceApp.init(cidl as any, constructorRegistry, "http://localhost:8787")); ``` Cloesce needs only the CIDL (generated interface definition) and Constructor Registry (linked Model, Service and Plain Old Object exports) to be used in tests. This means you can write tests for your Models and Services without needing to run a full Cloudflare Worker environment. ORM methods rely on Cloudflare Workers bindings (D1, KV, R2, etc.), so you will need to mock these bindings in your test environment. The best choice for this is [Miniflare](https://developers.cloudflare.com/workers/testing/miniflare/), which provides an in memory implementation of Cloudflare Workers runtime and bindings. A basic Miniflare setup is included in the template project which can be installed with: `bash npx create-cloesce my-cloesce-app`\# Compiler Reference > \[!IMPORTANT\] As Cloesce continues to evolve, the compiler > architecture and set of features will change. This chapter will be > updated accordingly to reflect the latest design and implementation > details. This chapter provides a detailed reference for the Cloesce, including its design principles, architecture and key components.# Future Vision Cloesce is an ambitious project that aims to create a simple but powerful paradigm for building full stack applications. Several goals drive Cloesce's future development, shaping its design and evolution. Although still in its early stages, these goals provide a clear roadmap for its growth. ## Language Agnosticism A central tenant of Cloesce's vision is full language agnosticism. Modern web development forces teams to navigate a maze of parallel ecosystems, each with its own web servers, dependency injection patterns, ORMs, migration tools, validation libraries, and routing conventions. These systems all solve the same problems, yet they do so with incompatible syntax and assumptions. Cloesce aims to collapse this redundancy by defining a single architecture that can be compiled into any target language, allowing developers to choose their preferred runtime without sacrificing consistency. On the client side, this goal is well within reach: any language can consume REST APIs, making generated client code naturally portable. The server side, however, presents a deeper challenge. To move toward true language independence, Cloesce's core is implemented in Rust and compiled to WebAssembly, enabling the possibility of targeting multiple server environments in the future. The most significant obstacle today is the Cloudflare Workers platform, which treats only JavaScript and TypeScript as first-class citizens. Encouragingly, Cloudflare's ongoing effort to support Rust based Workers through WebAssembly is now in beta and shows strong potential. As this matures, Cloesce will be able to target Rust and possibly other compiled languages without compromising its architecture. Equally important is the evolution of WASI. With the upcoming WASI Preview 3 introducing a native async runtime, WebAssembly is rapidly becoming a viable foundation for general purpose server development. This progress directly expands the horizons of what Cloesce can support in the future, and also allows Cloesce to move the entirety of its runtime into WebAssembly, further decoupling it from any single language or platform. ## Support the full set of Cloudflare Workers features Cloesce should be the best way to develop with Cloudflare. To achieve this, developers cannot be limited in leveraging the capabilities of the Workers platform. Future versions of Cloesce will aim to enhance the full range of Workers features, including: - Durable Objects and Web Socket API generation - A native solution for D1 sharding - Worker to Worker communication - Hyperdrive + PostgreSQL support - ... and more as Cloudflare continues to expand the Workers platform. ## Designed for AI Integration With Cloesce, you write less code. Significantly less. Furthermore, the code you do write operates at a high level of abstraction, focusing on only data Models and business logic. This design makes Cloesce an ideal candidate for AI assisted development. Not only would creating a project require a fraction of the tokens, but the high level nature of the code means that AI can more easily understand the intent and structure of the application. By its stable release, simply asking an AI agent to "Create Twitter with Cloesce" should yield a complete, functional application ready to deploy to Cloudflare Workers in a record low token count.# Architecture Overview We can break down the Cloesce architecture into three components, each with their own subdomains: the Frontend, Generator and the Migrations Engine. ## Frontend The Frontend layer of Cloesce encompasses all components responsible for interfacing with the user's project and extracting high level language into the Cloesce Interface Definition Language (CIDL). It serves as both the entrypoint for compilation and the runtime environment for generated code. ### IDL Extraction A key design choice when building Cloesce was not to force users to write their Models in a separate IDL or DSL as seen in tools like gRPC and Prisma. Instead, we opted to have Cloesce utilize the source languages AST to extract Model definitions directly from the user's source code. This allows users to define their Models using familiar syntax and semantics, while still benefiting from the powerful features of Cloesce. Of course, this means we must write an extractor for each supported frontend language. Currently, the only supported language is TypeScript. The Extractor portion of Cloesce is responsible for scanning the user's source files (marked with `.cloesce.`) and identifying Model definitions through stub preprocessor directives. Extraction does not perform any semantic analysis; it simply extracts the Model definitions and their properties into an intermediate representation (the CIDL). The CIDL describes a full stack project. Every Model definition, Service, API endpoint and Wrangler binding is stored in the CIDL. Currently, this representation is serialized as JSON, but in the future we may explore other formats such as Protocol Buffers or FlatBuffers for better performance and extensibility. At the end of CIDL Extraction, a `cidl.pre.json` file is produced to be semantically validated by the Generator. ### Runtime Beyond extraction, the Frontend layer also includes the runtime environment for workers. Originally, we considered generating entirely standalone code for the Workers, but shifting to interpret the CIDL at runtime allowed us to greatly reduce the amount of generated code, add tests and improve maintainability. Each supported frontend language has its own runtime implementation that can interpret the CIDL using simple state machine logic. Moving as much of the runtime logic into WebAssembly as possible helps portability to other languages in the future. The runtime consists of two components: the Router and the ORM. The Router is currently written entirely in TypeScript, while the ORM compiles to WebAssembly from Rust. > \[!TIP\] Depending on the context, the CIDL may be referred to as the > "Abstract Syntax Tree", the "Cloesce Interface Definition Language", > or during the runtime just "metadata" when a particular node is being > referenced. All of these labels are accurate-- it's a versatile > structure! #### Router The Cloesce Router is responsible for handling incoming HTTP requests, matching them to an API endpoint defined in the CIDL, validating request parameters and body, hydrating data from the ORM and dispatching to a user defined method on a Model or Service. Along the way, the Router calls middleware functions defined in the CIDL. Although each middleware function can produce undefined behavior, each state in the Router is well defined and can produce only a corresponding failure state or success state. This makes reasoning about the Router's behavior straightforward. #### ORM The Cloesce ORM is responsible for fetching and updating data stored in SQL, KV and R2 according to the Model definitions and Include Trees passed. The majority of the ORM is written in Rust, however some portions are written in TypeScript such as KV and R2 hydration logic. ## Generator After being passed the `pre.cidl.json` file from the Frontend, the Generator performs semantic analysis on the CIDL and Wrangler configuration to ensure that the project is valid. This includes checking for translatable SQLite types, sorting Models and Services topologically, validating API endpoints and more. If any errors are found, the Generator will output them to the user and halt compilation. After semantic analysis is complete, the Generator produces the final `cidl.json` file which is then used to generate code for the worker runtime and client code generation. The generator will augment the CIDL with additional information like hashes for migrations and CRUD API endpoints for Models and Services. To make the Generator easily available to the frontend, it is written entirely in Rust and compiled to WebAssembly. This allows frontend languages to easily call into the Generator without needing to write language specific bindings. In the future, we may explore compiling native binaries for better performance, but WASM reduces the complexity of distributing multiple binaries for different platforms. ## Migrations Engine After a successful compilation, the Migrations Engine is used to generate database migrations from changes in the Model definitions, utilizing the Merkle-Tree hashes the Generator added to the CIDL. The engine can sometimes encounter problems it does not know the solution to, such as when a Model is renamed. In these cases, the engine will prompt the user with options on how to proceed (such as generating a rename migration or creating a new Model). This interactive process ensures that the generated migrations align with the user's intent. The Migrations Engine outputs SQLite files intended to be applied to D1 databases using the Wrangler CLI. It is written entirely in Rust and comes with the Generator as a single WebAssembly module for easy distribution.