Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add solid #3327

Closed
wants to merge 17 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 72 additions & 0 deletions packages/solid-urql/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
{
"name": "@urql/solid",
"version": "4.0.4",
"description": "A highly customizable and versatile GraphQL client for Solid",
"sideEffects": false,
"homepage": "https://formidable.com/open-source/urql/docs/",
"bugs": "https://github.com/urql-graphql/urql/issues",
"license": "MIT",
"author": "urql GraphQL Contributors",
"repository": {
"type": "git",
"url": "https://github.com/urql-graphql/urql.git",
"directory": "packages/solid-urql"
},
"keywords": [
"graphql client",
"state management",
"cache",
"graphql",
"exchanges",
"solid"
],
"main": "dist/urql-solid",
"module": "dist/urql-solid.mjs",
"types": "dist/urql-solid.d.ts",
"source": "src/index.ts",
"exports": {
".": {
"types": "./dist/urql-solid.d.ts",
"import": "./dist/urql-solid.mjs",
"require": "./dist/urql-solid.js",
"source": "./src/index.ts"
},
"./package.json": "./package.json"
},
"files": [
"LICENSE",
"CHANGELOG.md",
"README.md",
"dist/"
],
"scripts": {
"test": "vitest",
"clean": "rimraf dist",
"check": "tsc --noEmit",
"lint": "eslint --ext=js,jsx,ts,tsx .",
"build": "rollup -c ../../scripts/rollup/config.mjs",
"prepare": "node ../../scripts/prepare/index.js",
"prepublishOnly": "run-s clean build"
},
"devDependencies": {
"@solidjs/testing-library": "^0.8.2",
"@testing-library/jest-dom": "^5.16.5",
"@urql/core": "workspace:*",
"graphql": "^16.0.0",
"jsdom": "^22.1.0",
"vite-plugin-solid": "^2.7.0",
"vite-tsconfig-paths": "^4.2.0",
"vitest": "^0.32.2"
},
"peerDependencies": {
"solid-js": "^1.7.7"
},
"dependencies": {
"@urql/core": "^4.0.0",
"wonka": "^6.3.2"
},
"publishConfig": {
"access": "public",
"provenance": true
}
}
20 changes: 20 additions & 0 deletions packages/solid-urql/src/context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import type { Client } from '@urql/core';
import { createContext, useContext } from 'solid-js';

export const Context = createContext<Client>();
export const Provider = Context.Provider;

export type UseClient = () => Client;
export const useClient: UseClient = () => {
const client = useContext(Context);

if (process.env.NODE_ENV !== 'production' && client === undefined) {
const error =
"No client has been specified using urql's Provider. please create a client and add a Provider.";

console.error(error);
throw new Error(error);
}

return client!;
};
111 changes: 111 additions & 0 deletions packages/solid-urql/src/createMutation.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { testEffect } from '@solidjs/testing-library';
import { expect, it, describe, vi } from 'vitest';
import { CreateMutationState, createMutation } from './createMutation';
import {
OperationResult,
OperationResultSource,
createClient,
gql,
} from '@urql/core';
import { makeSubject } from 'wonka';
import { createEffect } from 'solid-js';

const QUERY = gql`
mutation {
test
}
`;

const client = createClient({
url: '/graphql',
exchanges: [],
suspense: false,
});

vi.mock('./context', () => {
const useClient = () => {
return client!;
};

return { useClient };
});

// Given that it is not possible to directly listen to all store changes it is necessary
// to access all relevant parts on which `createEffect` should listen on
const markStateDependencies = (state: CreateMutationState<any, any>) => {
state.data;
state.error;
state.extensions;
state.fetching;
state.operation;
state.stale;
};

describe('createMutation', () => {
it('should have expected state before and after finish', () => {
const subject = makeSubject<any>();
const clientMutation = vi
.spyOn(client, 'executeMutation')
.mockImplementation(
() => subject.source as OperationResultSource<OperationResult>
);

return testEffect(done => {
const [state, execute] = createMutation<
{ test: boolean },
{ variable: number }
>(QUERY);

createEffect((run: number = 0) => {
markStateDependencies(state);

switch (run) {
case 0: {
expect(state).toMatchObject({
data: undefined,
stale: false,
fetching: false,
error: undefined,
extensions: undefined,
operation: undefined,
});

execute({ variable: 1 });
break;
}

case 1: {
expect(state).toMatchObject({
data: undefined,
stale: false,
fetching: true,
error: undefined,
extensions: undefined,
operation: undefined,
});

expect(clientMutation).toHaveBeenCalledTimes(1);
subject.next({ data: { test: true }, stale: false });

break;
}

case 2: {
expect(state).toMatchObject({
data: { test: true },
stale: false,
fetching: false,
error: undefined,
extensions: undefined,
});

done();
break;
}
}

return run + 1;
});
});
});
});
177 changes: 177 additions & 0 deletions packages/solid-urql/src/createMutation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
import { createStore } from 'solid-js/store';
import {
type AnyVariables,
type DocumentInput,
type OperationContext,
type Operation,
type OperationResult,
type CombinedError,
createRequest,
} from '@urql/core';
import { useClient } from './context';
import { pipe, onPush, filter, take, toPromise } from 'wonka';

export type CreateMutationState<
Data = any,
Variables extends AnyVariables = AnyVariables,
> = {
/** Indicates whether `createMutation` is currently executing a mutation. */
fetching: boolean;

/** Indicates that the mutation result is not fresh.
*
* @remarks
* The `stale` flag is set to `true` when a new result for the mutation
* is expected.
* This is mostly unused for mutations and will rarely affect you, and
* is more relevant for queries.
*
* @see {@link OperationResult.stale} for the source of this value.
*/
stale: boolean;
/** The {@link OperationResult.data} for the executed mutation. */
data?: Data;
/** The {@link OperationResult.error} for the executed mutation. */
error?: CombinedError;
/** The {@link OperationResult.extensions} for the executed mutation. */
extensions?: Record<string, any>;
/** The {@link Operation} that the current state is for.
*
* @remarks
* This is the mutation {@link Operation} that has last been executed.
* When {@link CreateMutationState.fetching} is `true`, this is the
* last `Operation` that the current state was for.
*/
operation?: Operation<Data, Variables>;
};

/** Triggers {@link createMutation} to execute its GraphQL mutation operation.
*
* @param variables - variables using which the mutation will be executed.
* @param context - optionally, context options that will be merged with the hook's
* context options and the `Client`’s options.
* @returns the {@link OperationResult} of the mutation.
*
* @remarks
* When called, {@link createMutation} will start the GraphQL mutation
* it currently holds and use the `variables` passed to it.
*
* Once the mutation response comes back from the API, its
* returned promise will resolve to the mutation’s {@link OperationResult}
* and the {@link CreateMutationState} will be updated with the result.
*
* @example
* ```ts
* const [result, executeMutation] = createMutation(UpdateTodo);
* const start = async ({ id, title }) => {
* const result = await executeMutation({ id, title });
* };
*/
export type CreateMutationExecute<
Data = any,
Variables extends AnyVariables = AnyVariables,
> = (
variables: Variables,
context?: Partial<OperationContext>
) => Promise<OperationResult<Data, Variables>>;

/** Result tuple returned by the {@link createMutation} hook.
*
* @remarks
* Similarly to a `createSignal` hook’s return value,
* the first element is the {@link createMutation}’s state, updated
* as mutations are executed with the second value, which is
* used to start mutations and is a {@link CreateMutationExecute}
* function.
*/
export type CreateMutationResult<
Data = any,
Variables extends AnyVariables = AnyVariables,
> = [
CreateMutationState<Data, Variables>,
CreateMutationExecute<Data, Variables>,
];

/** Hook to create a GraphQL mutation, run by passing variables to the returned execute function.
*
* @param query - a GraphQL mutation document which `createMutation` will execute.
* @returns a {@link CreateMutationResult} tuple of a {@link CreateMutationState} result,
* and an execute function to start the mutation.
*
* @remarks
* `createMutation` allows GraphQL mutations to be defined and keeps its state
* after the mutation is started with the returned execute function.
*
* Given a GraphQL mutation document it returns state to keep track of the
* mutation state and a {@link CreateMutationExecute} function, which accepts
* variables for the mutation to be executed.
* Once called, the mutation executes and the state will be updated with
* the mutation’s result.
*
* @example
* ```ts
* import { gql, createMutation } from '@urql/solid';
*
* const UpdateTodo = gql`
* mutation ($id: ID!, $title: String!) {
* updateTodo(id: $id, title: $title) {
* id, title
* }
* }
* `;
*
* const UpdateTodo = () => {
* const [result, executeMutation] = createMutation(UpdateTodo);
* const start = async ({ id, title }) => {
* const result = await executeMutation({ id, title });
* };
* // ...
* };
* ```
*/
export const createMutation = <
Data = any,
Variables extends AnyVariables = AnyVariables,
>(
query: DocumentInput<Data, Variables>
): CreateMutationResult<Data, Variables> => {
const client = useClient();
const initialResult: CreateMutationState<Data, Variables> = {
operation: undefined,
fetching: false,
stale: false,
data: undefined,
error: undefined,
extensions: undefined,
};

const [state, setState] =
createStore<CreateMutationState<Data, Variables>>(initialResult);

const execute = (
variables: Variables,
context?: Partial<OperationContext>
) => {
setState({ ...initialResult, fetching: true });

const request = createRequest(query, variables);
return pipe(
client.executeMutation(request, context),
onPush(result => {
setState({
fetching: false,
stale: result.stale,
data: result.data,
error: result.error,
extensions: result.extensions,
operation: result.operation,
});
}),
filter(result => !result.hasNext),
take(1),
toPromise
);
};

return [state, execute];
};
Loading