naystack 1.8.0 → 1.8.2
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/dist/graphql/client.cjs.js +3 -2
- package/dist/graphql/client.d.mts +7 -3
- package/dist/graphql/client.d.ts +7 -3
- package/dist/graphql/client.esm.js +3 -2
- package/dist/graphql/index.cjs.js +25 -9
- package/dist/graphql/index.esm.js +25 -9
- package/dist/graphql/utils.cjs.js +25 -9
- package/dist/graphql/utils.d.mts +47 -8
- package/dist/graphql/utils.d.ts +47 -8
- package/dist/graphql/utils.esm.js +25 -9
- package/package.json +1 -1
|
@@ -115,9 +115,10 @@ var tokenContext = (token) => {
|
|
|
115
115
|
credentials: `omit`
|
|
116
116
|
};
|
|
117
117
|
};
|
|
118
|
-
function useAuthQuery(query, variables) {
|
|
118
|
+
function useAuthQuery(query, variables, options) {
|
|
119
119
|
const [fetch, result] = (0, import_client.useLazyQuery)(query, {
|
|
120
|
-
fetchPolicy: "no-cache"
|
|
120
|
+
fetchPolicy: "no-cache",
|
|
121
|
+
...options
|
|
121
122
|
});
|
|
122
123
|
const prevVarsRef = (0, import_react.useRef)(null);
|
|
123
124
|
(0, import_react.useEffect)(() => {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as _apollo_client from '@apollo/client';
|
|
2
|
-
import { InMemoryCacheConfig, OperationVariables, MutationHookOptions, ApolloClient } from '@apollo/client';
|
|
2
|
+
import { InMemoryCacheConfig, OperationVariables, LazyQueryHookOptions, MutationHookOptions, ApolloClient } from '@apollo/client';
|
|
3
3
|
import { TypedDocumentNode } from '@graphql-typed-document-node/core';
|
|
4
4
|
import React__default, { PropsWithChildren } from 'react';
|
|
5
5
|
|
|
@@ -25,10 +25,14 @@ declare const tokenContext: (token?: string | null) => {
|
|
|
25
25
|
* Hook to run a GraphQL query with the current user's token. The query auto-fires when both the token
|
|
26
26
|
* and variables are available. Returns a refetch function and the Apollo query result.
|
|
27
27
|
*
|
|
28
|
-
* Uses `fetchPolicy
|
|
28
|
+
* Uses Apollo's default `fetchPolicy` (`cache-first`) unless you override it via
|
|
29
|
+
* `options`. Pass `{ fetchPolicy: "no-cache" }` for always-fresh, viewer-keyed data
|
|
30
|
+
* (e.g. the current user), or `{ fetchPolicy: "cache-and-network" }` to show cached
|
|
31
|
+
* data instantly while refetching.
|
|
29
32
|
*
|
|
30
33
|
* @param query - A `TypedDocumentNode` for the query (e.g. from codegen or a `gql` template).
|
|
31
34
|
* @param variables - Optional initial variables (the `input` value). Automatically wrapped as `{ input: variables }` before sending. Query fires automatically when this and token are set; change to refetch.
|
|
35
|
+
* @param options - Optional Apollo `LazyQueryHookOptions` (e.g. `fetchPolicy`, `notifyOnNetworkStatusChange`). Merged into the underlying `useLazyQuery`.
|
|
32
36
|
* @returns Tuple: `[refetch, result]`.
|
|
33
37
|
* - `refetch(input)` — runs the query again with the given input (wrapped as `variables.input`).
|
|
34
38
|
* - `result` — `{ data, loading, error, hasAuth }` from Apollo. `hasAuth` is `true` when an auth token is available.
|
|
@@ -58,7 +62,7 @@ declare const tokenContext: (token?: string | null) => {
|
|
|
58
62
|
*
|
|
59
63
|
* @category GraphQL
|
|
60
64
|
*/
|
|
61
|
-
declare function useAuthQuery<T, V extends OperationVariables>(query: TypedDocumentNode<T, V>, variables?: V["input"]): readonly [(input?: V["input"]) => Promise<_apollo_client.QueryResult<T, V>>, _apollo_client.QueryResult<T, V>];
|
|
65
|
+
declare function useAuthQuery<T, V extends OperationVariables>(query: TypedDocumentNode<T, V>, variables?: V["input"], options?: LazyQueryHookOptions<T, V>): readonly [(input?: V["input"]) => Promise<_apollo_client.QueryResult<T, V>>, _apollo_client.QueryResult<T, V>];
|
|
62
66
|
/**
|
|
63
67
|
* Hook to run a GraphQL mutation with the current user's token. Returns a function you call with the mutation input.
|
|
64
68
|
*
|
package/dist/graphql/client.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as _apollo_client from '@apollo/client';
|
|
2
|
-
import { InMemoryCacheConfig, OperationVariables, MutationHookOptions, ApolloClient } from '@apollo/client';
|
|
2
|
+
import { InMemoryCacheConfig, OperationVariables, LazyQueryHookOptions, MutationHookOptions, ApolloClient } from '@apollo/client';
|
|
3
3
|
import { TypedDocumentNode } from '@graphql-typed-document-node/core';
|
|
4
4
|
import React__default, { PropsWithChildren } from 'react';
|
|
5
5
|
|
|
@@ -25,10 +25,14 @@ declare const tokenContext: (token?: string | null) => {
|
|
|
25
25
|
* Hook to run a GraphQL query with the current user's token. The query auto-fires when both the token
|
|
26
26
|
* and variables are available. Returns a refetch function and the Apollo query result.
|
|
27
27
|
*
|
|
28
|
-
* Uses `fetchPolicy
|
|
28
|
+
* Uses Apollo's default `fetchPolicy` (`cache-first`) unless you override it via
|
|
29
|
+
* `options`. Pass `{ fetchPolicy: "no-cache" }` for always-fresh, viewer-keyed data
|
|
30
|
+
* (e.g. the current user), or `{ fetchPolicy: "cache-and-network" }` to show cached
|
|
31
|
+
* data instantly while refetching.
|
|
29
32
|
*
|
|
30
33
|
* @param query - A `TypedDocumentNode` for the query (e.g. from codegen or a `gql` template).
|
|
31
34
|
* @param variables - Optional initial variables (the `input` value). Automatically wrapped as `{ input: variables }` before sending. Query fires automatically when this and token are set; change to refetch.
|
|
35
|
+
* @param options - Optional Apollo `LazyQueryHookOptions` (e.g. `fetchPolicy`, `notifyOnNetworkStatusChange`). Merged into the underlying `useLazyQuery`.
|
|
32
36
|
* @returns Tuple: `[refetch, result]`.
|
|
33
37
|
* - `refetch(input)` — runs the query again with the given input (wrapped as `variables.input`).
|
|
34
38
|
* - `result` — `{ data, loading, error, hasAuth }` from Apollo. `hasAuth` is `true` when an auth token is available.
|
|
@@ -58,7 +62,7 @@ declare const tokenContext: (token?: string | null) => {
|
|
|
58
62
|
*
|
|
59
63
|
* @category GraphQL
|
|
60
64
|
*/
|
|
61
|
-
declare function useAuthQuery<T, V extends OperationVariables>(query: TypedDocumentNode<T, V>, variables?: V["input"]): readonly [(input?: V["input"]) => Promise<_apollo_client.QueryResult<T, V>>, _apollo_client.QueryResult<T, V>];
|
|
65
|
+
declare function useAuthQuery<T, V extends OperationVariables>(query: TypedDocumentNode<T, V>, variables?: V["input"], options?: LazyQueryHookOptions<T, V>): readonly [(input?: V["input"]) => Promise<_apollo_client.QueryResult<T, V>>, _apollo_client.QueryResult<T, V>];
|
|
62
66
|
/**
|
|
63
67
|
* Hook to run a GraphQL mutation with the current user's token. Returns a function you call with the mutation input.
|
|
64
68
|
*
|
|
@@ -90,9 +90,10 @@ var tokenContext = (token) => {
|
|
|
90
90
|
credentials: `omit`
|
|
91
91
|
};
|
|
92
92
|
};
|
|
93
|
-
function useAuthQuery(query, variables) {
|
|
93
|
+
function useAuthQuery(query, variables, options) {
|
|
94
94
|
const [fetch, result] = useLazyQuery(query, {
|
|
95
|
-
fetchPolicy: "no-cache"
|
|
95
|
+
fetchPolicy: "no-cache",
|
|
96
|
+
...options
|
|
96
97
|
});
|
|
97
98
|
const prevVarsRef = useRef(null);
|
|
98
99
|
useEffect(() => {
|
|
@@ -770,8 +770,16 @@ function resolver(fn, options) {
|
|
|
770
770
|
return {
|
|
771
771
|
...options,
|
|
772
772
|
fn,
|
|
773
|
-
authCall: getAuthCaller(
|
|
774
|
-
|
|
773
|
+
authCall: getAuthCaller(
|
|
774
|
+
fn,
|
|
775
|
+
options.authorized
|
|
776
|
+
),
|
|
777
|
+
call: options.authorized ? getAuthCaller(
|
|
778
|
+
fn,
|
|
779
|
+
options.authorized
|
|
780
|
+
) : getCaller(
|
|
781
|
+
fn
|
|
782
|
+
)
|
|
775
783
|
};
|
|
776
784
|
}
|
|
777
785
|
var getUserId = async () => {
|
|
@@ -779,11 +787,13 @@ var getUserId = async () => {
|
|
|
779
787
|
const refresh = Cookie.get(REFRESH_COOKIE_NAME)?.value;
|
|
780
788
|
return refresh ? getUserIdFromRefreshToken(refresh) : null;
|
|
781
789
|
};
|
|
782
|
-
function getAuthCaller(fn) {
|
|
790
|
+
function getAuthCaller(fn, authorized) {
|
|
783
791
|
return (0, import_react.cache)(
|
|
784
792
|
async (data) => {
|
|
793
|
+
const userId = await getUserId();
|
|
794
|
+
if (authorized && !userId) return null;
|
|
785
795
|
const ctx = {
|
|
786
|
-
userId
|
|
796
|
+
userId,
|
|
787
797
|
isRefreshID: true
|
|
788
798
|
};
|
|
789
799
|
return await fn(ctx, data);
|
|
@@ -801,11 +811,13 @@ function getCaller(fn) {
|
|
|
801
811
|
}
|
|
802
812
|
);
|
|
803
813
|
}
|
|
804
|
-
function getFieldAuthCaller(fn) {
|
|
814
|
+
function getFieldAuthCaller(fn, authorized) {
|
|
805
815
|
return (0, import_react.cache)(
|
|
806
816
|
async (root, data) => {
|
|
817
|
+
const userId = await getUserId();
|
|
818
|
+
if (authorized && !userId) return null;
|
|
807
819
|
const ctx = {
|
|
808
|
-
userId
|
|
820
|
+
userId,
|
|
809
821
|
isRefreshID: true
|
|
810
822
|
};
|
|
811
823
|
return await fn(root, ctx, data);
|
|
@@ -827,8 +839,8 @@ function field(fn, options) {
|
|
|
827
839
|
return {
|
|
828
840
|
...options,
|
|
829
841
|
fn,
|
|
830
|
-
authCall: getFieldAuthCaller(fn),
|
|
831
|
-
call: options.authorized ? getFieldAuthCaller(fn) : getFieldCaller(
|
|
842
|
+
authCall: getFieldAuthCaller(fn, options.authorized),
|
|
843
|
+
call: options.authorized ? getFieldAuthCaller(fn, options.authorized) : getFieldCaller(
|
|
832
844
|
fn
|
|
833
845
|
)
|
|
834
846
|
};
|
|
@@ -903,8 +915,12 @@ function FieldLibrary(type, queries) {
|
|
|
903
915
|
const def = queries[key];
|
|
904
916
|
if (!def) continue;
|
|
905
917
|
Object.defineProperty(GeneratedResolver.prototype, key, {
|
|
918
|
+
// `alwaysResolve` fields must run `fn` even when the parent pre-hydrated
|
|
919
|
+
// `root[key]`: they gate on `ctx`, and the short-circuit would return the
|
|
920
|
+
// pre-hydrated value without ever applying that check. Such a resolver is
|
|
921
|
+
// responsible for reusing `root[key]` itself, so the N+1 win is preserved.
|
|
906
922
|
value: async function(root, ctx, input) {
|
|
907
|
-
if (root[key]) return root[key];
|
|
923
|
+
if (!def.alwaysResolve && root[key]) return root[key];
|
|
908
924
|
return def.fn(root, ctx, input);
|
|
909
925
|
},
|
|
910
926
|
writable: false
|
|
@@ -764,8 +764,16 @@ function resolver(fn, options) {
|
|
|
764
764
|
return {
|
|
765
765
|
...options,
|
|
766
766
|
fn,
|
|
767
|
-
authCall: getAuthCaller(
|
|
768
|
-
|
|
767
|
+
authCall: getAuthCaller(
|
|
768
|
+
fn,
|
|
769
|
+
options.authorized
|
|
770
|
+
),
|
|
771
|
+
call: options.authorized ? getAuthCaller(
|
|
772
|
+
fn,
|
|
773
|
+
options.authorized
|
|
774
|
+
) : getCaller(
|
|
775
|
+
fn
|
|
776
|
+
)
|
|
769
777
|
};
|
|
770
778
|
}
|
|
771
779
|
var getUserId = async () => {
|
|
@@ -773,11 +781,13 @@ var getUserId = async () => {
|
|
|
773
781
|
const refresh = Cookie.get(REFRESH_COOKIE_NAME)?.value;
|
|
774
782
|
return refresh ? getUserIdFromRefreshToken(refresh) : null;
|
|
775
783
|
};
|
|
776
|
-
function getAuthCaller(fn) {
|
|
784
|
+
function getAuthCaller(fn, authorized) {
|
|
777
785
|
return cache(
|
|
778
786
|
async (data) => {
|
|
787
|
+
const userId = await getUserId();
|
|
788
|
+
if (authorized && !userId) return null;
|
|
779
789
|
const ctx = {
|
|
780
|
-
userId
|
|
790
|
+
userId,
|
|
781
791
|
isRefreshID: true
|
|
782
792
|
};
|
|
783
793
|
return await fn(ctx, data);
|
|
@@ -795,11 +805,13 @@ function getCaller(fn) {
|
|
|
795
805
|
}
|
|
796
806
|
);
|
|
797
807
|
}
|
|
798
|
-
function getFieldAuthCaller(fn) {
|
|
808
|
+
function getFieldAuthCaller(fn, authorized) {
|
|
799
809
|
return cache(
|
|
800
810
|
async (root, data) => {
|
|
811
|
+
const userId = await getUserId();
|
|
812
|
+
if (authorized && !userId) return null;
|
|
801
813
|
const ctx = {
|
|
802
|
-
userId
|
|
814
|
+
userId,
|
|
803
815
|
isRefreshID: true
|
|
804
816
|
};
|
|
805
817
|
return await fn(root, ctx, data);
|
|
@@ -821,8 +833,8 @@ function field(fn, options) {
|
|
|
821
833
|
return {
|
|
822
834
|
...options,
|
|
823
835
|
fn,
|
|
824
|
-
authCall: getFieldAuthCaller(fn),
|
|
825
|
-
call: options.authorized ? getFieldAuthCaller(fn) : getFieldCaller(
|
|
836
|
+
authCall: getFieldAuthCaller(fn, options.authorized),
|
|
837
|
+
call: options.authorized ? getFieldAuthCaller(fn, options.authorized) : getFieldCaller(
|
|
826
838
|
fn
|
|
827
839
|
)
|
|
828
840
|
};
|
|
@@ -897,8 +909,12 @@ function FieldLibrary(type, queries) {
|
|
|
897
909
|
const def = queries[key];
|
|
898
910
|
if (!def) continue;
|
|
899
911
|
Object.defineProperty(GeneratedResolver.prototype, key, {
|
|
912
|
+
// `alwaysResolve` fields must run `fn` even when the parent pre-hydrated
|
|
913
|
+
// `root[key]`: they gate on `ctx`, and the short-circuit would return the
|
|
914
|
+
// pre-hydrated value without ever applying that check. Such a resolver is
|
|
915
|
+
// responsible for reusing `root[key]` itself, so the N+1 win is preserved.
|
|
900
916
|
value: async function(root, ctx, input) {
|
|
901
|
-
if (root[key]) return root[key];
|
|
917
|
+
if (!def.alwaysResolve && root[key]) return root[key];
|
|
902
918
|
return def.fn(root, ctx, input);
|
|
903
919
|
},
|
|
904
920
|
writable: false
|
|
@@ -117,8 +117,16 @@ function resolver(fn, options) {
|
|
|
117
117
|
return {
|
|
118
118
|
...options,
|
|
119
119
|
fn,
|
|
120
|
-
authCall: getAuthCaller(
|
|
121
|
-
|
|
120
|
+
authCall: getAuthCaller(
|
|
121
|
+
fn,
|
|
122
|
+
options.authorized
|
|
123
|
+
),
|
|
124
|
+
call: options.authorized ? getAuthCaller(
|
|
125
|
+
fn,
|
|
126
|
+
options.authorized
|
|
127
|
+
) : getCaller(
|
|
128
|
+
fn
|
|
129
|
+
)
|
|
122
130
|
};
|
|
123
131
|
}
|
|
124
132
|
var getUserId = async () => {
|
|
@@ -126,11 +134,13 @@ var getUserId = async () => {
|
|
|
126
134
|
const refresh = Cookie.get(REFRESH_COOKIE_NAME)?.value;
|
|
127
135
|
return refresh ? getUserIdFromRefreshToken(refresh) : null;
|
|
128
136
|
};
|
|
129
|
-
function getAuthCaller(fn) {
|
|
137
|
+
function getAuthCaller(fn, authorized) {
|
|
130
138
|
return (0, import_react.cache)(
|
|
131
139
|
async (data) => {
|
|
140
|
+
const userId = await getUserId();
|
|
141
|
+
if (authorized && !userId) return null;
|
|
132
142
|
const ctx = {
|
|
133
|
-
userId
|
|
143
|
+
userId,
|
|
134
144
|
isRefreshID: true
|
|
135
145
|
};
|
|
136
146
|
return await fn(ctx, data);
|
|
@@ -148,11 +158,13 @@ function getCaller(fn) {
|
|
|
148
158
|
}
|
|
149
159
|
);
|
|
150
160
|
}
|
|
151
|
-
function getFieldAuthCaller(fn) {
|
|
161
|
+
function getFieldAuthCaller(fn, authorized) {
|
|
152
162
|
return (0, import_react.cache)(
|
|
153
163
|
async (root, data) => {
|
|
164
|
+
const userId = await getUserId();
|
|
165
|
+
if (authorized && !userId) return null;
|
|
154
166
|
const ctx = {
|
|
155
|
-
userId
|
|
167
|
+
userId,
|
|
156
168
|
isRefreshID: true
|
|
157
169
|
};
|
|
158
170
|
return await fn(root, ctx, data);
|
|
@@ -174,8 +186,8 @@ function field(fn, options) {
|
|
|
174
186
|
return {
|
|
175
187
|
...options,
|
|
176
188
|
fn,
|
|
177
|
-
authCall: getFieldAuthCaller(fn),
|
|
178
|
-
call: options.authorized ? getFieldAuthCaller(fn) : getFieldCaller(
|
|
189
|
+
authCall: getFieldAuthCaller(fn, options.authorized),
|
|
190
|
+
call: options.authorized ? getFieldAuthCaller(fn, options.authorized) : getFieldCaller(
|
|
179
191
|
fn
|
|
180
192
|
)
|
|
181
193
|
};
|
|
@@ -250,8 +262,12 @@ function FieldLibrary(type, queries) {
|
|
|
250
262
|
const def = queries[key];
|
|
251
263
|
if (!def) continue;
|
|
252
264
|
Object.defineProperty(GeneratedResolver.prototype, key, {
|
|
265
|
+
// `alwaysResolve` fields must run `fn` even when the parent pre-hydrated
|
|
266
|
+
// `root[key]`: they gate on `ctx`, and the short-circuit would return the
|
|
267
|
+
// pre-hydrated value without ever applying that check. Such a resolver is
|
|
268
|
+
// responsible for reusing `root[key]` itself, so the N+1 win is preserved.
|
|
253
269
|
value: async function(root, ctx, input) {
|
|
254
|
-
if (root[key]) return root[key];
|
|
270
|
+
if (!def.alwaysResolve && root[key]) return root[key];
|
|
255
271
|
return def.fn(root, ctx, input);
|
|
256
272
|
},
|
|
257
273
|
writable: false
|
package/dist/graphql/utils.d.mts
CHANGED
|
@@ -26,6 +26,23 @@ type ParsedGQLTypeWithArray<T, MergeNullUndefined extends boolean> = T extends A
|
|
|
26
26
|
type ParsedGQLTypeWithNullability<T, IsNullable extends boolean, MergeNullUndefined extends boolean> = IsNullable extends true ? ParsedGQLTypeWithArray<T, MergeNullUndefined> | null | undefined : ParsedGQLTypeWithArray<T, MergeNullUndefined>;
|
|
27
27
|
/** Allows resolver to return T or Promise<T>. */
|
|
28
28
|
type Promisify<T> = T | Promise<T>;
|
|
29
|
+
/**
|
|
30
|
+
* Return type of `.authCall()` / `.call()` for a definition.
|
|
31
|
+
*
|
|
32
|
+
* `authorized: true` promises the resolver an `AuthorizedContext` (non-null
|
|
33
|
+
* `userId`). Server-side callers can't be forced to hold a session, so when
|
|
34
|
+
* there's no refresh cookie the only way to keep that promise is to not call the
|
|
35
|
+
* resolver at all — hence the added `null`, which surfaces the logged-out case in
|
|
36
|
+
* the types instead of passing a null `userId` through the cast.
|
|
37
|
+
*
|
|
38
|
+
* `[IsAuth] extends [true]` (not `IsAuth extends true`) is deliberate: `field()`
|
|
39
|
+
* has no `= false` default and can't reverse-infer `IsAuth` from a `ctx: Context`
|
|
40
|
+
* signature, so a non-authorized field infers `IsAuth = boolean`. A naked
|
|
41
|
+
* conditional would distribute over `true | false` and union `null` into every
|
|
42
|
+
* public field's return. The tuple wrapper stops the distribution, so only a
|
|
43
|
+
* literal `true` adds `null`.
|
|
44
|
+
*/
|
|
45
|
+
type AuthCallReturn<IsAuth extends boolean, R> = [IsAuth] extends [true] ? Awaited<R> | null : Awaited<R>;
|
|
29
46
|
/** Base for query/field definition (output, input, options). */
|
|
30
47
|
interface BaseDefinition<T, U, IsAuth extends boolean = false, OutputNullable extends boolean = false, InputNullable extends boolean = false> {
|
|
31
48
|
output: T;
|
|
@@ -42,10 +59,16 @@ interface BaseDefinition<T, U, IsAuth extends boolean = false, OutputNullable ex
|
|
|
42
59
|
*/
|
|
43
60
|
interface QueryDefinition<T, U, IsAuth extends boolean = false, OutputNullable extends boolean = false, InputNullable extends boolean = false, R extends Promisify<ParsedGQLTypeWithNullability<T, OutputNullable, true>> = Promisify<ParsedGQLTypeWithNullability<T, OutputNullable, true>>> extends BaseDefinition<T, U, IsAuth, OutputNullable, InputNullable> {
|
|
44
61
|
fn: (ctx: IsAuth extends true ? AuthorizedContext : Context, data: ParsedGQLTypeWithNullability<U, InputNullable, false>) => R;
|
|
45
|
-
/**
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
62
|
+
/**
|
|
63
|
+
* Calls the resolver server-side. For authorized queries, reads the refresh cookie; for non-authorized, passes null userId.
|
|
64
|
+
* Resolves to `null` when `authorized: true` and there is no session.
|
|
65
|
+
*/
|
|
66
|
+
call: (data: ParsedGQLTypeWithNullability<U, InputNullable, false>) => Promise<AuthCallReturn<IsAuth, R>>;
|
|
67
|
+
/**
|
|
68
|
+
* Calls the resolver server-side with authentication (always reads the refresh cookie). Use in Server Components.
|
|
69
|
+
* Resolves to `null` when `authorized: true` and there is no session — guard it, or gate the page ahead of it.
|
|
70
|
+
*/
|
|
71
|
+
authCall: (data: ParsedGQLTypeWithNullability<U, InputNullable, false>) => Promise<AuthCallReturn<IsAuth, R>>;
|
|
49
72
|
mutation?: boolean;
|
|
50
73
|
}
|
|
51
74
|
/**
|
|
@@ -109,10 +132,26 @@ declare function resolver<T, U, IsAuth extends boolean = false, OutputNullable e
|
|
|
109
132
|
*/
|
|
110
133
|
interface FieldResolverDefinition<T, U, Root, IsAuth extends boolean = false, OutputNullable extends boolean = false, InputNullable extends boolean = false, R extends Promisify<ParsedGQLTypeWithNullability<T, OutputNullable, true>> = Promisify<ParsedGQLTypeWithNullability<T, OutputNullable, true>>> extends BaseDefinition<T, U, IsAuth, OutputNullable, InputNullable> {
|
|
111
134
|
fn: (root: Root, ctx: IsAuth extends true ? AuthorizedContext : Context, data: ParsedGQLTypeWithNullability<U, InputNullable, false>) => R;
|
|
112
|
-
/**
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
135
|
+
/**
|
|
136
|
+
* Calls the field resolver server-side. For authorized fields, reads the refresh cookie.
|
|
137
|
+
* Resolves to `null` when `authorized: true` and there is no session.
|
|
138
|
+
*/
|
|
139
|
+
call: (root: Root, data: ParsedGQLTypeWithNullability<U, InputNullable, false>) => Promise<AuthCallReturn<IsAuth, R>>;
|
|
140
|
+
/**
|
|
141
|
+
* Calls the field resolver server-side with authentication (always reads the refresh cookie).
|
|
142
|
+
* Resolves to `null` when `authorized: true` and there is no session.
|
|
143
|
+
*/
|
|
144
|
+
authCall: (root: Root, data: ParsedGQLTypeWithNullability<U, InputNullable, false>) => Promise<AuthCallReturn<IsAuth, R>>;
|
|
145
|
+
/**
|
|
146
|
+
* Skip `FieldLibrary`'s short-circuit, which returns a pre-hydrated `root[key]`
|
|
147
|
+
* without ever calling this resolver.
|
|
148
|
+
*
|
|
149
|
+
* Set this on any field that gates on `ctx` — an auth check inside the resolver
|
|
150
|
+
* is silently bypassed the moment a parent query selects the field's value into
|
|
151
|
+
* its own columns. The resolver stays responsible for reusing `root[key]` when
|
|
152
|
+
* present, so hydration still avoids the N+1.
|
|
153
|
+
*/
|
|
154
|
+
alwaysResolve?: boolean;
|
|
116
155
|
}
|
|
117
156
|
/**
|
|
118
157
|
* Defines a type-graphql field resolver with typed root, context, and input.
|
package/dist/graphql/utils.d.ts
CHANGED
|
@@ -26,6 +26,23 @@ type ParsedGQLTypeWithArray<T, MergeNullUndefined extends boolean> = T extends A
|
|
|
26
26
|
type ParsedGQLTypeWithNullability<T, IsNullable extends boolean, MergeNullUndefined extends boolean> = IsNullable extends true ? ParsedGQLTypeWithArray<T, MergeNullUndefined> | null | undefined : ParsedGQLTypeWithArray<T, MergeNullUndefined>;
|
|
27
27
|
/** Allows resolver to return T or Promise<T>. */
|
|
28
28
|
type Promisify<T> = T | Promise<T>;
|
|
29
|
+
/**
|
|
30
|
+
* Return type of `.authCall()` / `.call()` for a definition.
|
|
31
|
+
*
|
|
32
|
+
* `authorized: true` promises the resolver an `AuthorizedContext` (non-null
|
|
33
|
+
* `userId`). Server-side callers can't be forced to hold a session, so when
|
|
34
|
+
* there's no refresh cookie the only way to keep that promise is to not call the
|
|
35
|
+
* resolver at all — hence the added `null`, which surfaces the logged-out case in
|
|
36
|
+
* the types instead of passing a null `userId` through the cast.
|
|
37
|
+
*
|
|
38
|
+
* `[IsAuth] extends [true]` (not `IsAuth extends true`) is deliberate: `field()`
|
|
39
|
+
* has no `= false` default and can't reverse-infer `IsAuth` from a `ctx: Context`
|
|
40
|
+
* signature, so a non-authorized field infers `IsAuth = boolean`. A naked
|
|
41
|
+
* conditional would distribute over `true | false` and union `null` into every
|
|
42
|
+
* public field's return. The tuple wrapper stops the distribution, so only a
|
|
43
|
+
* literal `true` adds `null`.
|
|
44
|
+
*/
|
|
45
|
+
type AuthCallReturn<IsAuth extends boolean, R> = [IsAuth] extends [true] ? Awaited<R> | null : Awaited<R>;
|
|
29
46
|
/** Base for query/field definition (output, input, options). */
|
|
30
47
|
interface BaseDefinition<T, U, IsAuth extends boolean = false, OutputNullable extends boolean = false, InputNullable extends boolean = false> {
|
|
31
48
|
output: T;
|
|
@@ -42,10 +59,16 @@ interface BaseDefinition<T, U, IsAuth extends boolean = false, OutputNullable ex
|
|
|
42
59
|
*/
|
|
43
60
|
interface QueryDefinition<T, U, IsAuth extends boolean = false, OutputNullable extends boolean = false, InputNullable extends boolean = false, R extends Promisify<ParsedGQLTypeWithNullability<T, OutputNullable, true>> = Promisify<ParsedGQLTypeWithNullability<T, OutputNullable, true>>> extends BaseDefinition<T, U, IsAuth, OutputNullable, InputNullable> {
|
|
44
61
|
fn: (ctx: IsAuth extends true ? AuthorizedContext : Context, data: ParsedGQLTypeWithNullability<U, InputNullable, false>) => R;
|
|
45
|
-
/**
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
62
|
+
/**
|
|
63
|
+
* Calls the resolver server-side. For authorized queries, reads the refresh cookie; for non-authorized, passes null userId.
|
|
64
|
+
* Resolves to `null` when `authorized: true` and there is no session.
|
|
65
|
+
*/
|
|
66
|
+
call: (data: ParsedGQLTypeWithNullability<U, InputNullable, false>) => Promise<AuthCallReturn<IsAuth, R>>;
|
|
67
|
+
/**
|
|
68
|
+
* Calls the resolver server-side with authentication (always reads the refresh cookie). Use in Server Components.
|
|
69
|
+
* Resolves to `null` when `authorized: true` and there is no session — guard it, or gate the page ahead of it.
|
|
70
|
+
*/
|
|
71
|
+
authCall: (data: ParsedGQLTypeWithNullability<U, InputNullable, false>) => Promise<AuthCallReturn<IsAuth, R>>;
|
|
49
72
|
mutation?: boolean;
|
|
50
73
|
}
|
|
51
74
|
/**
|
|
@@ -109,10 +132,26 @@ declare function resolver<T, U, IsAuth extends boolean = false, OutputNullable e
|
|
|
109
132
|
*/
|
|
110
133
|
interface FieldResolverDefinition<T, U, Root, IsAuth extends boolean = false, OutputNullable extends boolean = false, InputNullable extends boolean = false, R extends Promisify<ParsedGQLTypeWithNullability<T, OutputNullable, true>> = Promisify<ParsedGQLTypeWithNullability<T, OutputNullable, true>>> extends BaseDefinition<T, U, IsAuth, OutputNullable, InputNullable> {
|
|
111
134
|
fn: (root: Root, ctx: IsAuth extends true ? AuthorizedContext : Context, data: ParsedGQLTypeWithNullability<U, InputNullable, false>) => R;
|
|
112
|
-
/**
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
135
|
+
/**
|
|
136
|
+
* Calls the field resolver server-side. For authorized fields, reads the refresh cookie.
|
|
137
|
+
* Resolves to `null` when `authorized: true` and there is no session.
|
|
138
|
+
*/
|
|
139
|
+
call: (root: Root, data: ParsedGQLTypeWithNullability<U, InputNullable, false>) => Promise<AuthCallReturn<IsAuth, R>>;
|
|
140
|
+
/**
|
|
141
|
+
* Calls the field resolver server-side with authentication (always reads the refresh cookie).
|
|
142
|
+
* Resolves to `null` when `authorized: true` and there is no session.
|
|
143
|
+
*/
|
|
144
|
+
authCall: (root: Root, data: ParsedGQLTypeWithNullability<U, InputNullable, false>) => Promise<AuthCallReturn<IsAuth, R>>;
|
|
145
|
+
/**
|
|
146
|
+
* Skip `FieldLibrary`'s short-circuit, which returns a pre-hydrated `root[key]`
|
|
147
|
+
* without ever calling this resolver.
|
|
148
|
+
*
|
|
149
|
+
* Set this on any field that gates on `ctx` — an auth check inside the resolver
|
|
150
|
+
* is silently bypassed the moment a parent query selects the field's value into
|
|
151
|
+
* its own columns. The resolver stays responsible for reusing `root[key]` when
|
|
152
|
+
* present, so hydration still avoids the N+1.
|
|
153
|
+
*/
|
|
154
|
+
alwaysResolve?: boolean;
|
|
116
155
|
}
|
|
117
156
|
/**
|
|
118
157
|
* Defines a type-graphql field resolver with typed root, context, and input.
|
|
@@ -102,8 +102,16 @@ function resolver(fn, options) {
|
|
|
102
102
|
return {
|
|
103
103
|
...options,
|
|
104
104
|
fn,
|
|
105
|
-
authCall: getAuthCaller(
|
|
106
|
-
|
|
105
|
+
authCall: getAuthCaller(
|
|
106
|
+
fn,
|
|
107
|
+
options.authorized
|
|
108
|
+
),
|
|
109
|
+
call: options.authorized ? getAuthCaller(
|
|
110
|
+
fn,
|
|
111
|
+
options.authorized
|
|
112
|
+
) : getCaller(
|
|
113
|
+
fn
|
|
114
|
+
)
|
|
107
115
|
};
|
|
108
116
|
}
|
|
109
117
|
var getUserId = async () => {
|
|
@@ -111,11 +119,13 @@ var getUserId = async () => {
|
|
|
111
119
|
const refresh = Cookie.get(REFRESH_COOKIE_NAME)?.value;
|
|
112
120
|
return refresh ? getUserIdFromRefreshToken(refresh) : null;
|
|
113
121
|
};
|
|
114
|
-
function getAuthCaller(fn) {
|
|
122
|
+
function getAuthCaller(fn, authorized) {
|
|
115
123
|
return cache(
|
|
116
124
|
async (data) => {
|
|
125
|
+
const userId = await getUserId();
|
|
126
|
+
if (authorized && !userId) return null;
|
|
117
127
|
const ctx = {
|
|
118
|
-
userId
|
|
128
|
+
userId,
|
|
119
129
|
isRefreshID: true
|
|
120
130
|
};
|
|
121
131
|
return await fn(ctx, data);
|
|
@@ -133,11 +143,13 @@ function getCaller(fn) {
|
|
|
133
143
|
}
|
|
134
144
|
);
|
|
135
145
|
}
|
|
136
|
-
function getFieldAuthCaller(fn) {
|
|
146
|
+
function getFieldAuthCaller(fn, authorized) {
|
|
137
147
|
return cache(
|
|
138
148
|
async (root, data) => {
|
|
149
|
+
const userId = await getUserId();
|
|
150
|
+
if (authorized && !userId) return null;
|
|
139
151
|
const ctx = {
|
|
140
|
-
userId
|
|
152
|
+
userId,
|
|
141
153
|
isRefreshID: true
|
|
142
154
|
};
|
|
143
155
|
return await fn(root, ctx, data);
|
|
@@ -159,8 +171,8 @@ function field(fn, options) {
|
|
|
159
171
|
return {
|
|
160
172
|
...options,
|
|
161
173
|
fn,
|
|
162
|
-
authCall: getFieldAuthCaller(fn),
|
|
163
|
-
call: options.authorized ? getFieldAuthCaller(fn) : getFieldCaller(
|
|
174
|
+
authCall: getFieldAuthCaller(fn, options.authorized),
|
|
175
|
+
call: options.authorized ? getFieldAuthCaller(fn, options.authorized) : getFieldCaller(
|
|
164
176
|
fn
|
|
165
177
|
)
|
|
166
178
|
};
|
|
@@ -235,8 +247,12 @@ function FieldLibrary(type, queries) {
|
|
|
235
247
|
const def = queries[key];
|
|
236
248
|
if (!def) continue;
|
|
237
249
|
Object.defineProperty(GeneratedResolver.prototype, key, {
|
|
250
|
+
// `alwaysResolve` fields must run `fn` even when the parent pre-hydrated
|
|
251
|
+
// `root[key]`: they gate on `ctx`, and the short-circuit would return the
|
|
252
|
+
// pre-hydrated value without ever applying that check. Such a resolver is
|
|
253
|
+
// responsible for reusing `root[key]` itself, so the N+1 win is preserved.
|
|
238
254
|
value: async function(root, ctx, input) {
|
|
239
|
-
if (root[key]) return root[key];
|
|
255
|
+
if (!def.alwaysResolve && root[key]) return root[key];
|
|
240
256
|
return def.fn(root, ctx, input);
|
|
241
257
|
},
|
|
242
258
|
writable: false
|