import {
Adapter,
Condition,
PersistenceKeys,
Repository,
Sequence,
SequenceOptions,
UnsupportedError,
} from "@decaf-ts/core";
import { Context, InternalError, OperationKeys } from "@decaf-ts/db-decorators";
import { HttpConfig, HttpFlags } from "./types";
import { Constructor, Model } from "@decaf-ts/decorator-validation";
import { RestService } from "./RestService";
import { Statement } from "@decaf-ts/core";
/**
* @description Abstract HTTP adapter for REST API interactions
* @summary Provides a base implementation for HTTP adapters with methods for CRUD operations,
* URL construction, and error handling. This class extends the core Adapter class and
* implements the necessary methods for HTTP communication. Concrete implementations
* must provide specific HTTP client functionality.
* @template Y - The native HTTP client type
* @template Q - The query type used by the adapter
* @template F - The HTTP flags type, extending HttpFlags
* @template C - The context type, extending Context<F>
* @param {Y} native - The native HTTP client instance
* @param {HttpConfig} config - Configuration for the HTTP adapter
* @param {string} flavour - The adapter flavor identifier
* @param {string} [alias] - Optional alias for the adapter
* @class HttpAdapter
* @example
* ```typescript
* // Example implementation with Axios
* class AxiosAdapter extends HttpAdapter<AxiosInstance, AxiosRequestConfig> {
* constructor(config: HttpConfig) {
* super(axios.create(), config, 'axios');
* }
*
* async request<V>(details: AxiosRequestConfig): Promise<V> {
* const response = await this.native.request(details);
* return response.data;
* }
*
* // Implement other abstract methods...
* }
* ```
*/
export abstract class HttpAdapter<
Y extends HttpConfig,
CON,
Q,
F extends HttpFlags = HttpFlags,
C extends Context<F> = Context<F>,
> extends Adapter<Y, CON, Q, F, C> {
protected constructor(config: Y, flavour: string, alias?: string) {
super(config, flavour, alias);
}
/**
* @description Generates operation flags with HTTP headers
* @summary Extends the base flags method to include HTTP-specific headers for operations.
* This method adds an empty headers object to the flags returned by the parent class.
* @template F - The Repository Flags type
* @template M - The model type
* @param {OperationKeys.CREATE|OperationKeys.READ|OperationKeys.UPDATE|OperationKeys.DELETE} operation - The operation type
* @param {Constructor<M>} model - The model constructor
* @param {Partial<F>} overrides - Optional flag overrides
* @return {F} The flags object with headers
*/
override flags<M extends Model>(
operation:
| OperationKeys.CREATE
| OperationKeys.READ
| OperationKeys.UPDATE
| OperationKeys.DELETE,
model: Constructor<M>,
overrides: Partial<F>
) {
return Object.assign(super.flags<M>(operation, model, overrides), {
headers: {},
});
}
/**
* @description Returns the repository constructor for this adapter
* @summary Provides the RestService class as the repository implementation for this HTTP adapter.
* This method is used to create repository instances that work with this adapter type.
* @template M - The model type
* @return {Constructor<Repository<M, Q, HttpAdapter<Y, Q, F, C>, F, C>>} The repository constructor
*/
override repository<M extends Model>(): Constructor<
Repository<M, Q, HttpAdapter<Y, CON, Q, F, C>, F, C>
> {
return RestService as unknown as Constructor<
Repository<M, Q, HttpAdapter<Y, CON, Q, F, C>, F, C>
>;
}
/**
* @description Prepares a model for persistence
* @summary Converts a model instance into a format suitable for database storage,
* handling column mapping and separating transient properties
* @template M - The model type
* @param {M} model - The model instance to prepare
* @param pk - The primary key property name
* @return The prepared data
*/
override prepare<M extends Model>(
model: M,
pk: keyof M
): {
record: Record<string, any>;
id: string;
transient?: Record<string, any>;
} {
const log = this.log.for(this.prepare);
const result = Object.assign({}, model);
if ((model as any)[PersistenceKeys.METADATA]) {
log.silly(
`Passing along persistence metadata for ${(model as any)[PersistenceKeys.METADATA]}`
);
Object.defineProperty(result, PersistenceKeys.METADATA, {
enumerable: false,
writable: false,
configurable: true,
value: (model as any)[PersistenceKeys.METADATA],
});
}
return {
record: model,
id: model[pk] as string,
};
}
/**
* @description Converts database data back into a model instance
* @summary Reconstructs a model instance from database data, handling column mapping
* and reattaching transient properties
* @template M - The model type
* @param obj - The database record
* @param {string|Constructor<M>} clazz - The model class or name
* @param pk - The primary key property name
* @param {string|number|bigint} id - The primary key value
* @return {M} The reconstructed model instance
*/
override revert<M extends Model>(
obj: Record<string, any>,
clazz: string | Constructor<M>,
pk: keyof M,
id: string | number | bigint
): M {
const log = this.log.for(this.revert);
const ob: Record<string, any> = {};
const m = (
typeof clazz === "string" ? Model.build(ob, clazz) : new clazz(ob)
) as M;
log.silly(`Rebuilding model ${m.constructor.name} id ${id}`);
const constr = typeof clazz === "string" ? Model.get(clazz) : clazz;
if (!constr)
throw new InternalError(
`Failed to retrieve model constructor for ${clazz}`
);
const result = new (constr as Constructor<M>)(obj);
const metadata = obj[PersistenceKeys.METADATA];
if (metadata) {
log.silly(
`Passing along ${this.flavour} persistence metadata for ${m.constructor.name} id ${id}: ${metadata}`
);
Object.defineProperty(result, PersistenceKeys.METADATA, {
enumerable: false,
configurable: false,
writable: false,
value: metadata,
});
}
return result;
}
/**
* @description Constructs a URL for API requests
* @summary Builds a complete URL for API requests using the configured protocol and host,
* the specified table name, and optional query parameters. The method handles URL encoding.
* @param {string} tableName - The name of the table or endpoint
* @param {Record<string, string | number>} [queryParams] - Optional query parameters
* @return {string} The encoded URL string
*/
url(tableName: string, queryParams?: Record<string, string | number>) {
const url = new URL(
`${this.config.protocol}://${this.config.host}/${tableName}`
);
if (queryParams)
Object.entries(queryParams).forEach(([key, value]) =>
url.searchParams.append(key, value.toString())
);
// ensure spaces are encoded as %20 (not '+') to match expectations
return encodeURI(url.toString()).replace(/\+/g, "%20");
}
/**
* @description Sends an HTTP request
* @summary Abstract method that must be implemented by subclasses to send HTTP requests
* using the native HTTP client. This is the core method for making API calls.
* @template V - The response value type
* @param {Q} details - The request details specific to the HTTP client
* @return {Promise<V>} A promise that resolves with the response data
*/
abstract request<V>(details: Q): Promise<V>;
/**
* @description Creates a new resource
* @summary Abstract method that must be implemented by subclasses to create a new resource
* via HTTP. This typically corresponds to a POST request.
* @param {string} tableName - The name of the table or endpoint
* @param {string|number} id - The identifier for the resource
* @param {Record<string, any>} model - The data model to create
* @param {...any[]} args - Additional arguments
* @return {Promise<Record<string, any>>} A promise that resolves with the created resource
*/
abstract override create(
tableName: string,
id: string | number,
model: Record<string, any>,
...args: any[]
): Promise<Record<string, any>>;
/**
* @description Retrieves a resource by ID
* @summary Abstract method that must be implemented by subclasses to retrieve a resource
* via HTTP. This typically corresponds to a GET request.
* @param {string} tableName - The name of the table or endpoint
* @param {string|number|bigint} id - The identifier for the resource
* @param {...any[]} args - Additional arguments
* @return {Promise<Record<string, any>>} A promise that resolves with the retrieved resource
*/
abstract override read(
tableName: string,
id: string | number | bigint,
...args: any[]
): Promise<Record<string, any>>;
/**
* @description Updates an existing resource
* @summary Abstract method that must be implemented by subclasses to update a resource
* via HTTP. This typically corresponds to a PUT or PATCH request.
* @param {string} tableName - The name of the table or endpoint
* @param {string|number} id - The identifier for the resource
* @param {Record<string, any>} model - The updated data model
* @param {...any[]} args - Additional arguments
* @return {Promise<Record<string, any>>} A promise that resolves with the updated resource
*/
abstract override update(
tableName: string,
id: string | number,
model: Record<string, any>,
...args: any[]
): Promise<Record<string, any>>;
/**
* @description Deletes a resource by ID
* @summary Abstract method that must be implemented by subclasses to delete a resource
* via HTTP. This typically corresponds to a DELETE request.
* @param {string} tableName - The name of the table or endpoint
* @param {string|number|bigint} id - The identifier for the resource to delete
* @param {...any[]} args - Additional arguments
* @return {Promise<Record<string, any>>} A promise that resolves with the deletion result
*/
abstract override delete(
tableName: string,
id: string | number | bigint,
...args: any[]
): Promise<Record<string, any>>;
/**
* @description Executes a raw query
* @summary Method for executing raw queries directly with the HTTP client.
* This method is not supported by default in HTTP adapters and throws an UnsupportedError.
* Subclasses can override this method to provide implementation.
* @template R - The result type
* @param {Q} rawInput - The raw query input
* @param {boolean} process - Whether to process the result
* @param {...any[]} args - Additional arguments
* @return {Promise<R>} A promise that resolves with the query result
* @throws {UnsupportedError} Always throws as this method is not supported by default
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
raw<R>(rawInput: Q, process: boolean, ...args: any[]): Promise<R> {
return Promise.reject(
new UnsupportedError(
"Api is not natively available for HttpAdapters. If required, please extends this class"
)
);
}
/**
* @description Creates a sequence
* @summary Method for creating a sequence for generating unique identifiers.
* This method is not supported by default in HTTP adapters and throws an UnsupportedError.
* Subclasses can override this method to provide implementation.
* @param {SequenceOptions} options - Options for creating the sequence
* @return {Promise<Sequence>} A promise that resolves with the created sequence
* @throws {UnsupportedError} Always throws as this method is not supported by default
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
Sequence(options: SequenceOptions): Promise<Sequence> {
return Promise.reject(
new UnsupportedError(
"Api is not natively available for HttpAdapters. If required, please extends this class"
)
);
}
/**
* @description Creates a statement for querying
* @summary Method for creating a statement for building and executing queries.
* This method is not supported by default in HTTP adapters and throws an UnsupportedError.
* Subclasses can override this method to provide implementation.
* @template M - The model type
* @template ! - The raw query type
* @return {Statement<Q, M, any>} A statement object for building queries
* @throws {UnsupportedError} Always throws as this method is not supported by default
*/
override Statement<M extends Model>(): Statement<Q, M, any> {
throw new UnsupportedError(
"Api is not natively available for HttpAdapters. If required, please extends this class"
);
}
/**
* @description Parses a condition into a query
* @summary Method for parsing a condition object into a query format understood by the HTTP client.
* This method is not supported by default in HTTP adapters and throws an UnsupportedError.
* Subclasses can override this method to provide implementation.
* @param {Condition<any>} condition - The condition to parse
* @return {Q} The parsed query
* @throws {UnsupportedError} Always throws as this method is not supported by default
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
parseCondition(condition: Condition<any>): Q {
throw new UnsupportedError(
"Api is not natively available for HttpAdapters. If required, please extends this class"
);
}
}
Source