Skip to content

Commit

Permalink
feature: Support GraphQL Trusted Documents aka Persisted Operations f…
Browse files Browse the repository at this point in the history
…or added security (redwoodjs#9416)

Co-authored-by: Tobbe Lundberg <[email protected]>
  • Loading branch information
dthyresson and Tobbe authored Nov 17, 2023
1 parent f0dd337 commit a1de078
Show file tree
Hide file tree
Showing 46 changed files with 1,531 additions and 384 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export const store = {
d67f5d54ba7d2a94e34809f20a0380f9921a5586:
'mutation AddTodo_CreateTodo($body: String!) { __typename createTodo(body: $body) { __typename body id status } }',
'81a7e7b720f992f8cfcaab15f42cf5a6802ed338':
'query NumTodosCell_GetCount { __typename todosCount }',
a9d0f2c090ac4320919f631ab0003fcdd2c30652:
'query TodoListCell_GetTodos { __typename todos { __typename body id status } }',
'69a8d2c6640912a8323a729adae2cc2f2f1bdb59':
'mutation TodoListCell_CheckTodo($id: Int!, $status: String!) { __typename updateTodoStatus(id: $id, status: $status) { __typename id status } }',
}
66 changes: 66 additions & 0 deletions __fixtures__/example-todo-main/web/src/graphql/fragment-masking.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { ResultOf, DocumentTypeDecoration, TypedDocumentNode } from '@graphql-typed-document-node/core';
import { FragmentDefinitionNode } from 'graphql';
import { Incremental } from './graphql';


export type FragmentType<TDocumentType extends DocumentTypeDecoration<any, any>> = TDocumentType extends DocumentTypeDecoration<
infer TType,
any
>
? [TType] extends [{ ' $fragmentName'?: infer TKey }]
? TKey extends string
? { ' $fragmentRefs'?: { [key in TKey]: TType } }
: never
: never
: never;

// return non-nullable if `fragmentType` is non-nullable
export function useFragment<TType>(
_documentNode: DocumentTypeDecoration<TType, any>,
fragmentType: FragmentType<DocumentTypeDecoration<TType, any>>
): TType;
// return nullable if `fragmentType` is nullable
export function useFragment<TType>(
_documentNode: DocumentTypeDecoration<TType, any>,
fragmentType: FragmentType<DocumentTypeDecoration<TType, any>> | null | undefined
): TType | null | undefined;
// return array of non-nullable if `fragmentType` is array of non-nullable
export function useFragment<TType>(
_documentNode: DocumentTypeDecoration<TType, any>,
fragmentType: ReadonlyArray<FragmentType<DocumentTypeDecoration<TType, any>>>
): ReadonlyArray<TType>;
// return array of nullable if `fragmentType` is array of nullable
export function useFragment<TType>(
_documentNode: DocumentTypeDecoration<TType, any>,
fragmentType: ReadonlyArray<FragmentType<DocumentTypeDecoration<TType, any>>> | null | undefined
): ReadonlyArray<TType> | null | undefined;
export function useFragment<TType>(
_documentNode: DocumentTypeDecoration<TType, any>,
fragmentType: FragmentType<DocumentTypeDecoration<TType, any>> | ReadonlyArray<FragmentType<DocumentTypeDecoration<TType, any>>> | null | undefined
): TType | ReadonlyArray<TType> | null | undefined {
return fragmentType as any;
}


export function makeFragmentData<
F extends DocumentTypeDecoration<any, any>,
FT extends ResultOf<F>
>(data: FT, _fragment: F): FragmentType<F> {
return data as FragmentType<F>;
}
export function isFragmentReady<TQuery, TFrag>(
queryNode: DocumentTypeDecoration<TQuery, any>,
fragmentNode: TypedDocumentNode<TFrag>,
data: FragmentType<TypedDocumentNode<Incremental<TFrag>, any>> | null | undefined
): data is FragmentType<typeof fragmentNode> {
const deferredFields = (queryNode as { __meta__?: { deferredFields: Record<string, (keyof TFrag)[]> } }).__meta__
?.deferredFields;

if (!deferredFields) return true;

const fragDef = fragmentNode.definitions[0] as FragmentDefinitionNode | undefined;
const fragName = fragDef?.name?.value;

const fields = (fragName && deferredFields[fragName]) || [];
return fields.length > 0 && fields.every(field => data && field in data);
}
73 changes: 73 additions & 0 deletions __fixtures__/example-todo-main/web/src/graphql/gql.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/* eslint-disable */
import * as types from "./graphql";
import { TypedDocumentNode as DocumentNode } from "@graphql-typed-document-node/core";

/**
* Map of all GraphQL operations in the project.
*
* This map has several performance disadvantages:
* 1. It is not tree-shakeable, so it will include all operations in the project.
* 2. It is not minifiable, so the string of a GraphQL query will be multiple times inside the bundle.
* 3. It does not support dead code elimination, so it will add unused operations.
*
* Therefore it is highly recommended to use the babel or swc plugin for production.
*/
const documents = {
"\n mutation AddTodo_CreateTodo($body: String!) {\n createTodo(body: $body) {\n id\n __typename\n body\n status\n }\n }\n":
types.AddTodo_CreateTodoDocument,
"\n query NumTodosCell_GetCount {\n todosCount\n }\n":
types.NumTodosCell_GetCountDocument,
"\n query TodoListCell_GetTodos {\n todos {\n id\n body\n status\n }\n }\n":
types.TodoListCell_GetTodosDocument,
"\n mutation TodoListCell_CheckTodo($id: Int!, $status: String!) {\n updateTodoStatus(id: $id, status: $status) {\n id\n __typename\n status\n }\n }\n":
types.TodoListCell_CheckTodoDocument,
};

/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*
*
* @example
* ```ts
* const query = graphql(`query GetUser($id: ID!) { user(id: $id) { name } }`);
* ```
*
* The query argument is unknown!
* Please regenerate the types.
*/
export function graphql(source: string): unknown;

/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(
source: "\n mutation AddTodo_CreateTodo($body: String!) {\n createTodo(body: $body) {\n id\n __typename\n body\n status\n }\n }\n"
): (typeof documents)["\n mutation AddTodo_CreateTodo($body: String!) {\n createTodo(body: $body) {\n id\n __typename\n body\n status\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(
source: "\n query NumTodosCell_GetCount {\n todosCount\n }\n"
): (typeof documents)["\n query NumTodosCell_GetCount {\n todosCount\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(
source: "\n query TodoListCell_GetTodos {\n todos {\n id\n body\n status\n }\n }\n"
): (typeof documents)["\n query TodoListCell_GetTodos {\n todos {\n id\n body\n status\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(
source: "\n mutation TodoListCell_CheckTodo($id: Int!, $status: String!) {\n updateTodoStatus(id: $id, status: $status) {\n id\n __typename\n status\n }\n }\n"
): (typeof documents)["\n mutation TodoListCell_CheckTodo($id: Int!, $status: String!) {\n updateTodoStatus(id: $id, status: $status) {\n id\n __typename\n status\n }\n }\n"];

export function graphql(source: string) {
return (documents as any)[source] ?? {};
}

export type DocumentType<TDocumentNode extends DocumentNode<any, any>> =
TDocumentNode extends DocumentNode<infer TType, any> ? TType : never;
export function gql(source: string) {
return graphql(source);
}
110 changes: 110 additions & 0 deletions __fixtures__/example-todo-main/web/src/graphql/graphql.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/* eslint-disable */
import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core';
export type Maybe<T> = T | null;
export type InputMaybe<T> = Maybe<T>;
export type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] };
export type MakeOptional<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]?: Maybe<T[SubKey]> };
export type MakeMaybe<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]: Maybe<T[SubKey]> };
export type MakeEmpty<T extends { [key: string]: unknown }, K extends keyof T> = { [_ in K]?: never };
export type Incremental<T> = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never };
/** All built-in and custom scalars, mapped to their actual values */
export type Scalars = {
ID: { input: string; output: string; }
String: { input: string; output: string; }
Boolean: { input: boolean; output: boolean; }
Int: { input: number; output: number; }
Float: { input: number; output: number; }
BigInt: { input: any; output: any; }
Date: { input: any; output: any; }
DateTime: { input: any; output: any; }
JSON: { input: any; output: any; }
JSONObject: { input: any; output: any; }
Time: { input: any; output: any; }
};

export type Mutation = {
__typename?: 'Mutation';
createTodo?: Maybe<Todo>;
renameTodo?: Maybe<Todo>;
updateTodoStatus?: Maybe<Todo>;
};


export type MutationCreateTodoArgs = {
body: Scalars['String']['input'];
};


export type MutationRenameTodoArgs = {
body: Scalars['String']['input'];
id: Scalars['Int']['input'];
};


export type MutationUpdateTodoStatusArgs = {
id: Scalars['Int']['input'];
status: Scalars['String']['input'];
};

/** About the Redwood queries. */
export type Query = {
__typename?: 'Query';
currentUser?: Maybe<Scalars['JSON']['output']>;
/** Fetches the Redwood root schema. */
redwood?: Maybe<Redwood>;
todos?: Maybe<Array<Maybe<Todo>>>;
todosCount: Scalars['Int']['output'];
};

/**
* The RedwoodJS Root Schema
*
* Defines details about RedwoodJS such as the current user and version information.
*/
export type Redwood = {
__typename?: 'Redwood';
/** The current user. */
currentUser?: Maybe<Scalars['JSON']['output']>;
/** The version of Prisma. */
prismaVersion?: Maybe<Scalars['String']['output']>;
/** The version of Redwood. */
version?: Maybe<Scalars['String']['output']>;
};

export type Todo = {
__typename?: 'Todo';
body: Scalars['String']['output'];
id: Scalars['Int']['output'];
status: Scalars['String']['output'];
};

export type AddTodo_CreateTodoMutationVariables = Exact<{
body: Scalars['String']['input'];
}>;


export type AddTodo_CreateTodoMutation = { __typename: 'Mutation', createTodo?: { __typename: 'Todo', id: number, body: string, status: string } | null };

export type NumTodosCell_GetCountQueryVariables = Exact<{ [key: string]: never; }>;


export type NumTodosCell_GetCountQuery = { __typename: 'Query', todosCount: number };

export type TodoListCell_GetTodosQueryVariables = Exact<{ [key: string]: never; }>;


export type TodoListCell_GetTodosQuery = { __typename: 'Query', todos?: Array<{ __typename: 'Todo', id: number, body: string, status: string } | null> | null };

export type TodoListCell_CheckTodoMutationVariables = Exact<{
id: Scalars['Int']['input'];
status: Scalars['String']['input'];
}>;


export type TodoListCell_CheckTodoMutation = { __typename: 'Mutation', updateTodoStatus?: { __typename: 'Todo', id: number, status: string } | null };


export const AddTodo_CreateTodoDocument = {"__meta__":{"hash":"d67f5d54ba7d2a94e34809f20a0380f9921a5586"},"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"AddTodo_CreateTodo"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"body"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"__typename"}},{"kind":"Field","name":{"kind":"Name","value":"createTodo"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"body"},"value":{"kind":"Variable","name":{"kind":"Name","value":"body"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"__typename"}},{"kind":"Field","name":{"kind":"Name","value":"body"}},{"kind":"Field","name":{"kind":"Name","value":"status"}}]}}]}}]} as unknown as DocumentNode<AddTodo_CreateTodoMutation, AddTodo_CreateTodoMutationVariables>;
export const NumTodosCell_GetCountDocument = {"__meta__":{"hash":"81a7e7b720f992f8cfcaab15f42cf5a6802ed338"},"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"NumTodosCell_GetCount"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"__typename"}},{"kind":"Field","name":{"kind":"Name","value":"todosCount"}}]}}]} as unknown as DocumentNode<NumTodosCell_GetCountQuery, NumTodosCell_GetCountQueryVariables>;
export const TodoListCell_GetTodosDocument = {"__meta__":{"hash":"a9d0f2c090ac4320919f631ab0003fcdd2c30652"},"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"TodoListCell_GetTodos"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"__typename"}},{"kind":"Field","name":{"kind":"Name","value":"todos"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"__typename"}},{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"body"}},{"kind":"Field","name":{"kind":"Name","value":"status"}}]}}]}}]} as unknown as DocumentNode<TodoListCell_GetTodosQuery, TodoListCell_GetTodosQueryVariables>;
export const TodoListCell_CheckTodoDocument = {"__meta__":{"hash":"69a8d2c6640912a8323a729adae2cc2f2f1bdb59"},"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"TodoListCell_CheckTodo"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"status"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"__typename"}},{"kind":"Field","name":{"kind":"Name","value":"updateTodoStatus"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}},{"kind":"Argument","name":{"kind":"Name","value":"status"},"value":{"kind":"Variable","name":{"kind":"Name","value":"status"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"__typename"}},{"kind":"Field","name":{"kind":"Name","value":"status"}}]}}]}}]} as unknown as DocumentNode<TodoListCell_CheckTodoMutation, TodoListCell_CheckTodoMutationVariables>;
2 changes: 2 additions & 0 deletions __fixtures__/example-todo-main/web/src/graphql/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from "./fragment-masking";
export * from "./gql";
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"d67f5d54ba7d2a94e34809f20a0380f9921a5586": "mutation AddTodo_CreateTodo($body: String!) { __typename createTodo(body: $body) { __typename body id status } }",
"81a7e7b720f992f8cfcaab15f42cf5a6802ed338": "query NumTodosCell_GetCount { __typename todosCount }",
"a9d0f2c090ac4320919f631ab0003fcdd2c30652": "query TodoListCell_GetTodos { __typename todos { __typename body id status } }",
"69a8d2c6640912a8323a729adae2cc2f2f1bdb59": "mutation TodoListCell_CheckTodo($id: Int!, $status: String!) { __typename updateTodoStatus(id: $id, status: $status) { __typename id status } }"
}
75 changes: 75 additions & 0 deletions __fixtures__/example-todo-main/web/src/types/types.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
export type Maybe<T> = T | null;
export type InputMaybe<T> = Maybe<T>;
export type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] };
export type MakeOptional<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]?: Maybe<T[SubKey]> };
export type MakeMaybe<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]: Maybe<T[SubKey]> };
/** All built-in and custom scalars, mapped to their actual values */
export type Scalars = {
ID: { input: string; output: string; }
String: { input: string; output: string; }
Boolean: { input: boolean; output: boolean; }
Int: { input: number; output: number; }
Float: { input: number; output: number; }
BigInt: { input: any; output: any; }
Date: { input: any; output: any; }
DateTime: { input: any; output: any; }
JSON: { input: any; output: any; }
JSONObject: { input: any; output: any; }
Time: { input: any; output: any; }
};

export type Mutation = {
__typename?: 'Mutation';
createTodo?: Maybe<Todo>;
renameTodo?: Maybe<Todo>;
updateTodoStatus?: Maybe<Todo>;
};


export type MutationCreateTodoArgs = {
body: Scalars['String']['input'];
};


export type MutationRenameTodoArgs = {
body: Scalars['String']['input'];
id: Scalars['Int']['input'];
};


export type MutationUpdateTodoStatusArgs = {
id: Scalars['Int']['input'];
status: Scalars['String']['input'];
};

/** About the Redwood queries. */
export type Query = {
__typename?: 'Query';
currentUser?: Maybe<Scalars['JSON']['output']>;
/** Fetches the Redwood root schema. */
redwood?: Maybe<Redwood>;
todos?: Maybe<Array<Maybe<Todo>>>;
todosCount: Scalars['Int']['output'];
};

/**
* The RedwoodJS Root Schema
*
* Defines details about RedwoodJS such as the current user and version information.
*/
export type Redwood = {
__typename?: 'Redwood';
/** The current user. */
currentUser?: Maybe<Scalars['JSON']['output']>;
/** The version of Prisma. */
prismaVersion?: Maybe<Scalars['String']['output']>;
/** The version of Redwood. */
version?: Maybe<Scalars['String']['output']>;
};

export type Todo = {
__typename?: 'Todo';
body: Scalars['String']['output'];
id: Scalars['Int']['output'];
status: Scalars['String']['output'];
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"GetGroceries": "9198a2438e6e5dbcf42362657baca24494a9f6cf83a6033983d2a978c1c1c703",
"GetProduce": "a8ee227d80bda6e1f785083aac537e8f1cd0340e0b52faaa27e18dbe4d629241"
}
2 changes: 2 additions & 0 deletions __fixtures__/fragment-test-project/redwood.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
port = 8910
apiUrl = "/.redwood/functions" # you can customise graphql and dbauth urls individually too: see https://redwoodjs.com/docs/app-configuration-redwood-toml#api-paths
includeEnvironmentVariables = [] # any ENV vars that should be available to the web side, see https://redwoodjs.com/docs/environment-variables#web
[graphql]
fragments = true
[api]
port = 8911
[browser]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"GetGroceries": "9198a2438e6e5dbcf42362657baca24494a9f6cf83a6033983d2a978c1c1c703",
"GetProduce": "a8ee227d80bda6e1f785083aac537e8f1cd0340e0b52faaa27e18dbe4d629241"
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ export interface PossibleTypesResultData {
}
}
const result: PossibleTypesResultData = {
possibleTypes: {},
possibleTypes: {
Groceries: ['Fruit', 'Vegetable'],
Grocery: ['Fruit', 'Vegetable'],
},
}
export default result
Loading

0 comments on commit a1de078

Please sign in to comment.