diff --git a/.changeset/chilled-buttons-grow.md b/.changeset/chilled-buttons-grow.md new file mode 100644 index 0000000000..a83675e6ae --- /dev/null +++ b/.changeset/chilled-buttons-grow.md @@ -0,0 +1,5 @@ +--- +'houdini': patch +--- + +avoid manipulating scalars with null values diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 4562601f90..e8d96afa04 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -18,17 +18,24 @@ jobs: runs-on: ubuntu-latest steps: + - name: Checkout source + uses: actions/checkout@master + with: + ref: ${{ github.ref }} + - name: Setup Node - uses: actions/setup-node@v1 + uses: actions/setup-node@v3 with: node-version: 16.14.2 + cache: 'yarn' - - name: Checkout source - uses: actions/checkout@master + - uses: actions/cache@v2 with: - ref: ${{ github.ref }} + path: '**/node_modules' + key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }} - name: Install Dependencies + if: steps.yarn-cache.outputs.cache-hit != 'true' run: yarn install env: YARN_ENABLE_IMMUTABLE_INSTALLS: false diff --git a/integration/.eslintrc.cjs b/integration/.eslintrc.cjs index acc9a701a6..2e05cb6515 100644 --- a/integration/.eslintrc.cjs +++ b/integration/.eslintrc.cjs @@ -18,6 +18,7 @@ module.exports = { node: true }, rules: { - '@typescript-eslint/ban-ts-comment': 'off' + '@typescript-eslint/ban-ts-comment': 'off', + '@typescript-eslint/ban-types': 'off' } }; diff --git a/integration/api/graphql.mjs b/integration/api/graphql.mjs index e1c2a68da1..04f59fa818 100644 --- a/integration/api/graphql.mjs +++ b/integration/api/graphql.mjs @@ -60,9 +60,16 @@ export const resolvers = { }, user: async (_, args) => { // simulate network delay - // await sleep(1000); + if (args.delay) { + await sleep(args.delay); + } const user = getSnapshot(args.snapshot).find((c) => c.id === `${args.snapshot}:${args.id}`); + + if (args.forceNullDate) { + user.birthDate = null; + } + if (!user) { throw new GraphQLYogaError('User not found', { code: 404 }); } @@ -75,6 +82,7 @@ export const resolvers = { const [snapshot, id] = nodeID.split(':'); const list = getSnapshot(snapshot); const user = list.find((u) => u.id === nodeID); + return { ...user, __typename: 'User' diff --git a/integration/api/schema.graphql b/integration/api/schema.graphql index 5a99905e7d..772629e48b 100644 --- a/integration/api/schema.graphql +++ b/integration/api/schema.graphql @@ -39,7 +39,7 @@ type PageInfo { type Query { avgYearsBirthDate: Float! node(id: ID!): Node - user(id: ID!, snapshot: String!, tmp: Boolean): User! + user(id: ID!, snapshot: String!, tmp: Boolean, delay: Int, forceNullDate: Boolean): User! usersConnection( after: String before: String @@ -52,7 +52,7 @@ type Query { } type User implements Node { - birthDate: DateTime! + birthDate: DateTime friendsConnection(after: String, before: String, first: Int, last: Int): UserConnection! friendsList(limit: Int, offset: Int): [User!]! id: ID! diff --git a/integration/houdini.config.js b/integration/houdini.config.js index 331d0f6e37..3c79cebadc 100644 --- a/integration/houdini.config.js +++ b/integration/houdini.config.js @@ -6,6 +6,7 @@ const config = { module: 'esm', apiUrl: 'http://localhost:4000/graphql', defaultCachePolicy: 'CacheOrNetwork', + defaultPartial: true, routes: (filepath) => filepath && !filepath.includes('_') && !filepath.includes('+'), scalars: { DateTime: { diff --git a/integration/src/lib/QueryComponent.svelte b/integration/src/lib/QueryComponent.svelte index d0ad8490e6..716d4fea3e 100644 --- a/integration/src/lib/QueryComponent.svelte +++ b/integration/src/lib/QueryComponent.svelte @@ -9,6 +9,7 @@ +ISO:
{$data?.user.birthDate.toISOString()}
+ +
+ +Local: +
+ {$data?.user.birthDate.toLocaleString()} +
diff --git a/integration/src/routes/stores/partial/partial.spec.ts b/integration/src/routes/stores/partial/partial.spec.ts new file mode 100644 index 0000000000..5445d55fc4 --- /dev/null +++ b/integration/src/routes/stores/partial/partial.spec.ts @@ -0,0 +1,42 @@ +import { sleep } from '@kitql/helper'; +import { test } from '@playwright/test'; +import { routes } from '../../../lib/utils/routes.js'; +import { clientSideNavigation, expectToBe } from '../../../lib/utils/testsHelper.js'; + +test.describe('Partial Pages', () => { + test('From the list to the detail should see 2 info then the date coming', async ({ page }) => { + // Go to the list + await page.goto(routes.Stores_Partial_List); + + // We should have the list + expectToBe( + page, + 'Full Light Partial:1 - Bruce Willis Full Light Partial:2 - Samuel Jackson Full Light Partial:3 - Morgan Freeman Full Light Partial:4 - Tom Hanks' + ); + + // Go on the light page 2 + await page.locator('a[id="l_2"]').click(); + + // Wait a bit so that the server respond with 2 fields + await sleep(2345); + + // Check that we have 2 fields + expectToBe(page, 'Partial:2', 'div[id="id"]'); + expectToBe(page, 'Samuel Jackson', 'div[id="name"]'); + + // go back to the list + await clientSideNavigation(page, routes.Stores_Partial_List); + + // Go on the light page 2 FULL + await page.locator('a[id="f_2"]').click(); + + // Click on the link and check directly the 3 divs + expectToBe(page, 'Partial:2', 'div[id="id"]'); + expectToBe(page, 'Samuel Jackson', 'div[id="name"]'); + expectToBe(page, 'undefined', 'div[id="birthDate"]'); + + // Wait a bit so that the server respond and birthDate is displayed + await sleep(2345); + expectToBe(page, '1948-12-21T00:00:00.000Z', 'div[id="birthDate"]'); + }); +}); diff --git a/integration/src/routes/stores/partial/partial_List.gql b/integration/src/routes/stores/partial/partial_List.gql new file mode 100644 index 0000000000..c1be7173f5 --- /dev/null +++ b/integration/src/routes/stores/partial/partial_List.gql @@ -0,0 +1,6 @@ +query Partial_List { + usersList(snapshot: "Partial") { + id + name + } +} diff --git a/integration/src/routes/stores/partial/partial_List.svelte b/integration/src/routes/stores/partial/partial_List.svelte new file mode 100644 index 0000000000..d59be907b8 --- /dev/null +++ b/integration/src/routes/stores/partial/partial_List.svelte @@ -0,0 +1,28 @@ + + + + +
+ {#each $GQL_Partial_List.data?.usersList ?? [] as { id, name }} + {@const realId = id.split(':')[1]} +
+ Full + Light + + {id} - {name} + +
+ {/each} +
diff --git a/integration/src/routes/stores/partial/partial_[userId].gql b/integration/src/routes/stores/partial/partial_[userId].gql new file mode 100644 index 0000000000..c87318ce68 --- /dev/null +++ b/integration/src/routes/stores/partial/partial_[userId].gql @@ -0,0 +1,7 @@ +query Partial_User($id: ID!) { + user(id: $id, snapshot: "Partial", delay: 1000) { + id + name + birthDate + } +} diff --git a/integration/src/routes/stores/partial/partial_[userId].svelte b/integration/src/routes/stores/partial/partial_[userId].svelte new file mode 100644 index 0000000000..2516599b61 --- /dev/null +++ b/integration/src/routes/stores/partial/partial_[userId].svelte @@ -0,0 +1,38 @@ + + + + +Back to the list + +
+
+ +
+ {$GQL_Partial_User.data?.user.id} +
+
+ {$GQL_Partial_User.data?.user.name} +
+
+ {$GQL_Partial_User.data?.user.birthDate?.toISOString()} +
diff --git a/integration/src/routes/stores/partial/partial_[userId]_Light.gql b/integration/src/routes/stores/partial/partial_[userId]_Light.gql new file mode 100644 index 0000000000..317037c2ff --- /dev/null +++ b/integration/src/routes/stores/partial/partial_[userId]_Light.gql @@ -0,0 +1,6 @@ +query Partial_User_Light($id: ID!) @cache(partial: true) { + user(id: $id, snapshot: "Partial", delay: 1000) { + id + name + } +} diff --git a/integration/src/routes/stores/partial/partial_[userId]_Light.svelte b/integration/src/routes/stores/partial/partial_[userId]_Light.svelte new file mode 100644 index 0000000000..88656e7299 --- /dev/null +++ b/integration/src/routes/stores/partial/partial_[userId]_Light.svelte @@ -0,0 +1,35 @@ + + + + +Back to the list + +
+
+ +
+ {$GQL_Partial_User_Light.data?.user.id} +
+
+ {$GQL_Partial_User_Light.data?.user.name} +
diff --git a/src/runtime/lib/scalars.test.ts b/src/runtime/lib/scalars.test.ts index d4581bc3cf..0ac005d524 100644 --- a/src/runtime/lib/scalars.test.ts +++ b/src/runtime/lib/scalars.test.ts @@ -1,8 +1,8 @@ +import { jest } from '@jest/globals' import { testConfigFile } from '../../common' import { RequestContext } from './network' import { marshalSelection, unmarshalSelection } from './scalars' import { ArtifactKind, QueryArtifact } from './types' -import { jest } from '@jest/globals' jest.mock('../cache', function () { return @@ -515,6 +515,34 @@ describe('unmarshal selection', function () { }) }) + test('null inside', function () { + const data = { + item: { + createdAt: null, + }, + } + + const selection = { + item: { + type: 'TodoItem', + keyRaw: 'item', + + fields: { + createdAt: { + type: 'DateTime', + keyRaw: 'createdAt', + }, + }, + }, + } + + expect(unmarshalSelection(config, selection, data)).toEqual({ + item: { + createdAt: null, + }, + }) + }) + test('nested objects', function () { // the date to compare against const date = new Date() diff --git a/src/runtime/lib/scalars.ts b/src/runtime/lib/scalars.ts index 7f49938dd4..0c107b53f0 100644 --- a/src/runtime/lib/scalars.ts +++ b/src/runtime/lib/scalars.ts @@ -157,7 +157,9 @@ export function unmarshalSelection( unmarshalSelection(config, fields, value), ] } - + if (value === null) { + return [fieldName, value] + } // is the type something that requires marshaling if (config.scalars?.[type]?.marshal) { const unmarshalFn = config.scalars[type].unmarshal