Skip to content

Commit

Permalink
feat: 🎸 query stack feats
Browse files Browse the repository at this point in the history
  • Loading branch information
TomTomB committed Jan 10, 2025
1 parent 005bf9e commit 4338bde
Show file tree
Hide file tree
Showing 8 changed files with 196 additions and 66 deletions.
45 changes: 21 additions & 24 deletions apps/playground/src/app/app.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ type Post = {
type GetPostQueryArgs = {
response: Post;
pathParams: {
postId: string;
postId: number;
};
};

Expand Down Expand Up @@ -198,38 +198,35 @@ export class DynCompComponent {
plusOnePage = signal(1);
currentPostId = signal(5);

myPost = getPost(E.withArgs(() => ({ pathParams: { postId: this.plusOnePage() as unknown as string } })));
myPost = getPost(E.withArgs(() => ({ pathParams: { postId: this.plusOnePage() } })));

posts = getPosts(E.withAutoRefresh({ signalChanges: [this.plusOnePage] }));

// myPostList = E.createQueryStack(
// () => getPost(E.withArgs(() => ({ pathParams: { postId: `${this.currentPostId()}` } }))),
// { append: true, transform: E.transformArrayResponse },
// );
posts = getPosts(E.withAutoRefresh({ onSignalChanges: [this.plusOnePage] }));

myPostList = E.createQueryStack({
queryCreator: getPost,
args: () => ({ pathParams: { postId: `${this.currentPostId()}` } }), // could return either one or an array of query args
dependencies: () => ({ myDep: this.plusOnePage() }),
args: ({ myDep }) => [
{ pathParams: { postId: this.currentPostId() * myDep } },
{ pathParams: { postId: this.currentPostId() * myDep + 1 } },
{ pathParams: { postId: this.currentPostId() * myDep + 2 } },
],
transform: E.transformArrayResponse,
append: true,
// features: [
// E.withPolling({ interval: 5000 }),
// E.withSuccessHandling<GetPostQueryArgs>({ handler: (post) => console.log(post.title) }),
// ],
});

// postAndUserList = E.createQueryStack(() => [
// getPost(E.withArgs(() => ({ pathParams: { postId: `${this.currentPostId()}` } }))),
// getUser(E.withArgs(() => ({ pathParams: { playerId: `${this.currentPostId()}` } }))),
// ]);

paged = E.createPagedQueryStack({
queryCreator: getPost,
args: (page) => ({ pathParams: { postId: `${page + this.plusOnePage()}` } }),
responseNormalizer: (response) => ({
items: [response],
totalHits: 10,
totalPages: 10,
currentPage: +response.id,
itemsPerPage: 1,
}),
initialPage: 6,
args: (page) => ({ pathParams: { postId: page + this.plusOnePage() } }),
responseNormalizer: E.fakePaginationAdapter(),
initialPage: 1,
// features: [
// E.withPolling({ interval: 5000 }),
// E.withSuccessHandling<GetPostQueryArgs>({ handler: (post) => console.log(post.title) }),
// ],
});

// id = computed(() => this.myPostQuery1.response()?.id);
Expand All @@ -246,7 +243,7 @@ export class DynCompComponent {
}

execAll() {
this.paged.execute();
this.paged.execute({ allowCache: true });
}

// login() {
Expand Down
49 changes: 42 additions & 7 deletions libs/query/src/lib/experimental/paged-query-stack.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { HttpErrorResponse } from '@angular/common/http';
import { computed, effect, isDevMode, Signal, signal, untracked } from '@angular/core';
import {
Expand All @@ -10,10 +11,11 @@ import {
import { AnyQuery, Query, QueryArgs, RequestArgs, ResponseType } from './query';
import { AnyQueryCreator, QueryArgsOf, QueryCreator } from './query-creator';
import {
pagedQueryNextPageCalledWithoutPreviousPage,
pagedQueryPageBiggerThanTotalPages,
pagedQueryPreviousPageCalledButAlreadyAtFirstPage,
pagedQueryStackNextPageCalledWithoutPreviousPage,
pagedQueryStackPageBiggerThanTotalPages,
pagedQueryStackPreviousPageCalledButAlreadyAtFirstPage,
} from './query-errors';
import { QueryFeature } from './query-features';
import { createQueryStack, transformArrayResponse } from './query-stack';
import { shouldRetryRequest } from './query-utils';

Expand Down Expand Up @@ -68,6 +70,20 @@ export const contentfulGqlLikePaginationAdapter = <T>(response: ContentfulGqlLik
return pagination;
};

export const fakePaginationAdapter = (totalHits = 10) => {
return <T>(response: T) => {
const pagination: NormalizedPagination<T> = {
items: [response],
totalPages: totalHits,
currentPage: 1,
itemsPerPage: 1,
totalHits,
};

return pagination;
};
};

export type CreatePagedQueryStackOptions<TArgs extends QueryArgs> = {
/**
* The normalizer function that will be used to normalize the response to a format that the paged query can understand.
Expand All @@ -77,6 +93,7 @@ export type CreatePagedQueryStackOptions<TArgs extends QueryArgs> = {
* - `ggLikePaginationAdapter`
* - `dynLikePaginationAdapter`
* - `contentfulGqlLikePaginationAdapter`
* - `fakePaginationAdapter` (for testing purposes)
*/
responseNormalizer: (response: ResponseType<TArgs>) => NormalizedPagination<ResponseType<TArgs>>;

Expand All @@ -95,6 +112,21 @@ export type CreatePagedQueryStackOptions<TArgs extends QueryArgs> = {
*/
args: (page: number) => RequestArgs<TArgs>;

/**
* The features that should be used for all queries in the stack.
*
* @throws If the `withArgs` feature is used. This feature is internally used.
*
* @example
* // Due to limitations in TypeScript, you have to manually add the needed generic types if needed.
* features: [E.withSuccessHandling<GetPostQueryArgs>({ handler: (post) => console.log(post.title) })]
*
* @example
* // If typings are not needed, you can use the feature without generics.
* features: [E.withPolling({ interval: 5000 })]
*/
features?: QueryFeature<any>[];

/**
* The page to start this paged query from.
* @default 1
Expand Down Expand Up @@ -131,6 +163,8 @@ export type PagedQueryStackExecuteOptions<TArgs extends QueryArgs> = {

export type PagedQueryStackDirection = 'next' | 'previous';

export type AnyPagedQueryStack = PagedQueryStack<AnyQuery>;

export type PagedQueryStack<TQuery extends AnyQuery> = {
/**
* The current pagination state of the paged query.
Expand Down Expand Up @@ -214,7 +248,7 @@ export type PagedQueryStack<TQuery extends AnyQuery> = {
export const createPagedQueryStack = <TCreator extends AnyQueryCreator, TArgs extends QueryArgsOf<TCreator>>(
options: CreatePagedQueryStackOptions<TArgs>,
) => {
const { responseNormalizer, queryCreator } = options;
const { responseNormalizer, queryCreator, features } = options;

const currentPageArgs = signal<RequestArgs<TArgs> | null>(null);
const initialPage = signal(options.initialPage ?? 1);
Expand All @@ -228,6 +262,7 @@ export const createPagedQueryStack = <TCreator extends AnyQueryCreator, TArgs ex
args: () => currentPageArgs(),
queryCreator,
append: true,
features,
appendFn: (oldQueries, newQueries) => {
const newQuery = newQueries[0];
const dir = pageDirection();
Expand Down Expand Up @@ -270,7 +305,7 @@ export const createPagedQueryStack = <TCreator extends AnyQueryCreator, TArgs ex

if (page < 1) {
if (isDevMode()) {
throw pagedQueryPreviousPageCalledButAlreadyAtFirstPage();
throw pagedQueryStackPreviousPageCalledButAlreadyAtFirstPage();
}

return;
Expand All @@ -287,15 +322,15 @@ export const createPagedQueryStack = <TCreator extends AnyQueryCreator, TArgs ex

if (!currentPagination) {
if (isDevMode()) {
throw pagedQueryNextPageCalledWithoutPreviousPage();
throw pagedQueryStackNextPageCalledWithoutPreviousPage();
}

return;
}

if (page > currentPagination.totalPages) {
if (isDevMode()) {
throw pagedQueryPageBiggerThanTotalPages(page, currentPagination.totalPages);
throw pagedQueryStackPageBiggerThanTotalPages(page, currentPagination.totalPages);
}

return;
Expand Down
2 changes: 1 addition & 1 deletion libs/query/src/lib/experimental/query-creator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { QueryClientConfig } from './query-client-config';
import { QueryFeature } from './query-features';

export type RouteType<TArgs extends QueryArgs> =
PathParamsType<TArgs> extends { [key: string]: string } ? (args: TArgs['pathParams']) => RouteString : RouteString;
PathParamsType<TArgs> extends { [key: string]: unknown } ? (args: TArgs['pathParams']) => RouteString : RouteString;

export type RouteString = `/${string}`;

Expand Down
30 changes: 20 additions & 10 deletions libs/query/src/lib/experimental/query-errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,13 @@ export const enum RuntimeErrorCode {
UNCACHEABLE_REQUEST_HAS_CACHE_KEY_PARAM = 300,
UNCACHEABLE_REQUEST_HAS_ALLOW_CACHE_PARAM = 301,

// Paged Query
PAGED_QUERY_PAGE_BIGGER_THAN_TOTAL_PAGES = 400,
PAGED_QUERY_NEXT_PAGE_CALLED_WITHOUT_PREVIOUS_PAGE = 401,
PAGED_QUERY_PREVIOUS_PAGE_CALLED_BUT_ALREADY_AT_FIRST_PAGE = 402,
// Paged Query Stack
PAGED_QUERY_STACK_PAGE_BIGGER_THAN_TOTAL_PAGES = 400,
PAGED_QUERY_STACK_NEXT_PAGE_CALLED_WITHOUT_PREVIOUS_PAGE = 401,
PAGED_QUERY_STACK_PREVIOUS_PAGE_CALLED_BUT_ALREADY_AT_FIRST_PAGE = 402,

// Query Stack
QUERY_STACK_WITH_ARGS_USED = 500,
}

export const queryFeatureUsedMultipleTimes = (type: QueryFeatureType) => {
Expand Down Expand Up @@ -170,23 +173,30 @@ export const uncacheableRequestHasAllowCacheParam = () => {
);
};

export const pagedQueryPageBiggerThanTotalPages = (page: number, totalPages: number) => {
export const pagedQueryStackPageBiggerThanTotalPages = (page: number, totalPages: number) => {
return new RuntimeError(
RuntimeErrorCode.PAGED_QUERY_PAGE_BIGGER_THAN_TOTAL_PAGES,
RuntimeErrorCode.PAGED_QUERY_STACK_PAGE_BIGGER_THAN_TOTAL_PAGES,
`The page "${page}" is bigger than the total pages "${totalPages}".`,
);
};

export const pagedQueryNextPageCalledWithoutPreviousPage = () => {
export const pagedQueryStackNextPageCalledWithoutPreviousPage = () => {
return new RuntimeError(
RuntimeErrorCode.PAGED_QUERY_NEXT_PAGE_CALLED_WITHOUT_PREVIOUS_PAGE,
RuntimeErrorCode.PAGED_QUERY_STACK_NEXT_PAGE_CALLED_WITHOUT_PREVIOUS_PAGE,
`fetchNextPage() has been called but the current page is not yet loaded. Please call it after the previous page has been loaded.`,
);
};

export const pagedQueryPreviousPageCalledButAlreadyAtFirstPage = () => {
export const pagedQueryStackPreviousPageCalledButAlreadyAtFirstPage = () => {
return new RuntimeError(
RuntimeErrorCode.PAGED_QUERY_PREVIOUS_PAGE_CALLED_BUT_ALREADY_AT_FIRST_PAGE,
RuntimeErrorCode.PAGED_QUERY_STACK_PREVIOUS_PAGE_CALLED_BUT_ALREADY_AT_FIRST_PAGE,
`fetchPreviousPage() has been called but the current page is already the first page.`,
);
};

export const queryStackWithArgsUsed = () => {
return new RuntimeError(
RuntimeErrorCode.QUERY_STACK_WITH_ARGS_USED,
`withArgs() has been used in a query stack or a paged query stack. This is not supported.`,
);
};
15 changes: 9 additions & 6 deletions libs/query/src/lib/experimental/query-features.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { HttpErrorResponse, HttpEvent } from '@angular/common/http';
import { computed, effect, Signal, untracked } from '@angular/core';
import { computed, Signal, untracked } from '@angular/core';
import { QueryArgs, RequestArgs, ResponseType } from './query';
import { CreateQueryCreatorOptions, InternalCreateQueryCreatorOptions, QueryConfig } from './query-creator';
import { QueryDependencies } from './query-dependencies';
Expand Down Expand Up @@ -155,7 +155,7 @@ export const withLogging = <TArgs extends QueryArgs>(options: WithLoggingFeature
return createQueryFeature<TArgs>({
type: QueryFeatureType.WithLogging,
fn: (context) => {
effect(
queryEffect(
() => {
const event = context.state.latestHttpEvent();

Expand All @@ -165,6 +165,7 @@ export const withLogging = <TArgs extends QueryArgs>(options: WithLoggingFeature
options.logFn(event);
});
},
QUERY_EFFECT_ERROR_MESSAGE,
{ injector: context.deps.injector },
);
},
Expand All @@ -181,7 +182,7 @@ export const withErrorHandling = <TArgs extends QueryArgs>(options: WithErrorHan
return createQueryFeature<TArgs>({
type: QueryFeatureType.WithErrorHandling,
fn: (context) => {
effect(
queryEffect(
() => {
const error = context.state.error();

Expand All @@ -191,6 +192,7 @@ export const withErrorHandling = <TArgs extends QueryArgs>(options: WithErrorHan
options.handler(error);
});
},
QUERY_EFFECT_ERROR_MESSAGE,
{ injector: context.deps.injector },
);
},
Expand All @@ -207,7 +209,7 @@ export const withSuccessHandling = <TArgs extends QueryArgs>(options: WithSucces
return createQueryFeature<TArgs>({
type: QueryFeatureType.WithSuccessHandling,
fn: (context) => {
effect(
queryEffect(
() => {
const response = context.state.response();

Expand All @@ -217,6 +219,7 @@ export const withSuccessHandling = <TArgs extends QueryArgs>(options: WithSucces
options.handler(response as NonNullable<ResponseType<TArgs>>);
});
},
QUERY_EFFECT_ERROR_MESSAGE,
{ injector: context.deps.injector },
);
},
Expand All @@ -225,7 +228,7 @@ export const withSuccessHandling = <TArgs extends QueryArgs>(options: WithSucces

export type WithAutoRefreshFeatureOptions = {
/** The signals that should trigger a refresh of the query */
signalChanges: Signal<unknown>[];
onSignalChanges: Signal<unknown>[];

/** Whether to ignore the `onlyManualExecution` query config flag */
ignoreOnlyManualExecution?: boolean;
Expand All @@ -250,7 +253,7 @@ export const withAutoRefresh = <TArgs extends QueryArgs>(options: WithAutoRefres

queryEffect(
() => {
for (const signal of options.signalChanges) {
for (const signal of options.onSignalChanges) {
signal();
}

Expand Down
6 changes: 2 additions & 4 deletions libs/query/src/lib/experimental/query-repository.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { HttpClient } from '@angular/common/http';
import { DestroyRef, inject } from '@angular/core';
import { buildQueryCacheKey, shouldCacheQuery } from '../query-client';
import { buildRoute } from '../request';
import { CreateHttpRequestClientOptions, HttpRequest, createHttpRequest } from './http-request';
import { QueryArgs, RequestArgs } from './query';
import { QueryClientConfig } from './query-client-config';
import { QueryMethod, RouteType } from './query-creator';
import { uncacheableRequestHasAllowCacheParam, uncacheableRequestHasCacheKeyParam } from './query-errors';
import { RunQueryExecuteOptions } from './query-execute-utils';
import { buildQueryCacheKey, shouldCacheQuery } from './query-utils';

export type QueryRepositoryRequestOptions<TArgs extends QueryArgs> = {
/**
Expand Down Expand Up @@ -104,9 +104,7 @@ export const createQueryRepository = (config: QueryClientConfig): QueryRepositor
body: args?.body,
queryParams: args?.queryParams,
pathParams: args?.pathParams,

// TODO: remaining props
// headers: args?.headers,
headers: args?.headers,
});

const previousKey = options.previousKey;
Expand Down
Loading

0 comments on commit 4338bde

Please sign in to comment.