@fluentity/core
Version:
Fluentity is a fluent, model-oriented, typed HTTP client for TypeScript and framework agnostic.
589 lines (588 loc) • 19.9 kB
TypeScript
import { RelationBuilder, HasManyRelationBuilder, Constructor, AdapterResponse, Fluentity, MethodType, AdapterOptions, AdapterInterface } from './index';
import { QueryBuilder } from './QueryBuilder';
/**
* Base interface for model attributes that all models must implement.
* Provides the basic structure for model data and allows for dynamic properties.
* All model attributes must extend this interface to ensure type safety.
*
* @interface
* @example
* ```typescript
* interface UserAttributes extends Attributes {
* name: string;
* email: string;
* age?: number;
* // Dynamic properties are allowed
* [key: string]: any;
* }
* ```
*/
export interface Attributes {
/** Unique identifier for the model instance. Can be either a string or number. */
id?: string | number;
/** Index signature allowing for dynamic properties of any type */
[key: string]: any;
}
/**
* Base class for all models in the ORM.
* Provides core functionality for interacting with the API and managing model data.
* Handles CRUD operations, relationships, and query building.
*
* Features:
* - Automatic API request handling
* - Relationship management
* - Query building and filtering
* - Data validation and transformation
* - Dynamic property access
*
* @template T - The type of attributes this model will have, must extend Attributes
* @example
* ```typescript
* // Basic model definition
* class User extends Model<UserAttributes> {
* static resource = 'users';
* }
*
* // Model with relationships
* class Post extends Model<PostAttributes> {
* static resource = 'posts';
*
* @HasOne(() => User)
* author: User;
*
* @HasMany(() => Comment)
* comments: Comment[];
* }
* ```
*/
export declare class Model<T extends Attributes = Attributes, A extends AdapterOptions = AdapterOptions> {
#private;
/**
* Custom query scopes that can be applied to model queries.
* Each scope is a function that modifies the query builder behavior.
*/
static scopes?: Record<string, (query: RelationBuilder<any>) => RelationBuilder<any>>;
/**
* Unique identifier for the model instance.
* Can be either a string or number, depending on the API's ID format.
*/
id?: string | number;
/**
* Index signature for dynamic properties.
* Allows models to have additional properties beyond their defined attributes.
*/
[key: string]: any;
/**
* Resource endpoint for the model, used to construct API URLs.
* Must be set by subclasses to define the API endpoint.
* @example
* ```typescript
* static resource = 'users';
* ```
*/
static resource: string;
/**
* Creates a new model instance with the given attributes.
* Initializes the query builder and sets up the model's state.
* Can optionally accept an existing query builder instance.
*
* @param attributes - The attributes to initialize the model with
* @param queryBuilder - Optional query builder instance to use instead of creating a new one
* @returns A new model instance
* @throws {Error} If required attributes are missing
* @example
* ```typescript
* // Create with basic attributes
* const user = new User({ name: 'John', email: 'john@example.com' });
*
* // Create with existing query builder
* const query = new QueryBuilder().where({ status: 'active' });
* const user = new User({ name: 'John' }, query);
* ```
*/
constructor(attributes: T, parentQuery?: QueryBuilder<A>);
/**
* Gets the query builder instance for this model.
* Used internally for constructing API requests.
* Provides access to the current query state and parameters.
*
* @returns The query builder instance
* @protected
* @example
* ```typescript
* class CustomModel extends Model {
* async customQuery() {
* const query = this.queryBuilder
* .where({ status: 'active' })
* .limit(10);
* // Use query for custom operations
* }
* }
* ```
*/
get queryBuilder(): QueryBuilder<A>;
/**
* Gets the Fluentity instance for making API requests.
* Provides access to the singleton instance that manages API communication.
* Used internally for all API operations.
*
* @protected
* @returns The singleton Fluentity instance
* @throws {Error} If Fluentity has not been initialized
* @example
* ```typescript
* class CustomModel extends Model {
* async customOperation() {
* const fluentity = this.fluentity;
* // Use fluentity for custom API operations
* }
* }
* ```
*/
protected get fluentity(): Fluentity<AdapterInterface>;
/**
* Gets or creates a relation builder for the given model class.
* Uses an internal cache to avoid creating duplicate relation builders.
* Supports both has-one and has-many relationships.
*
* @param model - The model class to create a relation builder for
* @param relationBuilderFactory - The factory class to create the relation builder
* @returns A relation builder instance configured for the model
* @private
* @example
* ```typescript
* // Internal usage in relationship decorators
* const userBuilder = Model.getRelationBuilder(User, HasOneRelationBuilder);
* const postsBuilder = Model.getRelationBuilder(Post, HasManyRelationBuilder);
* ```
*/
private static getRelationBuilder;
/**
* Creates a new model instance with the given ID.
* Useful for creating model instances when only the ID is known.
* The instance can be used for operations that only require the ID.
*
* @param id - The ID to assign to the new model instance
* @returns A new model instance with the specified ID
* @example
* ```typescript
* // Create a reference to an existing user
* const user = User.id(123);
* await user.get(); // Fetch full user data
*
* // Use for relationship operations
* const post = await Post.create({
* title: 'New Post',
* author: User.id(123)
* });
* ```
*/
static id<T extends Model<Attributes>>(this: Constructor<T>, id: string | number): T;
/**
* Starts a new query builder for the model.
* Returns a HasManyRelationBuilder for querying multiple records.
* Provides a fluent interface for building complex queries.
*
* @returns A HasManyRelationBuilder instance for building queries
* @example
* ```typescript
* // Basic query with filters
* const users = await User.query()
* .where({ status: 'active' })
* .orderBy('created_at', 'desc')
* .all();
*
* // Complex query with multiple conditions
* const users = await User.query()
* .filter({
* age: { gt: 18 },
* role: ['admin', 'moderator']
* })
* .orderBy('name', 'asc')
* .limit(50)
* .offset(100)
* .all();
* ```
*/
static query<T extends Model<Attributes>>(this: Constructor<T>): HasManyRelationBuilder<T>;
/**
* Starts a query with a where clause.
* Shorthand for query().where() for common filtering operations.
* Useful for simple equality-based queries.
*
* @param where - Conditions to filter by, as field-value pairs
* @returns A HasManyRelationBuilder instance with where conditions applied
* @example
* ```typescript
* // Simple equality conditions
* const activeUsers = await User.where({ active: true }).all();
*
* // Multiple conditions
* const users = await User.where({
* role: 'admin',
* status: 'active',
* verified: true
* }).all();
* ```
*/
static where<T extends Model<Attributes>>(this: Constructor<T>, where: Partial<Attributes>): HasManyRelationBuilder<T>;
/**
* Starts a query with filter conditions.
* Similar to where() but specifically for filter operations.
* Supports more complex filtering operations and operators.
*
* @param filters - Filter conditions to apply, as field-value pairs
* @returns A HasManyRelationBuilder instance with filters applied
* @example
* ```typescript
* // Comparison operators
* const users = await User.filter({
* age: { gt: 18, lt: 65 },
* score: { gte: 1000 }
* }).all();
*
* // Array conditions
* const users = await User.filter({
* role: { in: ['admin', 'moderator'] },
* status: ['active', 'pending']
* }).all();
*
* // Nested conditions
* const users = await User.filter({
* profile: {
* verified: true,
* location: { ne: 'unknown' }
* }
* }).all();
* ```
*/
static filter<T extends Model<Attributes>>(this: Constructor<T>, filters: Record<string, any>): HasManyRelationBuilder<T>;
/**
* Retrieves all records for the model.
* Fetches all records from the API without any filtering.
* Use with caution for large datasets - consider using pagination.
*
* @returns Promise resolving to an array of model instances
* @example
* ```typescript
* // Get all users
* const allUsers = await User.all();
*
* // Get all users with error handling
* try {
* const users = await User.all();
* console.log(`Found ${users.length} users`);
* } catch (error) {
* console.error('Failed to fetch users:', error);
* }
* ```
*/
static all<T extends Model<Attributes>>(this: Constructor<T>): Promise<T[]>;
/**
* Finds a single record by ID.
* Fetches a specific record from the API by its ID.
* Throws an error if the record is not found.
*
* @param id - The ID of the record to find
* @returns Promise resolving to a model instance
* @throws {Error} If the record is not found
* @example
* ```typescript
* // Find a user by ID
* const user = await User.find(123);
*
* // Find with error handling
* try {
* const user = await User.find(123);
* console.log(`Found user: ${user.name}`);
* } catch (error) {
* if (error.status === 404) {
* console.log('User not found');
* } else {
* console.error('Error fetching user:', error);
* }
* }
* ```
*/
static find<T extends Model<Attributes>>(this: Constructor<T>, id: string | number): Promise<T>;
/**
* Creates a new record.
* Sends a POST request to create a new record in the API.
* Returns the created record with any server-generated fields.
*
* @param data - The data to create the record with
* @returns Promise resolving to the created model instance
* @throws {Error} If the creation fails
* @example
* ```typescript
* // Create a new user
* const user = await User.create({
* name: 'John Doe',
* email: 'john@example.com',
* role: 'user'
* });
*
* // Create with error handling
* try {
* const user = await User.create({
* name: 'John Doe',
* email: 'john@example.com'
* });
* console.log(`Created user with ID: ${user.id}`);
* } catch (error) {
* if (error.status === 422) {
* console.log('Validation failed:', error.errors);
* } else {
* console.error('Error creating user:', error);
* }
* }
* ```
*/
static create<A extends Partial<Attributes>, T extends Model<Attributes>>(this: Constructor<T>, data: A): Promise<T>;
/**
* Updates an existing record.
* Sends a PUT/PATCH request to update a record in the API.
* Can use either PUT (full update) or PATCH (partial update).
*
* @param id - The ID of the record to update
* @param data - The data to update the record with
* @param method - The HTTP method to use for the update (PUT or PATCH)
* @returns Promise resolving to the updated model instance
* @throws {Error} If the update fails
* @example
* ```typescript
* // Full update with PUT
* const user = await User.update(123, {
* name: 'John Doe',
* email: 'john@example.com',
* role: 'admin'
* });
*
* // Partial update with PATCH
* const user = await User.update(123, {
* name: 'John Doe'
* }, Methods.PATCH);
*
* // Update with error handling
* try {
* const user = await User.update(123, {
* email: 'new@example.com'
* });
* console.log(`Updated user: ${user.name}`);
* } catch (error) {
* if (error.status === 404) {
* console.log('User not found');
* } else if (error.status === 422) {
* console.log('Validation failed:', error.errors);
* } else {
* console.error('Error updating user:', error);
* }
* }
* ```
*/
static update<A extends Partial<Attributes>, T extends Model<Attributes>>(this: Constructor<T>, id: string | number, data: A, method?: MethodType): Promise<T>;
/**
* Deletes a record by ID.
* Sends a DELETE request to remove a record from the API.
* Returns void on success, throws an error on failure.
*
* @param id - The ID of the record to delete
* @returns Promise that resolves when the deletion is complete
* @throws {Error} If the deletion fails
* @example
* ```typescript
* // Delete a user
* await User.delete(123);
*
* // Delete with error handling
* try {
* await User.delete(123);
* console.log('User deleted successfully');
* } catch (error) {
* if (error.status === 404) {
* console.log('User not found');
* } else {
* console.error('Error deleting user:', error);
* }
* }
* ```
*/
static delete<T extends Model<Attributes>>(this: Constructor<T>, id: string | number): Promise<void>;
/**
* Retrieves the current model instance from the server.
* Updates the local instance with fresh data from the API.
* Useful for refreshing model data or after updates.
*
* @returns Promise resolving to the updated model instance
* @throws {Error} If the record is not found
* @example
* ```typescript
* // Refresh user data
* await user.get();
* console.log(`User name: ${user.name}`);
*
* // Refresh with error handling
* try {
* await user.get();
* console.log('User data refreshed');
* } catch (error) {
* if (error.status === 404) {
* console.log('User no longer exists');
* } else {
* console.error('Error refreshing user:', error);
* }
* }
* ```
*/
get(): Promise<this>;
/**
* Saves the current model instance to the server.
* Creates a new record if the model doesn't have an ID, updates existing record otherwise.
* Automatically determines whether to use POST or PUT/PATCH.
*
* @returns Promise resolving to the saved model instance
* @throws {Error} If the save operation fails
* @example
* ```typescript
* // Create a new user
* const user = new User({ name: 'John', email: 'john@example.com' });
* await user.save();
*
* // Update existing user
* user.name = 'John Doe';
* await user.save();
*
* // Save with error handling
* try {
* await user.save();
* console.log(`User saved with ID: ${user.id}`);
* } catch (error) {
* if (error.status === 422) {
* console.log('Validation failed:', error.errors);
* } else {
* console.error('Error saving user:', error);
* }
* }
* ```
*/
save(): Promise<this>;
/**
* Updates the model instance with new attributes and saves to the server.
* Can use either PUT (full update) or PATCH (partial update).
* Updates the local instance with the server response.
*
* @param attributes - Optional attributes to update before saving
* @param method - The HTTP method to use for the update (PUT or PATCH)
* @returns Promise resolving to the updated model instance
* @throws {Error} If the update fails
* @example
* ```typescript
* // Full update with PUT
* await user.update({
* name: 'John Doe',
* email: 'john@example.com',
* role: 'admin'
* });
*
* // Partial update with PATCH
* await user.update({
* name: 'John Doe'
* }, Methods.PATCH);
*
* // Update with error handling
* try {
* await user.update({ name: 'John Doe' });
* console.log(`User updated: ${user.name}`);
* } catch (error) {
* if (error.status === 404) {
* console.log('User no longer exists');
* } else if (error.status === 422) {
* console.log('Validation failed:', error.errors);
* } else {
* console.error('Error updating user:', error);
* }
* }
* ```
*/
update(attributes?: Partial<T>, method?: MethodType): Promise<this>;
/**
* Deletes the model instance from the server.
* Sends a DELETE request to remove the record.
* The local instance remains but becomes detached from the server.
*
* @returns Promise that resolves when the deletion is complete
* @throws {Error} If the deletion fails
* @example
* ```typescript
* // Delete the user
* await user.delete();
*
* // Delete with error handling
* try {
* await user.delete();
* console.log('User deleted successfully');
* } catch (error) {
* if (error.status === 404) {
* console.log('User no longer exists');
* } else {
* console.error('Error deleting user:', error);
* }
* }
* ```
*/
delete(): Promise<void>;
/**
* Calls the adapter with the given query builder.
* @param queryBuilder - The query builder to use
* @returns The adapter response
* @protected
*/
protected call(queryBuilder: QueryBuilder): Promise<AdapterResponse<T>>;
static call(queryBuilder: QueryBuilder): Promise<AdapterResponse>;
/**
* Converts the model instance to a plain object.
* Recursively converts nested model instances to plain objects.
* Useful for serialization or sending data to the server.
*
* @returns A plain object representation of the model's attributes
* @example
* ```typescript
* // Convert to plain object
* const userData = user.toObject();
* console.log(JSON.stringify(userData));
*
* // Convert with nested models
* const post = await Post.find(123);
* const postData = post.toObject();
* // {
* // id: 123,
* // title: 'My Post',
* // author: {
* // id: 456,
* // name: 'John Doe'
* // },
* // comments: [
* // { id: 789, content: 'Great post!' }
* // ]
* // }
* ```
*/
toObject(): Record<string, any>;
/**
* Resets a property on the model, resetting it to undefined.
* This is useful for clearing relationships or properties.
*
* @param key - The property key to reset
* @returns The model instance for method chaining
* @example
* ```typescript
* // reset a relationship
* user.reset('address');
* // Now user.address is undefined
*
* // reset multiple properties
* user.reset('address', 'profile');
* ```
*/
reset(...keys: string[]): this;
}