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.
@@ -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: "no-cache"` by default to always get fresh data.
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
  *
@@ -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: "no-cache"` by default to always get fresh data.
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(fn),
774
- call: options.authorized ? getAuthCaller(fn) : getCaller(fn)
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: await getUserId(),
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: await getUserId(),
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(fn),
768
- call: options.authorized ? getAuthCaller(fn) : getCaller(fn)
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: await getUserId(),
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: await getUserId(),
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(fn),
121
- call: options.authorized ? getAuthCaller(fn) : getCaller(fn)
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: await getUserId(),
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: await getUserId(),
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
@@ -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
- /** Calls the resolver server-side. For authorized queries, reads the refresh cookie; for non-authorized, passes null userId. */
46
- call: (data: ParsedGQLTypeWithNullability<U, InputNullable, false>) => Promise<Awaited<R>>;
47
- /** Calls the resolver server-side with authentication (always reads the refresh cookie). Use in Server Components. */
48
- authCall: (data: ParsedGQLTypeWithNullability<U, InputNullable, false>) => Promise<Awaited<R>>;
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
- /** Calls the field resolver server-side. For authorized fields, reads the refresh cookie. */
113
- call: (root: Root, data: ParsedGQLTypeWithNullability<U, InputNullable, false>) => Promise<Awaited<R>>;
114
- /** Calls the field resolver server-side with authentication (always reads the refresh cookie). */
115
- authCall: (root: Root, data: ParsedGQLTypeWithNullability<U, InputNullable, false>) => Promise<Awaited<R>>;
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.
@@ -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
- /** Calls the resolver server-side. For authorized queries, reads the refresh cookie; for non-authorized, passes null userId. */
46
- call: (data: ParsedGQLTypeWithNullability<U, InputNullable, false>) => Promise<Awaited<R>>;
47
- /** Calls the resolver server-side with authentication (always reads the refresh cookie). Use in Server Components. */
48
- authCall: (data: ParsedGQLTypeWithNullability<U, InputNullable, false>) => Promise<Awaited<R>>;
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
- /** Calls the field resolver server-side. For authorized fields, reads the refresh cookie. */
113
- call: (root: Root, data: ParsedGQLTypeWithNullability<U, InputNullable, false>) => Promise<Awaited<R>>;
114
- /** Calls the field resolver server-side with authentication (always reads the refresh cookie). */
115
- authCall: (root: Root, data: ParsedGQLTypeWithNullability<U, InputNullable, false>) => Promise<Awaited<R>>;
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(fn),
106
- call: options.authorized ? getAuthCaller(fn) : getCaller(fn)
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: await getUserId(),
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: await getUserId(),
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "naystack",
3
- "version": "1.8.0",
3
+ "version": "1.8.2",
4
4
  "description": "A stack built with Next + GraphQL + S3 + Auth",
5
5
  "main": "dist/index.cjs.js",
6
6
  "module": "dist/index.esm.js",