Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 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.

import { Model, Integer, HttpResult } from "cloesce/backend";

@Model("db")
export class User {
    id: Integer;
    name: string;

    @Get()
    static echo(input: string): HttpResult<string> {
        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 operations available via the Orm class.

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:

@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:

TypeDescription
stringString values
numberFloating-point numbers
IntegerInteger values
booleanBoolean values (true/false)
DateDate and time values
Uint8ArrayBinary data
DataSourceOf<T>Any data source for Model type T
unknownJSON data of unknown structure
DeepPartial<T>Partial version of Model type T where anything can be missing
Plain Old ObjectsObjects with properties of supported types
Model typesCustom Models (e.g., User, Post)
ArraysArrays of any supported type (e.g., string[], User[])
Nullable unionsNullable versions of any type (e.g., string | null, User | null)
HttpResult<T>HTTP result wrapping any supported type T
ReadableStreamStream 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.

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<string> {
        // 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<T>, 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<ReadableStream> 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:

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<ReadableStream> {
        if (!this.r2Object) {
            return HttpResult.fail(404, "File not found.");
        }

        // Body is never loaded into memory, just streamed!
        return HttpResult.ok(this.r2Object.body);
    }
}