@veams/status-quo-query 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +12 -0
- package/.turbo/turbo-check$colon$types.log +4 -0
- package/.turbo/turbo-lint.log +8 -0
- package/.turbo/turbo-test.log +13 -0
- package/CHANGELOG.md +5 -0
- package/README.md +173 -0
- package/dist/cache.d.ts +16 -0
- package/dist/cache.js +17 -0
- package/dist/cache.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/mutation-service.d.ts +24 -0
- package/dist/mutation-service.js +31 -0
- package/dist/mutation-service.js.map +1 -0
- package/dist/mutation.d.ts +24 -0
- package/dist/mutation.js +31 -0
- package/dist/mutation.js.map +1 -0
- package/dist/query-service.d.ts +33 -0
- package/dist/query-service.js +55 -0
- package/dist/query-service.js.map +1 -0
- package/dist/query.d.ts +33 -0
- package/dist/query.js +55 -0
- package/dist/query.js.map +1 -0
- package/dist/runtime.d.ts +16 -0
- package/dist/runtime.js +17 -0
- package/dist/runtime.js.map +1 -0
- package/eslint.config.mjs +50 -0
- package/jest.config.cjs +22 -0
- package/package.json +84 -0
- package/src/__tests__/cache.spec.ts +32 -0
- package/src/__tests__/mutation.spec.ts +49 -0
- package/src/__tests__/query.spec.ts +80 -0
- package/src/cache.ts +34 -0
- package/src/index.ts +3 -0
- package/src/mutation.ts +100 -0
- package/src/query.ts +152 -0
- package/tsconfig.eslint.json +12 -0
- package/tsconfig.json +32 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { fileURLToPath } from 'node:url';
|
|
2
|
+
|
|
3
|
+
import js from '@eslint/js';
|
|
4
|
+
import { defineConfig } from 'eslint/config';
|
|
5
|
+
import globals from 'globals';
|
|
6
|
+
import tseslint from 'typescript-eslint';
|
|
7
|
+
|
|
8
|
+
const packageRoot = fileURLToPath(new URL('.', import.meta.url));
|
|
9
|
+
const sourceFiles = ['src/**/*.ts'];
|
|
10
|
+
const testFiles = ['src/**/*.{spec,test}.ts', 'src/**/__tests__/**/*.ts'];
|
|
11
|
+
|
|
12
|
+
export default defineConfig(
|
|
13
|
+
{
|
|
14
|
+
ignores: ['coverage/**', 'dist/**'],
|
|
15
|
+
},
|
|
16
|
+
js.configs.recommended,
|
|
17
|
+
tseslint.configs.recommended,
|
|
18
|
+
tseslint.configs.recommendedTypeChecked,
|
|
19
|
+
{
|
|
20
|
+
files: sourceFiles,
|
|
21
|
+
languageOptions: {
|
|
22
|
+
globals: {
|
|
23
|
+
...globals.node,
|
|
24
|
+
},
|
|
25
|
+
parserOptions: {
|
|
26
|
+
project: './tsconfig.eslint.json',
|
|
27
|
+
tsconfigRootDir: packageRoot,
|
|
28
|
+
},
|
|
29
|
+
sourceType: 'module',
|
|
30
|
+
},
|
|
31
|
+
rules: {
|
|
32
|
+
'@typescript-eslint/consistent-type-imports': 'error',
|
|
33
|
+
'@typescript-eslint/no-unused-vars': [
|
|
34
|
+
'error',
|
|
35
|
+
{
|
|
36
|
+
argsIgnorePattern: '^_',
|
|
37
|
+
varsIgnorePattern: '^_',
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
files: testFiles,
|
|
44
|
+
languageOptions: {
|
|
45
|
+
globals: {
|
|
46
|
+
...globals.jest,
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
}
|
|
50
|
+
);
|
package/jest.config.cjs
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
extensionsToTreatAsEsm: ['.ts'],
|
|
3
|
+
transformIgnorePatterns: [],
|
|
4
|
+
transform: {
|
|
5
|
+
'^.+\\.(t|j)sx?$': '@swc/jest',
|
|
6
|
+
'^.+\\.mjs$': '@swc/jest',
|
|
7
|
+
},
|
|
8
|
+
moduleNameMapper: {
|
|
9
|
+
'^(\\.{1,2}/.*)\\.js$': '$1',
|
|
10
|
+
'^(\\.{1,2}/.*)\\.mjs$': '$1',
|
|
11
|
+
},
|
|
12
|
+
verbose: false,
|
|
13
|
+
passWithNoTests: true,
|
|
14
|
+
clearMocks: true,
|
|
15
|
+
coverageDirectory: './coverage/',
|
|
16
|
+
coverageReporters: ['json-summary', ['text', { skipFull: true }], 'lcov'],
|
|
17
|
+
roots: ['<rootDir>/src'],
|
|
18
|
+
testMatch: ['**/__tests__/**/?(*.)+(spec|test).[jt]s?(x)'],
|
|
19
|
+
reporters: ['default'],
|
|
20
|
+
moduleFileExtensions: ['mjs', 'js', 'ts', 'jsx', 'tsx'],
|
|
21
|
+
testEnvironment: 'node',
|
|
22
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@veams/status-quo-query",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "TanStack Query service layer for the VEAMS StatusQuo ecosystem.",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"module": "dist/index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"import": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"default": "./dist/index.js"
|
|
12
|
+
},
|
|
13
|
+
"require": "./dist/index.js"
|
|
14
|
+
},
|
|
15
|
+
"./mutation": {
|
|
16
|
+
"import": {
|
|
17
|
+
"types": "./dist/mutation.d.ts",
|
|
18
|
+
"default": "./dist/mutation.js"
|
|
19
|
+
},
|
|
20
|
+
"require": "./dist/mutation.js"
|
|
21
|
+
},
|
|
22
|
+
"./query": {
|
|
23
|
+
"import": {
|
|
24
|
+
"types": "./dist/query.d.ts",
|
|
25
|
+
"default": "./dist/query.js"
|
|
26
|
+
},
|
|
27
|
+
"require": "./dist/query.js"
|
|
28
|
+
},
|
|
29
|
+
"./cache": {
|
|
30
|
+
"import": {
|
|
31
|
+
"types": "./dist/cache.d.ts",
|
|
32
|
+
"default": "./dist/cache.js"
|
|
33
|
+
},
|
|
34
|
+
"require": "./dist/cache.js"
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
"types": "dist/index.d.ts",
|
|
38
|
+
"type": "module",
|
|
39
|
+
"sideEffects": false,
|
|
40
|
+
"scripts": {
|
|
41
|
+
"build": "npm-run-all compile",
|
|
42
|
+
"bundle:ts": "tsc --project tsconfig.json",
|
|
43
|
+
"check:types": "tsc --skipLibCheck --noEmit",
|
|
44
|
+
"compile": "npm-run-all bundle:ts",
|
|
45
|
+
"lint": "npm run lint:ts",
|
|
46
|
+
"lint:ts": "eslint --fix \"src/**/*.ts\"",
|
|
47
|
+
"test": "cross-env NODE_ENV=test jest --config jest.config.cjs",
|
|
48
|
+
"release": "npm run build && release-it"
|
|
49
|
+
},
|
|
50
|
+
"peerDependencies": {
|
|
51
|
+
"@tanstack/query-core": ">=5.0.0"
|
|
52
|
+
},
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"@swc/core": "1.15.11",
|
|
55
|
+
"@swc/jest": "0.2.39",
|
|
56
|
+
"@tanstack/query-core": "5.91.2",
|
|
57
|
+
"@types/jest": "30.0.0",
|
|
58
|
+
"@types/node": "25.2.3",
|
|
59
|
+
"cross-env": "10.1.0",
|
|
60
|
+
"jest": "30.3.0",
|
|
61
|
+
"npm-run-all": "4.1.5",
|
|
62
|
+
"release-it": "19.2.4",
|
|
63
|
+
"tslib": "2.8.1",
|
|
64
|
+
"typescript": "5.9.3"
|
|
65
|
+
},
|
|
66
|
+
"keywords": [
|
|
67
|
+
"TanStack Query",
|
|
68
|
+
"StatusQuo",
|
|
69
|
+
"Service Layer"
|
|
70
|
+
],
|
|
71
|
+
"publishConfig": {
|
|
72
|
+
"access": "public"
|
|
73
|
+
},
|
|
74
|
+
"author": "Sebastian Fitzner",
|
|
75
|
+
"license": "MIT",
|
|
76
|
+
"repository": {
|
|
77
|
+
"type": "git",
|
|
78
|
+
"url": "git+https://github.com/Veams/veams.git",
|
|
79
|
+
"directory": "packages/status-quo-query"
|
|
80
|
+
},
|
|
81
|
+
"bugs": {
|
|
82
|
+
"url": "https://github.com/Veams/veams/issues"
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { QueryClient } from '@tanstack/query-core';
|
|
2
|
+
|
|
3
|
+
import { setupCache } from '../cache';
|
|
4
|
+
|
|
5
|
+
describe('Cache API', () => {
|
|
6
|
+
it('exposes cache-level query client operations', async () => {
|
|
7
|
+
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: 0 } } });
|
|
8
|
+
const invalidateQueriesSpy = jest.spyOn(queryClient, 'invalidateQueries');
|
|
9
|
+
const refetchQueriesSpy = jest.spyOn(queryClient, 'refetchQueries');
|
|
10
|
+
const cancelQueriesSpy = jest.spyOn(queryClient, 'cancelQueries');
|
|
11
|
+
const resetQueriesSpy = jest.spyOn(queryClient, 'resetQueries');
|
|
12
|
+
const removeQueriesSpy = jest.spyOn(queryClient, 'removeQueries');
|
|
13
|
+
const cache = setupCache(queryClient);
|
|
14
|
+
|
|
15
|
+
cache.setQueryData<{ id: number }>(['user', 42], { id: 42 });
|
|
16
|
+
|
|
17
|
+
expect(cache.getQueryData<{ id: number }>(['user', 42])).toEqual({ id: 42 });
|
|
18
|
+
expect(cache.unsafe_getClient()).toBe(queryClient);
|
|
19
|
+
|
|
20
|
+
await cache.invalidateQueries({ queryKey: ['user'] });
|
|
21
|
+
await cache.refetchQueries({ queryKey: ['user'] });
|
|
22
|
+
await cache.cancelQueries({ queryKey: ['user'] });
|
|
23
|
+
await cache.resetQueries({ queryKey: ['user'] });
|
|
24
|
+
cache.removeQueries({ queryKey: ['user'] });
|
|
25
|
+
|
|
26
|
+
expect(invalidateQueriesSpy).toHaveBeenCalledWith({ queryKey: ['user'] });
|
|
27
|
+
expect(refetchQueriesSpy).toHaveBeenCalledWith({ queryKey: ['user'] });
|
|
28
|
+
expect(cancelQueriesSpy).toHaveBeenCalledWith({ queryKey: ['user'] });
|
|
29
|
+
expect(resetQueriesSpy).toHaveBeenCalledWith({ queryKey: ['user'] });
|
|
30
|
+
expect(removeQueriesSpy).toHaveBeenCalledWith({ queryKey: ['user'] });
|
|
31
|
+
});
|
|
32
|
+
});
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { QueryClient } from '@tanstack/query-core';
|
|
2
|
+
|
|
3
|
+
import { setupMutation } from '../mutation';
|
|
4
|
+
|
|
5
|
+
describe('Mutation Service', () => {
|
|
6
|
+
it('tracks successful mutations', async () => {
|
|
7
|
+
const queryClient = new QueryClient({ defaultOptions: { mutations: { retry: 0 } } });
|
|
8
|
+
const createMutation = setupMutation(queryClient);
|
|
9
|
+
let receivedPayload: { id: number } | undefined;
|
|
10
|
+
const mutationFn = jest.fn((payload: { id: number }) => {
|
|
11
|
+
receivedPayload = payload;
|
|
12
|
+
return Promise.resolve({ ok: true as const, id: payload.id });
|
|
13
|
+
});
|
|
14
|
+
const service = createMutation(mutationFn);
|
|
15
|
+
|
|
16
|
+
const statuses: string[] = [];
|
|
17
|
+
const unsubscribe = service.subscribe((snapshot) => {
|
|
18
|
+
statuses.push(snapshot.status);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
expect(service.getSnapshot().status).toBe('idle');
|
|
22
|
+
expect(service.getSnapshot().isIdle).toBe(true);
|
|
23
|
+
|
|
24
|
+
const result = await service.mutate({ id: 42 });
|
|
25
|
+
|
|
26
|
+
unsubscribe();
|
|
27
|
+
|
|
28
|
+
expect(result).toEqual({ ok: true, id: 42 });
|
|
29
|
+
expect(receivedPayload).toEqual({ id: 42 });
|
|
30
|
+
expect(mutationFn).toHaveBeenCalledTimes(1);
|
|
31
|
+
expect(statuses).toContain('pending');
|
|
32
|
+
expect(statuses).toContain('success');
|
|
33
|
+
expect(service.getSnapshot().status).toBe('success');
|
|
34
|
+
expect(service.unsafe_getResult().data).toEqual({ ok: true, id: 42 });
|
|
35
|
+
|
|
36
|
+
service.reset();
|
|
37
|
+
expect(service.getSnapshot().status).toBe('idle');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('tracks failed mutations', async () => {
|
|
41
|
+
const queryClient = new QueryClient({ defaultOptions: { mutations: { retry: 0 } } });
|
|
42
|
+
const createMutation = setupMutation(queryClient);
|
|
43
|
+
const mutationFn = jest.fn().mockRejectedValue(new Error('boom'));
|
|
44
|
+
const service = createMutation(mutationFn);
|
|
45
|
+
|
|
46
|
+
await expect(service.mutate()).rejects.toThrow('boom');
|
|
47
|
+
expect(service.getSnapshot().status).toBe('error');
|
|
48
|
+
});
|
|
49
|
+
});
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { QueryClient } from '@tanstack/query-core';
|
|
2
|
+
|
|
3
|
+
import { isQueryLoading, setupQuery, toQueryMetaState } from '../query';
|
|
4
|
+
|
|
5
|
+
describe('Query Service', () => {
|
|
6
|
+
it('returns data after refetch', async () => {
|
|
7
|
+
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: 0 } } });
|
|
8
|
+
const createQuery = setupQuery(queryClient);
|
|
9
|
+
const queryFn = jest.fn().mockResolvedValue('hello');
|
|
10
|
+
|
|
11
|
+
const service = createQuery(['demo', 'query'], queryFn, { enabled: false });
|
|
12
|
+
|
|
13
|
+
expect(service.getSnapshot().data).toBeUndefined();
|
|
14
|
+
|
|
15
|
+
const result = await service.refetch();
|
|
16
|
+
|
|
17
|
+
expect(result.data).toBe('hello');
|
|
18
|
+
expect(service.getSnapshot().data).toBe('hello');
|
|
19
|
+
expect(service.unsafe_getResult().data).toBe('hello');
|
|
20
|
+
expect(queryFn).toHaveBeenCalledTimes(1);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('notifies subscribers on updates', async () => {
|
|
24
|
+
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: 0 } } });
|
|
25
|
+
const createQuery = setupQuery(queryClient);
|
|
26
|
+
const queryFn = jest.fn().mockResolvedValue('updated');
|
|
27
|
+
const service = createQuery(['demo', 'notify'], queryFn, { enabled: false });
|
|
28
|
+
|
|
29
|
+
const statuses: string[] = [];
|
|
30
|
+
const unsubscribe = service.subscribe((snapshot) => {
|
|
31
|
+
statuses.push(snapshot.status);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
await service.refetch();
|
|
35
|
+
unsubscribe();
|
|
36
|
+
|
|
37
|
+
expect(statuses).toContain('pending');
|
|
38
|
+
expect(statuses).toContain('success');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('invalidates its own query key exactly', async () => {
|
|
42
|
+
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: 0 } } });
|
|
43
|
+
const invalidateQueriesSpy = jest.spyOn(queryClient, 'invalidateQueries');
|
|
44
|
+
const createQuery = setupQuery(queryClient);
|
|
45
|
+
const service = createQuery(['demo', 'invalidate'], jest.fn().mockResolvedValue('updated'), {
|
|
46
|
+
enabled: false,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
await service.invalidate({ refetchType: 'none' });
|
|
50
|
+
|
|
51
|
+
expect(invalidateQueriesSpy).toHaveBeenCalledWith(
|
|
52
|
+
{
|
|
53
|
+
exact: true,
|
|
54
|
+
queryKey: ['demo', 'invalidate'],
|
|
55
|
+
refetchType: 'none',
|
|
56
|
+
},
|
|
57
|
+
undefined
|
|
58
|
+
);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('derives loading state from query metadata', () => {
|
|
62
|
+
expect(
|
|
63
|
+
isQueryLoading(
|
|
64
|
+
toQueryMetaState({
|
|
65
|
+
fetchStatus: 'fetching',
|
|
66
|
+
status: 'pending',
|
|
67
|
+
})
|
|
68
|
+
)
|
|
69
|
+
).toBe(true);
|
|
70
|
+
|
|
71
|
+
expect(
|
|
72
|
+
isQueryLoading(
|
|
73
|
+
toQueryMetaState({
|
|
74
|
+
fetchStatus: 'idle',
|
|
75
|
+
status: 'success',
|
|
76
|
+
})
|
|
77
|
+
)
|
|
78
|
+
).toBe(false);
|
|
79
|
+
});
|
|
80
|
+
});
|
package/src/cache.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type QueryClient,
|
|
3
|
+
} from '@tanstack/query-core';
|
|
4
|
+
|
|
5
|
+
import { type CreateMutation, setupMutation } from './mutation';
|
|
6
|
+
import { type CreateQuery, setupQuery } from './query';
|
|
7
|
+
|
|
8
|
+
export interface CacheApi {
|
|
9
|
+
createMutation: CreateMutation;
|
|
10
|
+
createQuery: CreateQuery;
|
|
11
|
+
cancelQueries: QueryClient['cancelQueries'];
|
|
12
|
+
getQueryData: QueryClient['getQueryData'];
|
|
13
|
+
invalidateQueries: QueryClient['invalidateQueries'];
|
|
14
|
+
refetchQueries: QueryClient['refetchQueries'];
|
|
15
|
+
removeQueries: QueryClient['removeQueries'];
|
|
16
|
+
resetQueries: QueryClient['resetQueries'];
|
|
17
|
+
setQueryData: QueryClient['setQueryData'];
|
|
18
|
+
unsafe_getClient: () => QueryClient;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function setupCache(queryClient: QueryClient): CacheApi {
|
|
22
|
+
return {
|
|
23
|
+
createMutation: setupMutation(queryClient),
|
|
24
|
+
createQuery: setupQuery(queryClient),
|
|
25
|
+
cancelQueries: queryClient.cancelQueries.bind(queryClient),
|
|
26
|
+
getQueryData: queryClient.getQueryData.bind(queryClient),
|
|
27
|
+
invalidateQueries: queryClient.invalidateQueries.bind(queryClient),
|
|
28
|
+
refetchQueries: queryClient.refetchQueries.bind(queryClient),
|
|
29
|
+
removeQueries: queryClient.removeQueries.bind(queryClient),
|
|
30
|
+
resetQueries: queryClient.resetQueries.bind(queryClient),
|
|
31
|
+
setQueryData: queryClient.setQueryData.bind(queryClient),
|
|
32
|
+
unsafe_getClient: () => queryClient,
|
|
33
|
+
};
|
|
34
|
+
}
|
package/src/index.ts
ADDED
package/src/mutation.ts
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import {
|
|
2
|
+
MutationObserver,
|
|
3
|
+
type MutationFunction,
|
|
4
|
+
type MutateOptions,
|
|
5
|
+
type MutationObserverOptions,
|
|
6
|
+
type MutationObserverResult,
|
|
7
|
+
type MutationStatus as TanstackMutationStatus,
|
|
8
|
+
type QueryClient,
|
|
9
|
+
} from '@tanstack/query-core';
|
|
10
|
+
|
|
11
|
+
export type MutationStatus = TanstackMutationStatus;
|
|
12
|
+
|
|
13
|
+
export interface MutationServiceSnapshot<TData = unknown, TError = Error, TVariables = void> {
|
|
14
|
+
data: TData | undefined;
|
|
15
|
+
error: TError | null;
|
|
16
|
+
status: MutationStatus;
|
|
17
|
+
variables: TVariables | undefined;
|
|
18
|
+
isError: boolean;
|
|
19
|
+
isIdle: boolean;
|
|
20
|
+
isPending: boolean;
|
|
21
|
+
isSuccess: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface MutationService<
|
|
25
|
+
TData = unknown,
|
|
26
|
+
TError = Error,
|
|
27
|
+
TVariables = void,
|
|
28
|
+
TOnMutateResult = unknown,
|
|
29
|
+
> {
|
|
30
|
+
getSnapshot: () => MutationServiceSnapshot<TData, TError, TVariables>;
|
|
31
|
+
subscribe: (
|
|
32
|
+
listener: (snapshot: MutationServiceSnapshot<TData, TError, TVariables>) => void
|
|
33
|
+
) => () => void;
|
|
34
|
+
mutate: (
|
|
35
|
+
variables: TVariables,
|
|
36
|
+
options?: MutateOptions<TData, TError, TVariables, TOnMutateResult>
|
|
37
|
+
) => Promise<TData>;
|
|
38
|
+
reset: () => void;
|
|
39
|
+
unsafe_getResult: () => MutationObserverResult<TData, TError, TVariables, TOnMutateResult>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export type MutationServiceOptions<
|
|
43
|
+
TData = unknown,
|
|
44
|
+
TError = Error,
|
|
45
|
+
TVariables = void,
|
|
46
|
+
TOnMutateResult = unknown,
|
|
47
|
+
> = Omit<MutationObserverOptions<TData, TError, TVariables, TOnMutateResult>, 'mutationFn'>;
|
|
48
|
+
|
|
49
|
+
export interface CreateMutation {
|
|
50
|
+
<TData = unknown, TError = Error, TVariables = void, TOnMutateResult = unknown>(
|
|
51
|
+
mutationFn: MutationFunction<TData, TVariables>,
|
|
52
|
+
options?: MutationServiceOptions<TData, TError, TVariables, TOnMutateResult>
|
|
53
|
+
): MutationService<TData, TError, TVariables, TOnMutateResult>;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function setupMutation(queryClient: QueryClient): CreateMutation {
|
|
57
|
+
return function createMutation<
|
|
58
|
+
TData = unknown,
|
|
59
|
+
TError = Error,
|
|
60
|
+
TVariables = void,
|
|
61
|
+
TOnMutateResult = unknown,
|
|
62
|
+
>(
|
|
63
|
+
mutationFn: MutationFunction<TData, TVariables>,
|
|
64
|
+
options?: MutationServiceOptions<TData, TError, TVariables, TOnMutateResult>
|
|
65
|
+
): MutationService<TData, TError, TVariables, TOnMutateResult> {
|
|
66
|
+
const observer = new MutationObserver<TData, TError, TVariables, TOnMutateResult>(
|
|
67
|
+
queryClient,
|
|
68
|
+
{
|
|
69
|
+
...options,
|
|
70
|
+
mutationFn,
|
|
71
|
+
}
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
getSnapshot: () => toMutationServiceSnapshot(observer.getCurrentResult()),
|
|
76
|
+
subscribe: (listener) =>
|
|
77
|
+
observer.subscribe((result) => {
|
|
78
|
+
listener(toMutationServiceSnapshot(result));
|
|
79
|
+
}),
|
|
80
|
+
mutate: (variables, mutateOptions) => observer.mutate(variables, mutateOptions),
|
|
81
|
+
reset: () => observer.reset(),
|
|
82
|
+
unsafe_getResult: () => observer.getCurrentResult(),
|
|
83
|
+
};
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function toMutationServiceSnapshot<TData, TError, TVariables, TOnMutateResult>(
|
|
88
|
+
result: MutationObserverResult<TData, TError, TVariables, TOnMutateResult>
|
|
89
|
+
): MutationServiceSnapshot<TData, TError, TVariables> {
|
|
90
|
+
return {
|
|
91
|
+
data: result.data,
|
|
92
|
+
error: result.error,
|
|
93
|
+
status: result.status,
|
|
94
|
+
variables: result.variables,
|
|
95
|
+
isError: result.isError,
|
|
96
|
+
isIdle: result.isIdle,
|
|
97
|
+
isPending: result.isPending,
|
|
98
|
+
isSuccess: result.isSuccess,
|
|
99
|
+
};
|
|
100
|
+
}
|
package/src/query.ts
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type FetchStatus,
|
|
3
|
+
type InvalidateOptions,
|
|
4
|
+
type InvalidateQueryFilters,
|
|
5
|
+
type QueryClient,
|
|
6
|
+
type QueryFunction,
|
|
7
|
+
type QueryKey,
|
|
8
|
+
QueryObserver,
|
|
9
|
+
type QueryObserverOptions,
|
|
10
|
+
type QueryObserverResult,
|
|
11
|
+
type RefetchOptions,
|
|
12
|
+
type QueryStatus as TanstackQueryStatus,
|
|
13
|
+
} from '@tanstack/query-core';
|
|
14
|
+
|
|
15
|
+
export type QueryFetchStatus = FetchStatus;
|
|
16
|
+
export type QueryStatus = TanstackQueryStatus;
|
|
17
|
+
|
|
18
|
+
export interface QueryServiceSnapshot<TData, TError> {
|
|
19
|
+
data: TData | undefined;
|
|
20
|
+
error: TError | null;
|
|
21
|
+
fetchStatus: QueryFetchStatus;
|
|
22
|
+
status: QueryStatus;
|
|
23
|
+
isError: boolean;
|
|
24
|
+
isFetching: boolean;
|
|
25
|
+
isPending: boolean;
|
|
26
|
+
isSuccess: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface QueryMetaState {
|
|
30
|
+
fetchStatus: QueryFetchStatus;
|
|
31
|
+
status: QueryStatus;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface QueryService<TData, TError> {
|
|
35
|
+
getSnapshot: () => QueryServiceSnapshot<TData, TError>;
|
|
36
|
+
subscribe: (listener: (snapshot: QueryServiceSnapshot<TData, TError>) => void) => () => void;
|
|
37
|
+
refetch: (options?: RefetchOptions) => Promise<QueryServiceSnapshot<TData, TError>>;
|
|
38
|
+
invalidate: (options?: QueryInvalidateOptions) => Promise<void>;
|
|
39
|
+
unsafe_getResult: () => QueryObserverResult<TData, TError>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface QueryInvalidateOptions
|
|
43
|
+
extends Pick<InvalidateOptions, 'cancelRefetch' | 'throwOnError'>,
|
|
44
|
+
Pick<InvalidateQueryFilters, 'refetchType'> {}
|
|
45
|
+
|
|
46
|
+
export interface CreateQuery {
|
|
47
|
+
<
|
|
48
|
+
TQueryFnData = unknown,
|
|
49
|
+
TError = Error,
|
|
50
|
+
TData = TQueryFnData,
|
|
51
|
+
TQueryData = TQueryFnData,
|
|
52
|
+
TQueryKey extends QueryKey = QueryKey,
|
|
53
|
+
>(
|
|
54
|
+
queryKey: TQueryKey,
|
|
55
|
+
queryFn: QueryFunction<TQueryFnData, TQueryKey>,
|
|
56
|
+
options?: QueryServiceOptions<TQueryFnData, TError, TData, TQueryData, TQueryKey>
|
|
57
|
+
): QueryService<TData, TError>;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export type QueryServiceOptions<
|
|
61
|
+
TQueryFnData = unknown,
|
|
62
|
+
TError = Error,
|
|
63
|
+
TData = TQueryFnData,
|
|
64
|
+
TQueryData = TQueryFnData,
|
|
65
|
+
TQueryKey extends QueryKey = QueryKey,
|
|
66
|
+
> = Omit<
|
|
67
|
+
QueryObserverOptions<TQueryFnData, TError, TData, TQueryData, TQueryKey>,
|
|
68
|
+
'queryFn' | 'queryKey'
|
|
69
|
+
>;
|
|
70
|
+
|
|
71
|
+
export function toQueryMetaState<TData, TError>(
|
|
72
|
+
snapshot: Pick<QueryServiceSnapshot<TData, TError>, 'fetchStatus' | 'status'>
|
|
73
|
+
): QueryMetaState {
|
|
74
|
+
return {
|
|
75
|
+
fetchStatus: snapshot.fetchStatus,
|
|
76
|
+
status: snapshot.status,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function isQueryLoading(query: QueryMetaState): boolean {
|
|
81
|
+
return query.status === 'pending' && query.fetchStatus === 'fetching';
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function setupQuery(queryClient: QueryClient): CreateQuery {
|
|
85
|
+
return function createQuery<
|
|
86
|
+
TQueryFnData = unknown,
|
|
87
|
+
TError = Error,
|
|
88
|
+
TData = TQueryFnData,
|
|
89
|
+
TQueryData = TQueryFnData,
|
|
90
|
+
TQueryKey extends QueryKey = QueryKey,
|
|
91
|
+
>(
|
|
92
|
+
queryKey: TQueryKey,
|
|
93
|
+
queryFn: QueryFunction<TQueryFnData, TQueryKey>,
|
|
94
|
+
options?: QueryServiceOptions<TQueryFnData, TError, TData, TQueryData, TQueryKey>
|
|
95
|
+
): QueryService<TData, TError> {
|
|
96
|
+
const observer = new QueryObserver<TQueryFnData, TError, TData, TQueryData, TQueryKey>(
|
|
97
|
+
queryClient,
|
|
98
|
+
{
|
|
99
|
+
...options,
|
|
100
|
+
queryFn,
|
|
101
|
+
queryKey,
|
|
102
|
+
}
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
getSnapshot: () => toQueryServiceSnapshot(observer.getCurrentResult()),
|
|
107
|
+
subscribe: (listener) =>
|
|
108
|
+
observer.subscribe((result) => {
|
|
109
|
+
listener(toQueryServiceSnapshot(result));
|
|
110
|
+
}),
|
|
111
|
+
refetch: async (options) => toQueryServiceSnapshot(await observer.refetch(options)),
|
|
112
|
+
invalidate: (options) =>
|
|
113
|
+
queryClient.invalidateQueries(
|
|
114
|
+
{
|
|
115
|
+
exact: true,
|
|
116
|
+
queryKey,
|
|
117
|
+
...(options?.refetchType === undefined ? {} : { refetchType: options.refetchType }),
|
|
118
|
+
},
|
|
119
|
+
toInvalidateOptions(options)
|
|
120
|
+
),
|
|
121
|
+
unsafe_getResult: () => observer.getCurrentResult(),
|
|
122
|
+
};
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function toQueryServiceSnapshot<TData, TError>(
|
|
127
|
+
result: QueryObserverResult<TData, TError>
|
|
128
|
+
): QueryServiceSnapshot<TData, TError> {
|
|
129
|
+
return {
|
|
130
|
+
data: result.data,
|
|
131
|
+
error: result.error,
|
|
132
|
+
fetchStatus: result.fetchStatus,
|
|
133
|
+
status: result.status,
|
|
134
|
+
isError: result.isError,
|
|
135
|
+
isFetching: result.isFetching,
|
|
136
|
+
isPending: result.isPending,
|
|
137
|
+
isSuccess: result.isSuccess,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function toInvalidateOptions(options?: QueryInvalidateOptions): InvalidateOptions | undefined {
|
|
142
|
+
if (options === undefined) {
|
|
143
|
+
return undefined;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const invalidateOptions: InvalidateOptions = {
|
|
147
|
+
...(options.cancelRefetch === undefined ? {} : { cancelRefetch: options.cancelRefetch }),
|
|
148
|
+
...(options.throwOnError === undefined ? {} : { throwOnError: options.throwOnError }),
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
return Object.keys(invalidateOptions).length > 0 ? invalidateOptions : undefined;
|
|
152
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"allowSyntheticDefaultImports": true,
|
|
4
|
+
"declaration": true,
|
|
5
|
+
"declarationDir": "dist",
|
|
6
|
+
"esModuleInterop": true,
|
|
7
|
+
"lib": [
|
|
8
|
+
"es2022"
|
|
9
|
+
],
|
|
10
|
+
"module": "es2022",
|
|
11
|
+
"moduleResolution": "bundler",
|
|
12
|
+
"outDir": "dist",
|
|
13
|
+
"skipLibCheck": true,
|
|
14
|
+
"sourceMap": true,
|
|
15
|
+
"strict": true,
|
|
16
|
+
"target": "es2022",
|
|
17
|
+
"typeRoots": [
|
|
18
|
+
"../../node_modules/@types"
|
|
19
|
+
]
|
|
20
|
+
},
|
|
21
|
+
"include": [
|
|
22
|
+
"src/**/*.ts"
|
|
23
|
+
],
|
|
24
|
+
"exclude": [
|
|
25
|
+
"coverage",
|
|
26
|
+
"dist",
|
|
27
|
+
"node_modules",
|
|
28
|
+
"src/**/__tests__/**",
|
|
29
|
+
"src/**/*.mock.ts",
|
|
30
|
+
"src/**/*.{spec,test}.ts"
|
|
31
|
+
]
|
|
32
|
+
}
|