diff --git a/packages/houdini-svelte/src/plugin/transforms/kit/load.ts b/packages/houdini-svelte/src/plugin/transforms/kit/load.ts index 8ce45da024..cb463554fe 100644 --- a/packages/houdini-svelte/src/plugin/transforms/kit/load.ts +++ b/packages/houdini-svelte/src/plugin/transforms/kit/load.ts @@ -528,12 +528,17 @@ function variable_function_for_query( for (const definition of query.variableDefinitions ?? []) { const unwrapped = unwrapType(page.config, definition.type) + // if the type is a runtime scalar, its optional + const runtime_scalar = + page.config.configFile.features?.runtimeScalars?.[unwrapped.type.name] + // we need to remember the definition if // the argument to the operation is non-null // the url param doesn't exist or does exist but is optional if ( unwrapped.wrappers[unwrapped.wrappers.length - 1] === TypeWrapper.NonNull && !definition.defaultValue && + !runtime_scalar && (!params[definition.variable.name.value] || params[definition.variable.name.value].optional) ) { @@ -564,8 +569,8 @@ function variable_function_for_query( AST.variableDeclarator( AST.identifier('result'), AST.objectExpression( - Object.entries(has_args).map(([arg, type]) => - AST.objectProperty( + Object.entries(has_args).map(([arg, type]) => { + return AST.objectProperty( AST.identifier(arg), AST.callExpression(AST.identifier('parseScalar'), [ AST.identifier('config'), @@ -579,7 +584,7 @@ function variable_function_for_query( ), ]) ) - ) + }) ) ), ]), diff --git a/packages/houdini/src/runtime/client/plugins/cache.test.ts b/packages/houdini/src/runtime/client/plugins/cache.test.ts index 01b3e4d36b..07c9ad4cd3 100644 --- a/packages/houdini/src/runtime/client/plugins/cache.test.ts +++ b/packages/houdini/src/runtime/client/plugins/cache.test.ts @@ -3,7 +3,7 @@ import { beforeEach, expect, test, vi } from 'vitest' import { createPluginHooks, HoudiniClient, type HoudiniClientConstructorArgs } from '..' import { testConfigFile } from '../../../test' import { Cache } from '../../cache/cache' -import { CachePolicy, PendingValue } from '../../lib' +import { CachePolicy, PendingValue, QueryArtifact } from '../../lib' import { setMockConfig } from '../../lib/config' import { ArtifactKind, DataSource } from '../../lib/types' import type { ClientPlugin } from '../documentStore' @@ -476,7 +476,7 @@ test('loading states when fetching is true', async function () { * Utilities for testing the cache plugin */ export function createStore( - args: Partial = {} + args: Partial & { artifact?: QueryArtifact } = {} ): DocumentStore { // if we dont have anything passed, just use the fake fetch as the plugin if (!args.plugins && !args.pipeline) { @@ -493,7 +493,7 @@ export function createStore( plugins: args.plugins ? createPluginHooks(client.plugins) : undefined, pipeline: args.pipeline ? createPluginHooks(client.plugins) : undefined, client, - artifact: { + artifact: args.artifact ?? { kind: ArtifactKind.Query, hash: '7777', raw: 'RAW_TEXT', @@ -535,7 +535,7 @@ export function createStore( }) } -function fakeFetch({ +export function fakeFetch({ result = { data: { viewer: { @@ -551,9 +551,11 @@ function fakeFetch({ partial: false, stale: false, }, + spy = vi.fn(), } = {}) { return (() => ({ network(ctx, { resolve }) { + spy(ctx) resolve(ctx, { ...result }) }, })) as ClientPlugin diff --git a/packages/houdini/src/runtime/client/plugins/query.test.ts b/packages/houdini/src/runtime/client/plugins/query.test.ts new file mode 100644 index 0000000000..6bbe09148f --- /dev/null +++ b/packages/houdini/src/runtime/client/plugins/query.test.ts @@ -0,0 +1,76 @@ +import { beforeEach, expect, test, vi } from 'vitest' + +import { testConfigFile } from '../../../test' +import { setMockConfig } from '../../lib/config' +import { createStore, fakeFetch } from './cache.test' +import { query } from './query' + +const config = testConfigFile() +beforeEach(async () => { + setMockConfig(config) +}) + +test('query plugin evaluates runtime scalars', async function () { + const fetchSpy = vi.fn() + + const store = createStore({ + artifact: { + kind: 'HoudiniQuery', + hash: '7777', + raw: 'RAW_TEXT', + name: 'TestArtifact', + rootType: 'Query', + pluginData: {}, + enableLoadingState: 'local', + input: { + fields: { + id: 'ID', + }, + types: {}, + defaults: {}, + runtimeScalars: { + id: 'ViewerIDFromSession', + }, + }, + selection: { + fields: { + viewer: { + type: 'User', + visible: true, + keyRaw: 'viewer', + loading: { kind: 'continue' }, + selection: { + fields: { + id: { + type: 'ID', + visible: true, + keyRaw: 'id', + }, + firstName: { + type: 'String', + visible: true, + keyRaw: 'firstName', + loading: { kind: 'value' }, + }, + __typename: { + type: 'String', + visible: true, + keyRaw: '__typename', + }, + }, + }, + }, + }, + }, + }, + pipeline: [query, fakeFetch({ spy: fetchSpy })], + }) + + // run the query with an artifact that contains runtime scalars + await store.send({ session: { token: 'world' } }) + + // the fetch spy should + const ctx = fetchSpy.mock.calls[0][0] + + expect(ctx.variables).toEqual({ id: 'world' }) +}) diff --git a/packages/houdini/src/runtime/client/plugins/query.ts b/packages/houdini/src/runtime/client/plugins/query.ts index 3e14031c54..e05dfb6174 100644 --- a/packages/houdini/src/runtime/client/plugins/query.ts +++ b/packages/houdini/src/runtime/client/plugins/query.ts @@ -1,4 +1,5 @@ import cache from '../../cache' +import { type RuntimeScalarPayload } from '../../lib' import { type SubscriptionSpec, ArtifactKind, DataSource } from '../../lib/types' import type { ClientPlugin } from '../documentStore' import { documentPlugin } from '../utils' @@ -13,9 +14,29 @@ export const query: ClientPlugin = documentPlugin(ArtifactKind.Query, function ( // the function to call when a query is sent return { start(ctx, { next }) { + const runtimeScalarPayload: RuntimeScalarPayload = { + session: ctx.session, + } + // make sure to include the last variables as well as the new ones ctx.variables = { ...lastVariables, + // we need to evaluate any runtime scalars but allow the user to overwrite them + // by explicitly passing variables + ...Object.fromEntries( + Object.entries(ctx.artifact.input?.runtimeScalars ?? {}).map( + ([field, type]) => { + const runtimeScalar = ctx.config.features?.runtimeScalars?.[type] + // make typescript happy + if (!runtimeScalar) { + return [field, type] + } + + // resolve the runtime scalar + return [field, runtimeScalar.resolve(runtimeScalarPayload)] + } + ) + ), ...ctx.variables, } next(ctx) diff --git a/packages/houdini/src/runtime/lib/config.ts b/packages/houdini/src/runtime/lib/config.ts index 9f00e7a945..d89fd959a3 100644 --- a/packages/houdini/src/runtime/lib/config.ts +++ b/packages/houdini/src/runtime/lib/config.ts @@ -225,21 +225,24 @@ export type ConfigFile = { features?: { /** Interact with the cache directly using an imperative API.*/ imperativeCache?: boolean - /** [React Only] Emebed component references in query responses*/ - componentFields?: boolean - /** [React Only] Compute query variable values using a runtime scalar*/ runtimeScalars?: Record< string, { // the equivalent GraphQL type type: string // the function to call that serializes the type for the API - resolve: (args: { session: App.Session }) => any + resolve: (args: RuntimeScalarPayload) => any } > + /** [React Only] Emebed component references in query responses*/ + componentFields?: boolean } } +export type RuntimeScalarPayload = { + session?: App.Session | null | undefined +} + type RouterConfig = { auth?: AuthStrategy apiEndpoint?: string diff --git a/packages/houdini/src/runtime/lib/scalars.test.ts b/packages/houdini/src/runtime/lib/scalars.test.ts index 26b79e2652..a0b7a2f7ef 100644 --- a/packages/houdini/src/runtime/lib/scalars.test.ts +++ b/packages/houdini/src/runtime/lib/scalars.test.ts @@ -90,6 +90,8 @@ const artifact: QueryArtifact = { enumValue: 'EnumValue', }, }, + runtimeScalars: {}, + defaults: {}, }, } diff --git a/packages/houdini/src/test/index.ts b/packages/houdini/src/test/index.ts index 58ad9e0396..33f8290572 100644 --- a/packages/houdini/src/test/index.ts +++ b/packages/houdini/src/test/index.ts @@ -1,7 +1,6 @@ import * as graphql from 'graphql' import { vol } from 'memfs' -import session from '../../../houdini-svelte/src/plugin/transforms/kit/session' import { runPipeline } from '../codegen' import type { Document } from '../lib' import { Config, fs, path } from '../lib' @@ -298,7 +297,8 @@ export function testConfigFile({ plugins, ...config }: Partial = {}) runtimeScalars: { ViewerIDFromSession: { type: 'ID', - resolve: ({ session }) => session, + resolve: ({ session }: { session?: App.Session | null | undefined }) => + (session as unknown as any).token, }, }, },