diff --git a/apps/desktop/src/lib/backend/backendService.svelte.ts b/apps/desktop/src/lib/backend/backendService.svelte.ts index 4a98c0a9d0..1eb0b8fb7c 100644 --- a/apps/desktop/src/lib/backend/backendService.svelte.ts +++ b/apps/desktop/src/lib/backend/backendService.svelte.ts @@ -1 +1,155 @@ -export default class BackendService {} +import { + createMutationEndpoint, + createQueryEndpointWithTransform, + type CustomBuilder, + type EndpointMap +} from '$lib/state/butlerModule'; +import { invalidatesItem, invalidatesList, providesItems, ReduxTag } from '$lib/state/tags'; +import { createEntityAdapter, type EntityState } from '@reduxjs/toolkit'; +import type { + CreateRuleRequest, + UpdateRuleRequest, + WorkspaceRule, + WorkspaceRuleId +} from '$lib/rules/rule'; +import type { BackendApi } from '$lib/state/clientState.svelte'; + +function typedEntries>(obj: T): [keyof T, T[keyof T]][] { + return Object.entries(obj) as [keyof T, T[keyof T]][]; +} + +function typedFromEntries>(entries: [keyof T, T[keyof T]][]): T { + return Object.fromEntries(entries) as T; +} + +export default class BackendService { + private static instance: BackendService; + private mutationApi: ReturnType; + private queryApi: ReturnType; + + private constructor(backendApi: BackendApi) { + this.mutationApi = injectMutationEndpoints(backendApi); + this.queryApi = injectQueryEndpoints(backendApi); + } + + static getInstance(backendApi: BackendApi): BackendService { + if (!BackendService.instance) { + BackendService.instance = new BackendService(backendApi); + } + return BackendService.instance; + } + + get() { + // Mutations + type MutationEndpoints = typeof this.mutationApi.endpoints; + + type MutateMap = { + [K in keyof MutationEndpoints as `${K}Mutate`]: MutationEndpoints[K]['mutate']; + }; + + type UseMutationMap = { + [K in keyof MutationEndpoints as `${K}UseMutation`]: MutationEndpoints[K]['useMutation']; + }; + + const mutate = typedFromEntries( + typedEntries(this.mutationApi.endpoints).map( + ([key, value]) => [`${key}Mutate`, value.mutate] as const + ) + ) as MutateMap; + + const useMutation = typedFromEntries( + typedEntries(this.mutationApi.endpoints).map( + ([key, value]) => [`${key}UseMutation`, value.useMutation] as const + ) + ) as UseMutationMap; + + // Queries + type QueryEndpoints = typeof this.queryApi.endpoints; + + type UseQueryMap = { + [K in keyof QueryEndpoints as `${K}UseQuery`]: (typeof this.queryApi.endpoints)[K]['useQuery']; + }; + + type FetchMap = { + [K in keyof QueryEndpoints as `${K}Fetch`]: (typeof this.queryApi.endpoints)[K]['fetch']; + }; + + const useQuery = typedFromEntries( + typedEntries(this.queryApi.endpoints).map( + ([key, value]) => [`${key}UseQuery`, value.useQuery] as const + ) + ) as UseQueryMap; + + const fetchMap = typedFromEntries( + typedEntries(this.queryApi.endpoints).map( + ([key, value]) => [`${key}Fetch`, value.fetch] as const + ) + ) as FetchMap; + + return { + ...mutate, + ...useMutation, + ...useQuery, + ...fetchMap + }; + } +} + +function injectQueryEndpoints(api: BackendApi) { + return api.injectEndpoints({ + endpoints: (build) => getQueryEndpointMap(build) + }); +} + +function injectMutationEndpoints(api: BackendApi) { + return api.injectEndpoints({ + endpoints: (build) => getMutationEndpointMap(build) + }); +} + +function getMutationEndpointMap(builder: CustomBuilder) { + return { + createWorkspaceRule: createMutationEndpoint< + WorkspaceRule, + { projectId: string; request: CreateRuleRequest } + >(builder, 'create_workspace_rule', () => [invalidatesList(ReduxTag.WorkspaceRules)]), + deleteWorkspaceRule: createMutationEndpoint< + void, + { projectId: string; ruleId: WorkspaceRuleId } + >(builder, 'delete_workspace_rule', () => [invalidatesList(ReduxTag.WorkspaceRules)]), + updateWorkspaceRule: createMutationEndpoint< + WorkspaceRule, + { projectId: string; request: UpdateRuleRequest } + >(builder, 'update_workspace_rule', (result) => + result + ? [ + invalidatesItem(ReduxTag.WorkspaceRules, result.id), + invalidatesList(ReduxTag.WorkspaceRules) + ] + : [] + ) + } satisfies EndpointMap; +} + +function getQueryEndpointMap(builder: CustomBuilder) { + return { + listWorkspaceRules: createQueryEndpointWithTransform< + WorkspaceRule[], + { projectId: string }, + EntityState + >( + builder, + 'list_workspace_rules', + (response: WorkspaceRule[]) => { + return workspaceRulesAdapter.addMany(workspaceRulesAdapter.getInitialState(), response); + }, + (result) => providesItems(ReduxTag.WorkspaceRules, result?.ids ?? []) + ) + } satisfies EndpointMap; +} + +const workspaceRulesAdapter = createEntityAdapter({ + selectId: (rule) => rule.id +}); + +export const workspaceRulesSelectors = workspaceRulesAdapter.getSelectors(); diff --git a/apps/desktop/src/lib/rules/rulesService.svelte.ts b/apps/desktop/src/lib/rules/rulesService.svelte.ts index ddb8e158aa..69a5957f6d 100644 --- a/apps/desktop/src/lib/rules/rulesService.svelte.ts +++ b/apps/desktop/src/lib/rules/rulesService.svelte.ts @@ -1,87 +1,31 @@ -import { invalidatesItem, invalidatesList, providesItems, ReduxTag } from '$lib/state/tags'; -import { createEntityAdapter, type EntityState } from '@reduxjs/toolkit'; -import type { - CreateRuleRequest, - UpdateRuleRequest, - WorkspaceRule, - WorkspaceRuleId -} from '$lib/rules/rule'; +import BackendService, { workspaceRulesSelectors } from '$lib/backend/backendService.svelte'; import type { BackendApi } from '$lib/state/clientState.svelte'; export default class RulesService { - private api: ReturnType; + private backendService: BackendService; + private apis: ReturnType; constructor(backendApi: BackendApi) { - this.api = injectEndpoints(backendApi); + this.backendService = BackendService.getInstance(backendApi); + this.apis = this.backendService.get(); } get createWorkspaceRule() { - return this.api.endpoints.createWorkspaceRule.useMutation(); + return this.apis.createWorkspaceRuleUseMutation(); } get deleteWorkspaceRule() { - return this.api.endpoints.deleteWorkspaceRule.useMutation(); + return this.apis.deleteWorkspaceRuleUseMutation(); } get updateWorkspaceRule() { - return this.api.endpoints.updateWorkspaceRule.useMutation(); + return this.apis.updateWorkspaceRuleUseMutation(); } listWorkspaceRules(projectId: string) { - return this.api.endpoints.listWorkspaceRules.useQuery( + return this.apis.listWorkspaceRulesUseQuery( { projectId }, { transform: (result) => workspaceRulesSelectors.selectAll(result) } ); } } - -function injectEndpoints(api: BackendApi) { - return api.injectEndpoints({ - endpoints: (build) => ({ - createWorkspaceRule: build.mutation< - WorkspaceRule, - { projectId: string; request: CreateRuleRequest } - >({ - extraOptions: { command: 'create_workspace_rule' }, - query: (args) => args, - invalidatesTags: () => [invalidatesList(ReduxTag.WorkspaceRules)] - }), - deleteWorkspaceRule: build.mutation({ - extraOptions: { command: 'delete_workspace_rule' }, - query: (args) => args, - invalidatesTags: () => [invalidatesList(ReduxTag.WorkspaceRules)] - }), - updateWorkspaceRule: build.mutation< - WorkspaceRule, - { projectId: string; request: UpdateRuleRequest } - >({ - extraOptions: { command: 'update_workspace_rule' }, - query: (args) => args, - invalidatesTags: (result) => - result - ? [ - invalidatesItem(ReduxTag.WorkspaceRules, result.id), - invalidatesList(ReduxTag.WorkspaceRules) - ] - : [] - }), - listWorkspaceRules: build.query< - EntityState, - { projectId: string } - >({ - extraOptions: { command: 'list_workspace_rules' }, - query: (args) => args, - providesTags: (result) => providesItems(ReduxTag.WorkspaceRules, result?.ids ?? []), - transformResponse: (response: WorkspaceRule[]) => { - return workspaceRulesAdapter.addMany(workspaceRulesAdapter.getInitialState(), response); - } - }) - }) - }); -} - -const workspaceRulesAdapter = createEntityAdapter({ - selectId: (rule) => rule.id -}); - -const workspaceRulesSelectors = workspaceRulesAdapter.getSelectors(); diff --git a/apps/desktop/src/lib/state/butlerModule.ts b/apps/desktop/src/lib/state/butlerModule.ts index 4d22653c77..78ed42c27e 100644 --- a/apps/desktop/src/lib/state/butlerModule.ts +++ b/apps/desktop/src/lib/state/butlerModule.ts @@ -4,6 +4,7 @@ import { type MutationHook } from '$lib/state/customHooks.svelte'; import { isMutationDefinition, isQueryDefinition } from '$lib/state/helpers'; +import { ReduxTag } from '$lib/state/tags'; import { type Reactive } from '@gitbutler/shared/storeUtils'; import { type BaseQueryFn, @@ -18,7 +19,13 @@ import { type QueryResultSelectorResult, type ApiModules, type QueryActionCreatorResult, - type StartQueryActionCreatorOptions + type StartQueryActionCreatorOptions, + type ResultDescription, + type BaseQueryError, + type BaseQueryMeta, + type BaseQueryArg, + type EndpointBuilder, + type BaseQueryResult } from '@reduxjs/toolkit/query'; import type { TauriBaseQueryFn } from '$lib/state/backendQuery'; import type { HookContext } from '$lib/state/context'; @@ -145,6 +152,144 @@ export function butlerModule(ctx: HookContext): Module { }; } +export type CustomBuilder = EndpointBuilder; + +export type CustomTags> = ResultDescription< + ReduxTag, + Result, + Args, + BaseQueryError, + BaseQueryMeta +>; + +export type CustomMutationEndpoint< + Result, + Args extends Record +> = MutationDefinition; + +export type CustomMutationDefinitionArgs> = Omit< + CustomMutationEndpoint, + 'type' +> & { + query: (args: Args) => BaseQueryArg; + queryFn: undefined; +}; + +export function createMutationDefinition>( + command: string, + invalidatesTags?: CustomTags +): CustomMutationDefinitionArgs { + return { + extraOptions: { command }, + query: (args: Args) => args, + invalidatesTags, + transformResponse: (result: Result) => result, + queryFn: undefined + }; +} + +export function createMutationEndpoint>( + builder: CustomBuilder, + command: string, + invalidatesTags?: CustomTags +): CustomMutationEndpoint { + const defaultDefinition = createMutationDefinition(command, invalidatesTags); + return builder.mutation(defaultDefinition); +} + +export type CustomQueryEndpoint> = QueryDefinition< + Args, + TauriBaseQueryFn, + ReduxTag, + Result, + 'backend' +>; + +export type CustomQueryDefinitionArgs< + Result, + Args extends Record, + APIResult extends BaseQueryResult +> = Omit, 'type'> & { + query: (args: Args) => BaseQueryArg; + queryFn: undefined; + transformResponse: (result: APIResult) => Result | Promise; +}; + +export function createQueryDefinition< + Result, + Args extends Record, + TransformedResult = Result +>( + command: string, + transformResponse: (result: Result) => TransformedResult | Promise, + providesTags?: CustomTags +): CustomQueryDefinitionArgs { + return { + extraOptions: { command }, + query: (args: Args) => args, + providesTags, + transformResponse, + queryFn: undefined + }; +} + +export function createQueryEndpointWithTransform< + Result, + Args extends Record, + TransformedResult = Result +>( + builder: CustomBuilder, + command: string, + transformResponse: (result: Result) => TransformedResult | Promise, + providesTags?: CustomTags +): CustomQueryEndpoint { + const defaultDefinition = createQueryDefinition( + command, + transformResponse, + providesTags + ); + return builder.query(defaultDefinition); +} + +export function createQueryEndpoint>( + builder: CustomBuilder, + command: string, + providesTags?: CustomTags +): CustomQueryEndpoint { + const defaultDefinition = createQueryDefinition( + command, + (result) => result, + providesTags + ); + return builder.query(defaultDefinition); +} + +export type EndpointMap = { + [endpointName: string]: CustomQueryEndpoint | CustomMutationEndpoint; +}; + +// interface BaseCustomEndpointDef { +// type: 'query' | 'mutation'; +// } + +// export interface CustomQueryEndpointDef> +// extends BaseCustomEndpointDef { +// type: 'query'; +// command: string; +// providesTags?: CustomTags; +// } + +// export interface CustomMutationEndpointDef> +// extends BaseCustomEndpointDef { +// type: 'mutation'; +// command: string; +// invalidatesTags?: CustomTags; +// } + +// export type CustomEndpointDef> = +// | CustomQueryEndpointDef +// | CustomMutationEndpointDef; + /** * Custom return type for the `QueryHooks` extensions. */