import {
Context,
DirectionLimitOffset,
OrderDirection,
PersistenceKeys,
prepared,
PreparedStatement,
PreparedStatementKeys,
Repository,
} from "@decaf-ts/core";
import type {
ContextOf,
MaybeContextualArg,
SerializedPage,
} from "@decaf-ts/core";
import { Model } from "@decaf-ts/decorator-validation";
import { Constructor } from "@decaf-ts/decoration";
import { HttpAdapter } from "./adapter";
import { OperationKeys } from "@decaf-ts/db-decorators";
/**
* @description Repository for REST API interactions
* @summary A specialized repository implementation for interacting with REST APIs.
* This class extends the core Repository class and works with HTTP adapters to
* provide CRUD operations for models via REST endpoints.
* This Is NOT the default repository for the HTTP adapter. That would be {@link RestService}.
* Use this only in the specific case of needing to run the CURD model logic (decoration) before submitting to the backend
* @template M - The model type, extending Model
* @template Q - The query type used by the adapter
* @template A - The HTTP adapter type, extending HttpAdapter
* @template F - The HTTP flags type, extending HttpFlags
* @template C - The context type, extending Context<F>
* @param {A} adapter - The HTTP adapter instance
* @param {Constructor<M>} [clazz] - Optional constructor for the model class
* @class RestRepository
* @example
* ```typescript
* // Create a repository for User model with Axios adapter
* const axiosAdapter = new AxiosAdapter({
* protocol: 'https',
* host: 'api.example.com'
* });
* const userRepository = new RestRepository(axiosAdapter, User);
*
* // Use the repository for CRUD operations
* const user = await userRepository.findById('123');
* ```
* @see {@link RestService}
*/
export class RestRepository<
M extends Model,
A extends HttpAdapter<any, any, any, any, any>,
Q = A extends HttpAdapter<any, any, any, infer Q, any> ? Q : never,
> extends Repository<M, A> {
protected override _overrides = Object.assign({}, super["_overrides"], {
allowRawStatements: false,
forcePrepareSimpleQueries: true,
forcePrepareComplexQueries: true,
});
constructor(adapter: A, clazz?: Constructor<M>) {
super(adapter, clazz);
}
url<M extends Model>(tableName: string | Constructor<M>): string;
url<M extends Model>(
tableName: string | Constructor<M>,
pathParams: string[]
): string;
url<M extends Model>(
tableName: string | Constructor<M>,
queryParams: Record<string, string | number> | undefined
): string;
url<M extends Model>(
tableName: string | Constructor<M>,
pathParams?: string[] | Record<string, string | number>,
queryParams?: Record<string, string | number>
): string {
return this.adapter.url(tableName, pathParams as any, queryParams as any);
}
override async paginateBy(
key: keyof M,
order: OrderDirection,
ref: Omit<DirectionLimitOffset, "direction"> = {
offset: 1,
limit: 10,
},
...args: MaybeContextualArg<ContextOf<A>>
): Promise<SerializedPage<M>> {
const { offset, bookmark, limit } = ref;
const { log, ctxArgs } = (
await this.logCtx(args, PreparedStatementKeys.PAGE_BY, true)
).for(this.paginateBy);
log.verbose(
`paginating ${Model.tableName(this.class)} with page size ${limit}`
);
const params: DirectionLimitOffset = {
direction: order,
limit: limit,
};
if (bookmark) {
params.bookmark = bookmark as any;
}
return this.statement(
this.paginateBy.name,
key,
offset,
params,
...ctxArgs
);
}
override async listBy(
key: keyof M,
order: OrderDirection,
...args: MaybeContextualArg<ContextOf<A>>
) {
const { log, ctxArgs } = (
await this.logCtx(args, PreparedStatementKeys.LIST_BY, true)
).for(this.listBy);
log.verbose(
`listing ${Model.tableName(this.class)} by ${key as string} ${order}`
);
return (await this.statement(
this.listBy.name,
key,
{ direction: order },
...ctxArgs
)) as any;
}
override async findBy(
key: keyof M,
value: any,
...args: MaybeContextualArg<ContextOf<A>>
): Promise<M[]> {
const { log, ctxArgs } = (
await this.logCtx(args, PreparedStatementKeys.FIND_BY, true)
).for(this.findBy);
log.verbose(
`finding ${Model.tableName(this.class)} with ${key as string} ${value}`
);
return (await this.statement(
this.findBy.name,
key,
value,
{},
...ctxArgs
)) as any;
}
override async findOneBy(
key: keyof M,
value: any,
...args: MaybeContextualArg<ContextOf<A>>
): Promise<M> {
const { log, ctxArgs } = (
await this.logCtx(args, PreparedStatementKeys.FIND_ONE_BY, true)
).for(this.findOneBy);
log.verbose(
`finding ${Model.tableName(this.class)} with ${key as string} ${value}`
);
return (await this.statement(
this.findOneBy.name,
key,
value,
{},
...ctxArgs
)) as any;
}
override async statement(
name: string,
...args: MaybeContextualArg<ContextOf<A>>
): Promise<any> {
const { log, ctx, ctxArgs } = (
await this.logCtx(args, PersistenceKeys.STATEMENT, true)
).for(this.statement);
const argList = ctxArgs.slice(0, -1);
const lastArg = argList[argList.length - 1];
const hasParams =
typeof lastArg === "object" &&
lastArg !== null &&
!Array.isArray(lastArg);
const params = hasParams
? (argList.pop() as Record<string, any>)
: undefined;
const query: PreparedStatement<any> = {
class: this.class,
args: argList,
method: name,
params: params,
} as PreparedStatement<any>;
const req = this.adapter.toRequest(query, ctx);
log.verbose(`Executing prepared statement ${name}`);
return this.adapter.parseResponse(
this.class,
name,
await this.request(req, ...ctxArgs)
);
}
async request<R>(
details: Q,
...args: MaybeContextualArg<ContextOf<A>>
): Promise<R> {
let contextualizeArgs: any;
if (args.length && args[args.length - 1] instanceof Context) {
contextualizeArgs = this.logCtx(args, this.request);
} else {
contextualizeArgs = (
await this.logCtx(args, OperationKeys.READ, true)
).for(this.request);
}
const { ctxArgs } = contextualizeArgs;
return this.adapter.request<R>(details, ...ctxArgs);
}
@prepared()
override async countOf(
key?: keyof M,
...args: MaybeContextualArg<ContextOf<A>>
): Promise<number> {
const { log, ctxArgs } = (
await this.logCtx(args, PreparedStatementKeys.COUNT_OF, true)
).for(this.countOf);
log.verbose(
`counting ${Model.tableName(this.class)}${key ? ` by ${key as string}` : ""}`
);
const stmtArgs: any[] = key ? [key, {}] : [{}];
return (await this.statement(
this.countOf.name,
...stmtArgs,
...ctxArgs
)) as any;
}
@prepared()
override async maxOf<K extends keyof M>(
key: K,
...args: MaybeContextualArg<ContextOf<A>>
): Promise<M[K]> {
const { log, ctxArgs } = (
await this.logCtx(args, PreparedStatementKeys.MAX_OF, true)
).for(this.maxOf);
log.verbose(`finding max of ${key as string} in ${Model.tableName(this.class)}`);
return (await this.statement(this.maxOf.name, key, {}, ...ctxArgs)) as any;
}
@prepared()
override async minOf<K extends keyof M>(
key: K,
...args: MaybeContextualArg<ContextOf<A>>
): Promise<M[K]> {
const { log, ctxArgs } = (
await this.logCtx(args, PreparedStatementKeys.MIN_OF, true)
).for(this.minOf);
log.verbose(`finding min of ${key as string} in ${Model.tableName(this.class)}`);
return (await this.statement(this.minOf.name, key, {}, ...ctxArgs)) as any;
}
@prepared()
override async avgOf<K extends keyof M>(
key: K,
...args: MaybeContextualArg<ContextOf<A>>
): Promise<number> {
const { log, ctxArgs } = (
await this.logCtx(args, PreparedStatementKeys.AVG_OF, true)
).for(this.avgOf);
log.verbose(`calculating avg of ${key as string} in ${Model.tableName(this.class)}`);
return (await this.statement(this.avgOf.name, key, {}, ...ctxArgs)) as any;
}
@prepared()
override async sumOf<K extends keyof M>(
key: K,
...args: MaybeContextualArg<ContextOf<A>>
): Promise<number> {
const { log, ctxArgs } = (
await this.logCtx(args, PreparedStatementKeys.SUM_OF, true)
).for(this.sumOf);
log.verbose(`calculating sum of ${key as string} in ${Model.tableName(this.class)}`);
return (await this.statement(this.sumOf.name, key, {}, ...ctxArgs)) as any;
}
@prepared()
override async distinctOf<K extends keyof M>(
key: K,
...args: MaybeContextualArg<ContextOf<A>>
): Promise<M[K][]> {
const { log, ctxArgs } = (
await this.logCtx(args, PreparedStatementKeys.DISTINCT_OF, true)
).for(this.distinctOf);
log.verbose(
`finding distinct values of ${key as string} in ${Model.tableName(this.class)}`
);
return (await this.statement(
this.distinctOf.name,
key,
{},
...ctxArgs
)) as any;
}
@prepared()
override async groupOf<K extends keyof M>(
key: K,
...args: MaybeContextualArg<ContextOf<A>>
): Promise<Record<string, M[]>> {
const { log, ctxArgs } = (
await this.logCtx(args, PreparedStatementKeys.GROUP_OF, true)
).for(this.groupOf);
log.verbose(`grouping ${Model.tableName(this.class)} by ${key as string}`);
return (await this.statement(
this.groupOf.name,
key,
{},
...ctxArgs
)) as any;
}
}
Source