@tuyau/query-core 1.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/LICENSE.md ADDED
@@ -0,0 +1,9 @@
1
+ # The MIT License
2
+
3
+ Copyright (c) 2023
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
+
7
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
+
9
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,174 @@
1
+ import { QueryFilters, SkipToken, skipToken, QueryFunctionContext } from '@tanstack/query-core';
2
+ import { RawRequestArgs, BaseRequestOptions } from '@tuyau/core/types';
3
+ import { Tuyau } from '@tuyau/core/client';
4
+
5
+ /**
6
+ * Discriminator for the type of TanStack Query operation.
7
+ * Used in query keys to differentiate between regular queries,
8
+ * infinite queries, and catch-all path filters
9
+ */
10
+ type QueryType = 'any' | 'infinite' | 'query';
11
+ /**
12
+ * Structured query key used by Tuyau's TanStack Query integration.
13
+ * First element is the route path segments, second is an optional
14
+ * metadata object containing the request arguments and query type
15
+ */
16
+ type TuyauQueryKey = [
17
+ readonly string[],
18
+ {
19
+ request?: RawRequestArgs<any>;
20
+ type?: Exclude<QueryType, 'any'>;
21
+ }?
22
+ ];
23
+ /**
24
+ * Structured mutation key used by Tuyau's TanStack Query integration.
25
+ * Contains only the route path segments since mutations don't need
26
+ * request-based differentiation
27
+ */
28
+ type TuyauMutationKey = [readonly string[]];
29
+ /**
30
+ * Options forwarded to the underlying Ky HTTP client, extended
31
+ * with `abortOnUnmount` to automatically abort requests when
32
+ * the component using the query unmounts
33
+ */
34
+ interface TuyauRequestOptions extends BaseRequestOptions {
35
+ abortOnUnmount?: boolean;
36
+ }
37
+ /**
38
+ * Base options shared by all Tuyau query/mutation option builders.
39
+ * Allows passing Ky-specific options via the `tuyau` property
40
+ */
41
+ interface TuyauQueryBaseOptions {
42
+ tuyau?: TuyauRequestOptions;
43
+ }
44
+ /**
45
+ * Methods available on every node of the Tuyau query client proxy tree.
46
+ * Provides path-level query key and filter generation for cache operations
47
+ * like invalidation or removal across all query types for a given route
48
+ */
49
+ interface DecorateRouterKeyable {
50
+ pathKey: () => TuyauQueryKey;
51
+ pathFilter: (filters?: QueryFilters<TuyauQueryKey>) => WithRequired<QueryFilters<TuyauQueryKey>, 'queryKey'>;
52
+ }
53
+ /**
54
+ * Generic function type helper
55
+ */
56
+ type Fn<Result = void, Args extends unknown[] = unknown[]> = (...args: Args) => Result;
57
+ /**
58
+ * Like `Omit` but distributes over union types.
59
+ * Standard `Omit` collapses unions, this preserves each union member
60
+ */
61
+ type DistributiveOmit<TObj, TKey extends keyof any> = TObj extends any ? Omit<TObj, TKey> : never;
62
+ /**
63
+ * Makes specific keys of a type required while keeping the rest unchanged
64
+ */
65
+ type WithRequired<T, K extends keyof T> = T & Required<Pick<T, K>>;
66
+
67
+ /**
68
+ * Type guard that checks if a value is a plain object
69
+ * (not null, not an array)
70
+ */
71
+ declare function isObject(value: unknown): value is Record<string, unknown>;
72
+ /**
73
+ * Immediately invokes a function and returns its result.
74
+ * Used to create query/mutation functions inline with
75
+ * proper skip token handling
76
+ */
77
+ declare function invoke(fn: undefined): undefined;
78
+ declare function invoke<T>(fn: Fn<T>): T;
79
+ /**
80
+ * Builds a structured TanStack Query key from route segments,
81
+ * request arguments, and query type.
82
+ *
83
+ * For infinite queries, automatically strips `cursor` and `direction`
84
+ * from query/body params to ensure stable cache keys across pages
85
+ */
86
+ declare function buildKey(opts: {
87
+ segments: string[];
88
+ request?: unknown;
89
+ type: QueryType;
90
+ }): TuyauQueryKey;
91
+ /**
92
+ * Extracts Ky-compatible HTTP options from Tuyau request options,
93
+ * filtering out Tuyau-specific properties like `abortOnUnmount`
94
+ */
95
+ declare function extractKyOptions(tuyauOpts?: TuyauRequestOptions): Omit<TuyauRequestOptions, 'abortOnUnmount'>;
96
+
97
+ /**
98
+ * Converts a camelCase string to snake_case.
99
+ * Used to transform proxy property names (e.g. `getArticles`)
100
+ * into AdonisJS route name segments (e.g. `get_articles`)
101
+ */
102
+ declare function toSnakeCase(str: string): string;
103
+ /**
104
+ * Joins an array of camelCase segments into a dot-separated
105
+ * snake_case route name that matches AdonisJS conventions.
106
+ * e.g. `['users', 'getProfile']` → `'users.get_profile'`
107
+ */
108
+ declare function segmentsToRouteName(segments: string[]): string;
109
+
110
+ /**
111
+ * Internal options for building a query function
112
+ */
113
+ interface CreateQueryFnOptions {
114
+ request: RawRequestArgs<any> | SkipToken;
115
+ opts?: TuyauQueryBaseOptions;
116
+ routeName: string;
117
+ client: Tuyau<any>;
118
+ globalOptions?: TuyauRequestOptions;
119
+ }
120
+ /**
121
+ * Creates a TanStack Query `queryFn` from Tuyau route information.
122
+ * Returns `skipToken` when the request is a skip token (conditional queries).
123
+ * Handles abort-on-unmount and Ky options extraction.
124
+ * Ky retries are disabled (retry: 0) to let TanStack Query handle retries
125
+ */
126
+ declare function createQueryFn(options: CreateQueryFnOptions): typeof skipToken | ((queryFnContext: QueryFunctionContext) => Promise<any>);
127
+
128
+ /**
129
+ * Internal options for building an infinite query function.
130
+ * The `opts` parameter accepts any object with optional `tuyau` and
131
+ * `pageParamKey` properties — this avoids coupling to framework-specific
132
+ * option types (React vs Vue) while still accessing the needed values
133
+ */
134
+ interface CreateInfiniteQueryFnOptions {
135
+ request: RawRequestArgs<any> | SkipToken;
136
+ opts?: {
137
+ tuyau?: TuyauRequestOptions;
138
+ pageParamKey?: string;
139
+ [key: string]: any;
140
+ };
141
+ routeName: string;
142
+ client: Tuyau<any>;
143
+ globalOptions?: TuyauRequestOptions;
144
+ }
145
+ /**
146
+ * Creates a TanStack Query `queryFn` for infinite queries.
147
+ * Injects the page parameter into the request query using the
148
+ * configured `pageParamKey` (defaults to `'page'`).
149
+ * Returns `skipToken` when the request is a skip token.
150
+ * Ky retries are disabled (retry: 0) to let TanStack Query handle retries
151
+ */
152
+ declare function createInfiniteQueryFn(options: CreateInfiniteQueryFnOptions): typeof skipToken | ((queryFnContext: QueryFunctionContext) => Promise<any>);
153
+
154
+ /**
155
+ * Builds a mutation key from route path segments
156
+ */
157
+ declare function getMutationKeyInternal(options: {
158
+ segments: string[];
159
+ }): TuyauMutationKey;
160
+ /**
161
+ * Internal options for building a mutation function
162
+ */
163
+ interface CreateMutationFnOptions {
164
+ opts?: TuyauQueryBaseOptions;
165
+ routeName: string;
166
+ client: Tuyau<any>;
167
+ }
168
+ /**
169
+ * Creates a TanStack Query `mutationFn` from Tuyau route information.
170
+ * Ky retries are disabled (retry: 0) to let TanStack Query handle retries
171
+ */
172
+ declare function createMutationFn(options: CreateMutationFnOptions): (request: any) => Promise<any>;
173
+
174
+ export { type CreateInfiniteQueryFnOptions, type CreateMutationFnOptions, type CreateQueryFnOptions, type DecorateRouterKeyable, type DistributiveOmit, type Fn, type QueryType, type TuyauMutationKey, type TuyauQueryBaseOptions, type TuyauQueryKey, type TuyauRequestOptions, type WithRequired, buildKey, createInfiniteQueryFn, createMutationFn, createQueryFn, extractKyOptions, getMutationKeyInternal, invoke, isObject, segmentsToRouteName, toSnakeCase };
package/build/index.js ADDED
@@ -0,0 +1,135 @@
1
+ // src/utils.ts
2
+ import { skipToken } from "@tanstack/query-core";
3
+ function isObject(value) {
4
+ return typeof value === "object" && value !== null && !Array.isArray(value);
5
+ }
6
+ function invoke(fn) {
7
+ return fn?.();
8
+ }
9
+ function buildKey(opts) {
10
+ const { segments, request, type } = opts;
11
+ const splitPath = segments.flatMap((part) => part.split("."));
12
+ if (!request && type === "any") {
13
+ return splitPath.length ? [splitPath] : [];
14
+ }
15
+ if (type === "infinite" && isObject(request)) {
16
+ const query = request.query;
17
+ const body = request.body;
18
+ const hasCursorInQuery = isObject(query) && ("direction" in query || "cursor" in query);
19
+ const hasCursorInBody = isObject(body) && ("direction" in body || "cursor" in body);
20
+ if (hasCursorInQuery || hasCursorInBody) {
21
+ const cleanedRequest = { ...request };
22
+ if (hasCursorInQuery && isObject(query)) {
23
+ const { cursor: _, direction: __, ...cleanQuery } = query;
24
+ cleanedRequest.query = cleanQuery;
25
+ }
26
+ if (hasCursorInBody && isObject(body)) {
27
+ const { cursor: _, direction: __, ...cleanBody } = body;
28
+ cleanedRequest.body = cleanBody;
29
+ }
30
+ return [splitPath, { request: cleanedRequest, type: "infinite" }];
31
+ }
32
+ }
33
+ return [
34
+ splitPath,
35
+ {
36
+ ...typeof request !== "undefined" && request !== skipToken && { request },
37
+ ...type && type !== "any" && { type }
38
+ }
39
+ ];
40
+ }
41
+ function extractKyOptions(tuyauOpts) {
42
+ if (!tuyauOpts) return {};
43
+ const { abortOnUnmount, ...kyOptions } = tuyauOpts;
44
+ return kyOptions;
45
+ }
46
+
47
+ // src/helpers.ts
48
+ function toSnakeCase(str) {
49
+ return str.replace(/([a-z])([A-Z])/g, "$1_$2").toLowerCase();
50
+ }
51
+ function segmentsToRouteName(segments) {
52
+ return segments.map(toSnakeCase).join(".");
53
+ }
54
+
55
+ // src/query_fn.ts
56
+ import { skipToken as skipToken2 } from "@tanstack/query-core";
57
+ function createQueryFn(options) {
58
+ const { request, routeName, opts, client, globalOptions } = options;
59
+ return invoke(() => {
60
+ if (request === skipToken2) return skipToken2;
61
+ return async (queryFnContext) => {
62
+ const effectiveAbortOnUnmount = opts?.tuyau?.abortOnUnmount ?? globalOptions?.abortOnUnmount ?? false;
63
+ const kyOptions = extractKyOptions(opts?.tuyau);
64
+ return await client.request(routeName, {
65
+ ...request,
66
+ ...kyOptions,
67
+ retry: 0,
68
+ ...effectiveAbortOnUnmount ? { signal: queryFnContext.signal } : {}
69
+ });
70
+ };
71
+ });
72
+ }
73
+
74
+ // src/infinite_query_fn.ts
75
+ import { skipToken as skipToken3 } from "@tanstack/query-core";
76
+ function createInfiniteQueryFn(options) {
77
+ const { request, routeName, opts, client, globalOptions } = options;
78
+ return invoke(() => {
79
+ if (request === skipToken3) return skipToken3;
80
+ return async (queryFnContext) => {
81
+ const { pageParam } = queryFnContext;
82
+ const effectiveAbortOnUnmount = opts?.tuyau?.abortOnUnmount ?? globalOptions?.abortOnUnmount ?? false;
83
+ const kyOptions = extractKyOptions(opts?.tuyau);
84
+ const pageParamKey = opts?.pageParamKey || "page";
85
+ let requestArgs;
86
+ if (isObject(request)) {
87
+ if (pageParam !== void 0) {
88
+ requestArgs = {
89
+ ...request,
90
+ query: { ...request.query, [pageParamKey]: pageParam }
91
+ };
92
+ } else {
93
+ requestArgs = request;
94
+ }
95
+ } else {
96
+ requestArgs = { query: pageParam !== void 0 ? { [pageParamKey]: pageParam } : {} };
97
+ }
98
+ return await client.request(routeName, {
99
+ ...requestArgs,
100
+ ...kyOptions,
101
+ retry: 0,
102
+ ...effectiveAbortOnUnmount ? { signal: queryFnContext.signal } : {}
103
+ });
104
+ };
105
+ });
106
+ }
107
+
108
+ // src/mutation_fn.ts
109
+ function getMutationKeyInternal(options) {
110
+ const key = [options.segments.flatMap((part) => part.split("."))];
111
+ return key;
112
+ }
113
+ function createMutationFn(options) {
114
+ return async (request) => {
115
+ const kyOptions = extractKyOptions(options.opts?.tuyau);
116
+ const requestArgs = request || {};
117
+ return await options.client.request(options.routeName, {
118
+ ...requestArgs,
119
+ ...kyOptions,
120
+ retry: 0
121
+ });
122
+ };
123
+ }
124
+ export {
125
+ buildKey,
126
+ createInfiniteQueryFn,
127
+ createMutationFn,
128
+ createQueryFn,
129
+ extractKyOptions,
130
+ getMutationKeyInternal,
131
+ invoke,
132
+ isObject,
133
+ segmentsToRouteName,
134
+ toSnakeCase
135
+ };
package/package.json ADDED
@@ -0,0 +1,64 @@
1
+ {
2
+ "name": "@tuyau/query-core",
3
+ "type": "module",
4
+ "version": "1.1.0",
5
+ "description": "Shared utilities for Tuyau Tanstack Query integrations",
6
+ "author": "Julien Ripouteau <julien@ripouteau.com>",
7
+ "license": "MIT",
8
+ "homepage": "https://github.com/Julien-R44/tuyau#readme",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/Julien-R44/tuyau.git",
12
+ "directory": "packages/query-core"
13
+ },
14
+ "bugs": {
15
+ "url": "https://github.com/Julien-R44/tuyau/issues"
16
+ },
17
+ "keywords": [
18
+ "adonisjs",
19
+ "typescript",
20
+ "typesafe",
21
+ "tanstack-query",
22
+ "tuyau"
23
+ ],
24
+ "exports": {
25
+ ".": "./build/index.js"
26
+ },
27
+ "main": "build/index.js",
28
+ "files": [
29
+ "build"
30
+ ],
31
+ "engines": {
32
+ "node": ">=24.0.0"
33
+ },
34
+ "peerDependencies": {
35
+ "@tanstack/query-core": "^5.74.7",
36
+ "@tuyau/core": "^1.0.0-beta.9"
37
+ },
38
+ "devDependencies": {
39
+ "@tanstack/query-core": "^5.74.7",
40
+ "@tuyau/core": "1.2.1"
41
+ },
42
+ "tsup": {
43
+ "entry": [
44
+ "./src/index.ts"
45
+ ],
46
+ "outDir": "./build",
47
+ "clean": true,
48
+ "format": "esm",
49
+ "dts": true,
50
+ "target": "esnext"
51
+ },
52
+ "publishConfig": {
53
+ "access": "public",
54
+ "tag": "latest"
55
+ },
56
+ "scripts": {
57
+ "lint": "oxlint .",
58
+ "typecheck": "tsc --noEmit",
59
+ "build": "tsup-node",
60
+ "test": "pnpm quick:test",
61
+ "quick:test": "node --import=@poppinss/ts-exec --enable-source-maps bin/test.ts --force-exit",
62
+ "checks": "pnpm lint && pnpm typecheck"
63
+ }
64
+ }