diff --git a/.run/start - auth.run.xml b/.run/start - auth.run.xml
new file mode 100644
index 000000000..83e7e462f
--- /dev/null
+++ b/.run/start - auth.run.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/examples/auth/e2e/fixtures.ts b/examples/auth/e2e/fixtures.ts
new file mode 100644
index 000000000..2baaf34db
--- /dev/null
+++ b/examples/auth/e2e/fixtures.ts
@@ -0,0 +1,59 @@
+import { Connection } from 'typeorm';
+import { SubTaskEntity } from '../src/sub-task/sub-task.entity';
+import { TagEntity } from '../src/tag/tag.entity';
+import { TodoItemEntity } from '../src/todo-item/todo-item.entity';
+import { executeTruncate } from '../../helpers';
+import { UserEntity } from '../src/user/user.entity';
+
+const tables = ['todo_item', 'sub_task', 'tag', 'user'];
+export const truncate = async (connection: Connection): Promise => executeTruncate(connection, tables);
+
+export const refresh = async (connection: Connection): Promise => {
+ await truncate(connection);
+
+ const userRepo = connection.getRepository(UserEntity);
+ const todoRepo = connection.getRepository(TodoItemEntity);
+ const subTaskRepo = connection.getRepository(SubTaskEntity);
+ const tagsRepo = connection.getRepository(TagEntity);
+
+ const users = await userRepo.save([
+ { username: 'nestjs-query', password: '123' },
+ { username: 'nestjs-query-2', password: '123' },
+ { username: 'nestjs-query-3', password: '123' },
+ ]);
+
+ const urgentTag = await tagsRepo.save({ name: 'Urgent' });
+ const homeTag = await tagsRepo.save({ name: 'Home' });
+ const workTag = await tagsRepo.save({ name: 'Work' });
+ const questionTag = await tagsRepo.save({ name: 'Question' });
+ const blockedTag = await tagsRepo.save({ name: 'Blocked' });
+
+ const todoItems: TodoItemEntity[] = await users.reduce(async (prev, user) => {
+ const allTodos = await prev;
+ const userTodos = await todoRepo.save([
+ { title: 'Create Nest App', completed: true, priority: 0, tags: [urgentTag, homeTag], owner: user },
+ { title: 'Create Entity', completed: false, priority: 1, tags: [urgentTag, workTag], owner: user },
+ { title: 'Create Entity Service', completed: false, priority: 2, tags: [blockedTag, workTag], owner: user },
+ { title: 'Add Todo Item Resolver', completed: false, priority: 3, tags: [blockedTag, homeTag], owner: user },
+ {
+ title: 'How to create item With Sub Tasks',
+ completed: false,
+ priority: 4,
+ tags: [questionTag, blockedTag],
+ owner: user,
+ },
+ ]);
+ return [...allTodos, ...userTodos];
+ }, Promise.resolve([] as TodoItemEntity[]));
+
+ await subTaskRepo.save(
+ todoItems.reduce((subTasks, todo) => {
+ return [
+ ...subTasks,
+ { completed: true, title: `${todo.title} - Sub Task 1`, todoItem: todo },
+ { completed: false, title: `${todo.title} - Sub Task 2`, todoItem: todo },
+ { completed: false, title: `${todo.title} - Sub Task 3`, todoItem: todo },
+ ];
+ }, [] as Partial[]),
+ );
+};
diff --git a/examples/auth/e2e/graphql-fragments.ts b/examples/auth/e2e/graphql-fragments.ts
new file mode 100644
index 000000000..c62e4e97c
--- /dev/null
+++ b/examples/auth/e2e/graphql-fragments.ts
@@ -0,0 +1,118 @@
+export const todoItemFields = `
+ id
+ title
+ completed
+ description
+ age
+ `;
+
+export const subTaskFields = `
+id
+title
+description
+completed
+todoItemId
+`;
+
+export const tagFields = `
+id
+name
+`;
+
+export const pageInfoField = `
+pageInfo{
+ hasNextPage
+ hasPreviousPage
+ startCursor
+ endCursor
+}
+`;
+
+export const edgeNodes = (fields: string): string => {
+ return `
+ edges {
+ node{
+ ${fields}
+ }
+ cursor
+ }
+ `;
+};
+
+export const todoItemAggregateFields = `
+count {
+ id
+ title
+ description
+ completed
+ created
+ updated
+}
+sum {
+ id
+}
+avg {
+ id
+}
+min {
+ id
+ title
+ description
+}
+max {
+ id
+ title
+ description
+}
+`;
+
+export const tagAggregateFields = `
+count {
+ id
+ name
+ created
+ updated
+}
+sum {
+ id
+}
+avg {
+ id
+}
+min {
+ id
+ name
+}
+max {
+ id
+ name
+}
+`;
+
+export const subTaskAggregateFields = `
+count {
+ id
+ title
+ description
+ completed
+ todoItemId
+}
+sum {
+ id
+}
+avg {
+ id
+}
+min {
+ id
+ title
+ description
+ todoItemId
+}
+max {
+ id
+ title
+ description
+ todoItemId
+}
+`;
diff --git a/examples/auth/e2e/sub-task.resolver.spec.ts b/examples/auth/e2e/sub-task.resolver.spec.ts
new file mode 100644
index 000000000..413642674
--- /dev/null
+++ b/examples/auth/e2e/sub-task.resolver.spec.ts
@@ -0,0 +1,865 @@
+import { AggregateResponse } from '@nestjs-query/core';
+import { CursorConnectionType } from '@nestjs-query/query-graphql';
+import { Test } from '@nestjs/testing';
+import request from 'supertest';
+import { INestApplication, ValidationPipe } from '@nestjs/common';
+import { Connection } from 'typeorm';
+import { AppModule } from '../src/app.module';
+import { SubTaskDTO } from '../src/sub-task/dto/sub-task.dto';
+import { TodoItemDTO } from '../src/todo-item/dto/todo-item.dto';
+import { refresh } from './fixtures';
+import { edgeNodes, pageInfoField, subTaskAggregateFields, subTaskFields, todoItemFields } from './graphql-fragments';
+
+describe('SubTaskResolver (auth - e2e)', () => {
+ let app: INestApplication;
+
+ beforeAll(async () => {
+ const moduleRef = await Test.createTestingModule({
+ imports: [AppModule],
+ }).compile();
+
+ app = moduleRef.createNestApplication();
+ app.useGlobalPipes(
+ new ValidationPipe({
+ transform: true,
+ whitelist: true,
+ forbidNonWhitelisted: true,
+ skipMissingProperties: false,
+ forbidUnknownValues: true,
+ }),
+ );
+
+ await app.init();
+ await refresh(app.get(Connection));
+ });
+
+ afterAll(() => refresh(app.get(Connection)));
+
+ const subTasks = [
+ { id: '1', title: 'Create Nest App - Sub Task 1', completed: true, description: null, todoItemId: '1' },
+ { id: '2', title: 'Create Nest App - Sub Task 2', completed: false, description: null, todoItemId: '1' },
+ { id: '3', title: 'Create Nest App - Sub Task 3', completed: false, description: null, todoItemId: '1' },
+ { id: '4', title: 'Create Entity - Sub Task 1', completed: true, description: null, todoItemId: '2' },
+ { id: '5', title: 'Create Entity - Sub Task 2', completed: false, description: null, todoItemId: '2' },
+ { id: '6', title: 'Create Entity - Sub Task 3', completed: false, description: null, todoItemId: '2' },
+ {
+ id: '7',
+ title: 'Create Entity Service - Sub Task 1',
+ completed: true,
+ description: null,
+ todoItemId: '3',
+ },
+ {
+ id: '8',
+ title: 'Create Entity Service - Sub Task 2',
+ completed: false,
+ description: null,
+ todoItemId: '3',
+ },
+ {
+ id: '9',
+ title: 'Create Entity Service - Sub Task 3',
+ completed: false,
+ description: null,
+ todoItemId: '3',
+ },
+ {
+ id: '10',
+ title: 'Add Todo Item Resolver - Sub Task 1',
+ completed: true,
+ description: null,
+ todoItemId: '4',
+ },
+ {
+ completed: false,
+ description: null,
+ id: '11',
+ title: 'Add Todo Item Resolver - Sub Task 2',
+ todoItemId: '4',
+ },
+ {
+ completed: false,
+ description: null,
+ id: '12',
+ title: 'Add Todo Item Resolver - Sub Task 3',
+ todoItemId: '4',
+ },
+ {
+ completed: true,
+ description: null,
+ id: '13',
+ title: 'How to create item With Sub Tasks - Sub Task 1',
+ todoItemId: '5',
+ },
+ {
+ completed: false,
+ description: null,
+ id: '14',
+ title: 'How to create item With Sub Tasks - Sub Task 2',
+ todoItemId: '5',
+ },
+ {
+ completed: false,
+ description: null,
+ id: '15',
+ title: 'How to create item With Sub Tasks - Sub Task 3',
+ todoItemId: '5',
+ },
+ ];
+
+ describe('find one', () => {
+ it(`should a sub task by id`, () => {
+ return request(app.getHttpServer())
+ .post('/graphql')
+ .send({
+ operationName: null,
+ variables: {},
+ query: `{
+ subTask(id: 1) {
+ ${subTaskFields}
+ }
+ }`,
+ })
+ .expect(200)
+ .then(({ body }) => {
+ expect(body).toEqual({
+ data: {
+ subTask: {
+ id: '1',
+ title: 'Create Nest App - Sub Task 1',
+ completed: true,
+ description: null,
+ todoItemId: '1',
+ },
+ },
+ });
+ });
+ });
+
+ it(`should return null if the sub task is not found`, () => {
+ return request(app.getHttpServer())
+ .post('/graphql')
+ .send({
+ operationName: null,
+ variables: {},
+ query: `{
+ subTask(id: 100) {
+ ${subTaskFields}
+ }
+ }`,
+ })
+ .expect(200, {
+ data: {
+ subTask: null,
+ },
+ });
+ });
+
+ it(`should return a todo item`, () => {
+ return request(app.getHttpServer())
+ .post('/graphql')
+ .send({
+ operationName: null,
+ variables: {},
+ query: `{
+ subTask(id: 1) {
+ todoItem {
+ ${todoItemFields}
+ }
+ }
+ }`,
+ })
+ .expect(200)
+ .then(({ body }) => {
+ expect(body).toEqual({
+ data: {
+ subTask: {
+ todoItem: {
+ id: '1',
+ title: 'Create Nest App',
+ completed: true,
+ description: null,
+ age: expect.any(Number),
+ },
+ },
+ },
+ });
+ });
+ });
+ });
+
+ describe('query', () => {
+ it(`should return a connection`, () => {
+ return request(app.getHttpServer())
+ .post('/graphql')
+ .send({
+ operationName: null,
+ variables: {},
+ query: `{
+ subTasks {
+ ${pageInfoField}
+ ${edgeNodes(subTaskFields)}
+ totalCount
+ }
+ }`,
+ })
+ .expect(200)
+ .then(({ body }) => {
+ const { edges, pageInfo, totalCount }: CursorConnectionType = body.data.subTasks;
+ expect(pageInfo).toEqual({
+ endCursor: 'YXJyYXljb25uZWN0aW9uOjk=',
+ hasNextPage: true,
+ hasPreviousPage: false,
+ startCursor: 'YXJyYXljb25uZWN0aW9uOjA=',
+ });
+ expect(totalCount).toBe(15);
+ expect(edges).toHaveLength(10);
+ expect(edges.map((e) => e.node)).toEqual(subTasks.slice(0, 10));
+ });
+ });
+
+ it(`should allow querying`, () => {
+ return request(app.getHttpServer())
+ .post('/graphql')
+ .send({
+ operationName: null,
+ variables: {},
+ query: `{
+ subTasks(filter: { id: { in: [1, 2, 3] } }) {
+ ${pageInfoField}
+ ${edgeNodes(subTaskFields)}
+ totalCount
+ }
+ }`,
+ })
+ .expect(200)
+ .then(({ body }) => {
+ const { edges, pageInfo, totalCount }: CursorConnectionType = body.data.subTasks;
+ expect(pageInfo).toEqual({
+ endCursor: 'YXJyYXljb25uZWN0aW9uOjI=',
+ hasNextPage: false,
+ hasPreviousPage: false,
+ startCursor: 'YXJyYXljb25uZWN0aW9uOjA=',
+ });
+ expect(totalCount).toBe(3);
+ expect(edges).toHaveLength(3);
+ expect(edges.map((e) => e.node)).toEqual(subTasks.slice(0, 3));
+ });
+ });
+
+ it(`should allow querying on todoItem`, () => {
+ return request(app.getHttpServer())
+ .post('/graphql')
+ .send({
+ operationName: null,
+ variables: {},
+ query: `{
+ subTasks(filter: { todoItem: { title: { like: "Create Entity%" } } }) {
+ ${pageInfoField}
+ ${edgeNodes(subTaskFields)}
+ totalCount
+ }
+ }`,
+ })
+ .expect(200)
+ .then(({ body }) => {
+ const { edges, pageInfo, totalCount }: CursorConnectionType = body.data.subTasks;
+ expect(pageInfo).toEqual({
+ endCursor: 'YXJyYXljb25uZWN0aW9uOjU=',
+ hasNextPage: false,
+ hasPreviousPage: false,
+ startCursor: 'YXJyYXljb25uZWN0aW9uOjA=',
+ });
+ expect(totalCount).toBe(6);
+ expect(edges).toHaveLength(6);
+ expect(edges.map((e) => e.node)).toEqual(subTasks.slice(3, 9));
+ });
+ });
+
+ it(`should allow sorting`, () => {
+ return request(app.getHttpServer())
+ .post('/graphql')
+ .send({
+ operationName: null,
+ variables: {},
+ query: `{
+ subTasks(sorting: [{field: id, direction: DESC}]) {
+ ${pageInfoField}
+ ${edgeNodes(subTaskFields)}
+ totalCount
+ }
+ }`,
+ })
+ .expect(200)
+ .then(({ body }) => {
+ const { edges, pageInfo, totalCount }: CursorConnectionType = body.data.subTasks;
+ expect(pageInfo).toEqual({
+ endCursor: 'YXJyYXljb25uZWN0aW9uOjk=',
+ hasNextPage: true,
+ hasPreviousPage: false,
+ startCursor: 'YXJyYXljb25uZWN0aW9uOjA=',
+ });
+ expect(totalCount).toBe(15);
+ expect(edges).toHaveLength(10);
+ expect(edges.map((e) => e.node)).toEqual(subTasks.slice().reverse().slice(0, 10));
+ });
+ });
+
+ describe('paging', () => {
+ it(`should allow paging with the 'first' field`, () => {
+ return request(app.getHttpServer())
+ .post('/graphql')
+ .send({
+ operationName: null,
+ variables: {},
+ query: `{
+ subTasks(paging: {first: 2}) {
+ ${pageInfoField}
+ ${edgeNodes(subTaskFields)}
+ totalCount
+ }
+ }`,
+ })
+ .expect(200)
+ .then(({ body }) => {
+ const { edges, pageInfo, totalCount }: CursorConnectionType = body.data.subTasks;
+ expect(pageInfo).toEqual({
+ endCursor: 'YXJyYXljb25uZWN0aW9uOjE=',
+ hasNextPage: true,
+ hasPreviousPage: false,
+ startCursor: 'YXJyYXljb25uZWN0aW9uOjA=',
+ });
+ expect(totalCount).toBe(15);
+ expect(edges).toHaveLength(2);
+ expect(edges.map((e) => e.node)).toEqual(subTasks.slice(0, 2));
+ });
+ });
+
+ it(`should allow paging with the 'first' field and 'after'`, () => {
+ return request(app.getHttpServer())
+ .post('/graphql')
+ .send({
+ operationName: null,
+ variables: {},
+ query: `{
+ subTasks(paging: {first: 2, after: "YXJyYXljb25uZWN0aW9uOjE="}) {
+ ${pageInfoField}
+ ${edgeNodes(subTaskFields)}
+ totalCount
+ }
+ }`,
+ })
+ .expect(200)
+ .then(({ body }) => {
+ const { edges, pageInfo, totalCount }: CursorConnectionType = body.data.subTasks;
+ expect(pageInfo).toEqual({
+ endCursor: 'YXJyYXljb25uZWN0aW9uOjM=',
+ hasNextPage: true,
+ hasPreviousPage: true,
+ startCursor: 'YXJyYXljb25uZWN0aW9uOjI=',
+ });
+ expect(totalCount).toBe(15);
+ expect(edges).toHaveLength(2);
+ expect(edges.map((e) => e.node)).toEqual(subTasks.slice(2, 4));
+ });
+ });
+ });
+ });
+
+ describe('aggregate', () => {
+ it(`should return a aggregate response`, () => {
+ return request(app.getHttpServer())
+ .post('/graphql')
+ .send({
+ operationName: null,
+ variables: {},
+ query: `{
+ subTaskAggregate {
+ ${subTaskAggregateFields}
+ }
+ }`,
+ })
+ .expect(200)
+ .then(({ body }) => {
+ const res: AggregateResponse = body.data.subTaskAggregate;
+ expect(res).toEqual({
+ count: { id: 15, title: 15, description: 0, completed: 15, todoItemId: 15 },
+ sum: { id: 120 },
+ avg: { id: 8 },
+ min: { id: '1', title: 'Add Todo Item Resolver - Sub Task 1', description: null, todoItemId: '1' },
+ max: {
+ id: '15',
+ title: 'How to create item With Sub Tasks - Sub Task 3',
+ description: null,
+ todoItemId: '5',
+ },
+ });
+ });
+ });
+
+ it(`should allow filtering`, () => {
+ return request(app.getHttpServer())
+ .post('/graphql')
+ .send({
+ operationName: null,
+ variables: {},
+ query: `{
+ subTaskAggregate(filter: {completed: {is: true}}) {
+ ${subTaskAggregateFields}
+ }
+ }`,
+ })
+ .expect(200)
+ .then(({ body }) => {
+ const res: AggregateResponse = body.data.subTaskAggregate;
+ expect(res).toEqual({
+ count: { id: 5, title: 5, description: 0, completed: 5, todoItemId: 5 },
+ sum: { id: 35 },
+ avg: { id: 7 },
+ min: { id: '1', title: 'Add Todo Item Resolver - Sub Task 1', description: null, todoItemId: '1' },
+ max: {
+ id: '13',
+ title: 'How to create item With Sub Tasks - Sub Task 1',
+ description: null,
+ todoItemId: '5',
+ },
+ });
+ });
+ });
+ });
+
+ describe('create one', () => {
+ it('should allow creating a subTask', () => {
+ return request(app.getHttpServer())
+ .post('/graphql')
+ .send({
+ operationName: null,
+ variables: {},
+ query: `mutation {
+ createOneSubTask(
+ input: {
+ subTask: { title: "Test SubTask", completed: false, todoItemId: "1" }
+ }
+ ) {
+ ${subTaskFields}
+ }
+ }`,
+ })
+ .expect(200, {
+ data: {
+ createOneSubTask: {
+ id: '16',
+ title: 'Test SubTask',
+ description: null,
+ completed: false,
+ todoItemId: '1',
+ },
+ },
+ });
+ });
+
+ it('should validate a subTask', () => {
+ return request(app.getHttpServer())
+ .post('/graphql')
+ .send({
+ operationName: null,
+ variables: {},
+ query: `mutation {
+ createOneSubTask(
+ input: {
+ subTask: { title: "", completed: false, todoItemId: "1" }
+ }
+ ) {
+ ${subTaskFields}
+ }
+ }`,
+ })
+ .expect(200)
+ .then(({ body }) => {
+ expect(body.errors).toHaveLength(1);
+ expect(JSON.stringify(body.errors[0])).toContain('title should not be empty');
+ });
+ });
+ });
+
+ describe('create many', () => {
+ it('should allow creating a subTask', () => {
+ return request(app.getHttpServer())
+ .post('/graphql')
+ .send({
+ operationName: null,
+ variables: {},
+ query: `mutation {
+ createManySubTasks(
+ input: {
+ subTasks: [
+ { title: "Test Create Many SubTask - 1", completed: false, todoItemId: "2" },
+ { title: "Test Create Many SubTask - 2", completed: true, todoItemId: "2" },
+ ]
+ }
+ ) {
+ ${subTaskFields}
+ }
+ }`,
+ })
+ .expect(200, {
+ data: {
+ createManySubTasks: [
+ { id: '17', title: 'Test Create Many SubTask - 1', description: null, completed: false, todoItemId: '2' },
+ { id: '18', title: 'Test Create Many SubTask - 2', description: null, completed: true, todoItemId: '2' },
+ ],
+ },
+ });
+ });
+
+ it('should validate a subTask', () => {
+ return request(app.getHttpServer())
+ .post('/graphql')
+ .send({
+ operationName: null,
+ variables: {},
+ query: `mutation {
+ createManySubTasks(
+ input: {
+ subTasks: [{ title: "", completed: false, todoItemId: "2" }]
+ }
+ ) {
+ ${subTaskFields}
+ }
+ }`,
+ })
+ .expect(200)
+ .then(({ body }) => {
+ expect(body.errors).toHaveLength(1);
+ expect(JSON.stringify(body.errors[0])).toContain('title should not be empty');
+ });
+ });
+ });
+
+ describe('update one', () => {
+ it('should allow updating a subTask', () => {
+ return request(app.getHttpServer())
+ .post('/graphql')
+ .send({
+ operationName: null,
+ variables: {},
+ query: `mutation {
+ updateOneSubTask(
+ input: {
+ id: "16",
+ update: { title: "Update Test Sub Task", completed: true }
+ }
+ ) {
+ ${subTaskFields}
+ }
+ }`,
+ })
+ .expect(200, {
+ data: {
+ updateOneSubTask: {
+ id: '16',
+ title: 'Update Test Sub Task',
+ description: null,
+ completed: true,
+ todoItemId: '1',
+ },
+ },
+ });
+ });
+
+ it('should require an id', () => {
+ return request(app.getHttpServer())
+ .post('/graphql')
+ .send({
+ operationName: null,
+ variables: {},
+ query: `mutation {
+ updateOneSubTask(
+ input: {
+ update: { title: "Update Test Sub Task" }
+ }
+ ) {
+ id
+ title
+ completed
+ }
+ }`,
+ })
+ .expect(400)
+ .then(({ body }) => {
+ expect(body.errors).toHaveLength(1);
+ expect(body.errors[0].message).toBe(
+ 'Field "UpdateOneSubTaskInput.id" of required type "ID!" was not provided.',
+ );
+ });
+ });
+
+ it('should validate an update', () => {
+ return request(app.getHttpServer())
+ .post('/graphql')
+ .send({
+ operationName: null,
+ variables: {},
+ query: `mutation {
+ updateOneSubTask(
+ input: {
+ id: "16",
+ update: { title: "" }
+ }
+ ) {
+ id
+ title
+ completed
+ }
+ }`,
+ })
+ .expect(200)
+ .then(({ body }) => {
+ expect(body.errors).toHaveLength(1);
+ expect(JSON.stringify(body.errors[0])).toContain('title should not be empty');
+ });
+ });
+ });
+
+ describe('update many', () => {
+ it('should allow updating a subTask', () => {
+ return request(app.getHttpServer())
+ .post('/graphql')
+ .send({
+ operationName: null,
+ variables: {},
+ query: `mutation {
+ updateManySubTasks(
+ input: {
+ filter: {id: { in: ["17", "18"]} },
+ update: { title: "Update Many Test", completed: true }
+ }
+ ) {
+ updatedCount
+ }
+ }`,
+ })
+ .expect(200, {
+ data: {
+ updateManySubTasks: {
+ updatedCount: 2,
+ },
+ },
+ });
+ });
+
+ it('should require a filter', () => {
+ return request(app.getHttpServer())
+ .post('/graphql')
+ .send({
+ operationName: null,
+ variables: {},
+ query: `mutation {
+ updateManySubTasks(
+ input: {
+ update: { title: "Update Many Test", completed: true }
+ }
+ ) {
+ updatedCount
+ }
+ }`,
+ })
+ .expect(400)
+ .then(({ body }) => {
+ expect(body.errors).toHaveLength(1);
+ expect(body.errors[0].message).toBe(
+ 'Field "UpdateManySubTasksInput.filter" of required type "SubTaskUpdateFilter!" was not provided.',
+ );
+ });
+ });
+
+ it('should require a non-empty filter', () => {
+ return request(app.getHttpServer())
+ .post('/graphql')
+ .send({
+ operationName: null,
+ variables: {},
+ query: `mutation {
+ updateManySubTasks(
+ input: {
+ filter: { },
+ update: { title: "Update Many Test", completed: true }
+ }
+ ) {
+ updatedCount
+ }
+ }`,
+ })
+ .expect(200)
+ .then(({ body }) => {
+ expect(body.errors).toHaveLength(1);
+ expect(JSON.stringify(body.errors[0])).toContain('filter must be a non-empty object');
+ });
+ });
+ });
+
+ describe('delete one', () => {
+ it('should allow deleting a subTask', () => {
+ return request(app.getHttpServer())
+ .post('/graphql')
+ .send({
+ operationName: null,
+ variables: {},
+ query: `mutation {
+ deleteOneSubTask(
+ input: { id: "16" }
+ ) {
+ ${subTaskFields}
+ }
+ }`,
+ })
+ .expect(200, {
+ data: {
+ deleteOneSubTask: {
+ id: null,
+ title: 'Update Test Sub Task',
+ completed: true,
+ description: null,
+ todoItemId: '1',
+ },
+ },
+ });
+ });
+
+ it('should require an id', () => {
+ return request(app.getHttpServer())
+ .post('/graphql')
+ .send({
+ operationName: null,
+ variables: {},
+ query: `mutation {
+ deleteOneSubTask(
+ input: { }
+ ) {
+ ${subTaskFields}
+ }
+ }`,
+ })
+ .expect(400)
+ .then(({ body }) => {
+ expect(body.errors).toHaveLength(1);
+ expect(body.errors[0].message).toBe('Field "DeleteOneInput.id" of required type "ID!" was not provided.');
+ });
+ });
+ });
+
+ describe('delete many', () => {
+ it('should allow updating a subTask', () => {
+ return request(app.getHttpServer())
+ .post('/graphql')
+ .send({
+ operationName: null,
+ variables: {},
+ query: `mutation {
+ deleteManySubTasks(
+ input: {
+ filter: {id: { in: ["17", "18"]} },
+ }
+ ) {
+ deletedCount
+ }
+ }`,
+ })
+ .expect(200, {
+ data: {
+ deleteManySubTasks: {
+ deletedCount: 2,
+ },
+ },
+ });
+ });
+
+ it('should require a filter', () => {
+ return request(app.getHttpServer())
+ .post('/graphql')
+ .send({
+ operationName: null,
+ variables: {},
+ query: `mutation {
+ deleteManySubTasks(
+ input: { }
+ ) {
+ deletedCount
+ }
+ }`,
+ })
+ .expect(400)
+ .then(({ body }) => {
+ expect(body.errors).toHaveLength(1);
+ expect(body.errors[0].message).toBe(
+ 'Field "DeleteManySubTasksInput.filter" of required type "SubTaskDeleteFilter!" was not provided.',
+ );
+ });
+ });
+
+ it('should require a non-empty filter', () => {
+ return request(app.getHttpServer())
+ .post('/graphql')
+ .send({
+ operationName: null,
+ variables: {},
+ query: `mutation {
+ deleteManySubTasks(
+ input: {
+ filter: { },
+ }
+ ) {
+ deletedCount
+ }
+ }`,
+ })
+ .expect(200)
+ .then(({ body }) => {
+ expect(body.errors).toHaveLength(1);
+ expect(JSON.stringify(body.errors[0])).toContain('filter must be a non-empty object');
+ });
+ });
+ });
+
+ describe('setTodoItemOnSubTask', () => {
+ it('should set a the todoItem on a subtask', () => {
+ return request(app.getHttpServer())
+ .post('/graphql')
+ .send({
+ operationName: null,
+ variables: {},
+ query: `mutation {
+ setTodoItemOnSubTask(input: { id: "1", relationId: "2" }) {
+ id
+ title
+ todoItem {
+ ${todoItemFields}
+ }
+ }
+ }`,
+ })
+ .expect(200)
+ .then(({ body }) => {
+ expect(body).toEqual({
+ data: {
+ setTodoItemOnSubTask: {
+ id: '1',
+ title: 'Create Nest App - Sub Task 1',
+ todoItem: {
+ id: '2',
+ title: 'Create Entity',
+ completed: false,
+ description: null,
+ age: expect.any(Number),
+ },
+ },
+ },
+ });
+ });
+ });
+ });
+
+ afterAll(async () => {
+ await app.close();
+ });
+});
diff --git a/examples/auth/e2e/tag.resolver.spec.ts b/examples/auth/e2e/tag.resolver.spec.ts
new file mode 100644
index 000000000..b72af3664
--- /dev/null
+++ b/examples/auth/e2e/tag.resolver.spec.ts
@@ -0,0 +1,987 @@
+import { AggregateResponse, getQueryServiceToken, QueryService } from '@nestjs-query/core';
+import { CursorConnectionType } from '@nestjs-query/query-graphql';
+import { Test } from '@nestjs/testing';
+import request from 'supertest';
+import { INestApplication, ValidationPipe } from '@nestjs/common';
+import { Connection } from 'typeorm';
+import { AppModule } from '../src/app.module';
+import { TagDTO } from '../src/tag/dto/tag.dto';
+import { TodoItemDTO } from '../src/todo-item/dto/todo-item.dto';
+import { refresh } from './fixtures';
+import {
+ edgeNodes,
+ pageInfoField,
+ tagFields,
+ todoItemFields,
+ tagAggregateFields,
+ todoItemAggregateFields,
+} from './graphql-fragments';
+import { USER_HEADER_NAME } from '../src/constants';
+import { TagEntity } from '../src/tag/tag.entity';
+
+describe('TagResolver (auth - e2e)', () => {
+ let app: INestApplication;
+
+ beforeAll(async () => {
+ const moduleRef = await Test.createTestingModule({
+ imports: [AppModule],
+ }).compile();
+
+ app = moduleRef.createNestApplication();
+ app.useGlobalPipes(
+ new ValidationPipe({
+ transform: true,
+ whitelist: true,
+ forbidNonWhitelisted: true,
+ skipMissingProperties: false,
+ forbidUnknownValues: true,
+ }),
+ );
+
+ await app.init();
+ await refresh(app.get(Connection));
+ });
+
+ afterAll(() => refresh(app.get(Connection)));
+
+ const tags = [
+ { id: '1', name: 'Urgent' },
+ { id: '2', name: 'Home' },
+ { id: '3', name: 'Work' },
+ { id: '4', name: 'Question' },
+ { id: '5', name: 'Blocked' },
+ ];
+
+ describe('find one', () => {
+ it(`should find a tag by id`, () => {
+ return request(app.getHttpServer())
+ .post('/graphql')
+ .send({
+ operationName: null,
+ variables: {},
+ query: `{
+ tag(id: 1) {
+ ${tagFields}
+ }
+ }`,
+ })
+ .expect(200, { data: { tag: tags[0] } });
+ });
+
+ it(`should return null if the tag is not found`, () => {
+ return request(app.getHttpServer())
+ .post('/graphql')
+ .send({
+ operationName: null,
+ variables: {},
+ query: `{
+ tag(id: 100) {
+ ${tagFields}
+ }
+ }`,
+ })
+ .expect(200, {
+ data: {
+ tag: null,
+ },
+ });
+ });
+
+ it(`should return todoItems as a connection`, () => {
+ return request(app.getHttpServer())
+ .post('/graphql')
+ .send({
+ operationName: null,
+ variables: {},
+ query: `{
+ tag(id: 1) {
+ todoItems(sorting: [{ field: id, direction: ASC }]) {
+ ${pageInfoField}
+ ${edgeNodes('id')}
+ totalCount
+ }
+ }
+ }`,
+ })
+ .expect(200)
+ .then(({ body }) => {
+ const { edges, pageInfo, totalCount }: CursorConnectionType = body.data.tag.todoItems;
+ expect(pageInfo).toEqual({
+ endCursor: 'YXJyYXljb25uZWN0aW9uOjE=',
+ hasNextPage: false,
+ hasPreviousPage: false,
+ startCursor: 'YXJyYXljb25uZWN0aW9uOjA=',
+ });
+ expect(totalCount).toBe(2);
+ expect(edges).toHaveLength(2);
+ expect(edges.map((e) => e.node.id)).toEqual(['1', '2']);
+ });
+ });
+
+ it(`should return todoItems aggregate`, () => {
+ return request(app.getHttpServer())
+ .post('/graphql')
+ .send({
+ operationName: null,
+ variables: {},
+ query: `{
+ tag(id: 1) {
+ todoItemsAggregate {
+ ${todoItemAggregateFields}
+ }
+ }
+ }`,
+ })
+ .expect(200)
+ .then(({ body }) => {
+ const agg: AggregateResponse = body.data.tag.todoItemsAggregate;
+ expect(agg).toEqual({
+ avg: { id: 1.5 },
+ count: { completed: 2, created: 2, description: 0, id: 2, title: 2, updated: 2 },
+ max: { description: null, id: '2', title: 'Create Nest App' },
+ min: { description: null, id: '1', title: 'Create Entity' },
+ sum: { id: 3 },
+ });
+ });
+ });
+ });
+
+ describe('query', () => {
+ it(`should return a connection`, () => {
+ return request(app.getHttpServer())
+ .post('/graphql')
+ .send({
+ operationName: null,
+ variables: {},
+ query: `{
+ tags {
+ ${pageInfoField}
+ ${edgeNodes(tagFields)}
+ totalCount
+ }
+ }`,
+ })
+ .expect(200)
+ .then(({ body }) => {
+ const { edges, pageInfo, totalCount }: CursorConnectionType = body.data.tags;
+ expect(pageInfo).toEqual({
+ endCursor: 'YXJyYXljb25uZWN0aW9uOjQ=',
+ hasNextPage: false,
+ hasPreviousPage: false,
+ startCursor: 'YXJyYXljb25uZWN0aW9uOjA=',
+ });
+ expect(totalCount).toBe(5);
+ expect(edges).toHaveLength(5);
+ expect(edges.map((e) => e.node)).toEqual(tags);
+ });
+ });
+
+ it(`should allow querying`, () => {
+ return request(app.getHttpServer())
+ .post('/graphql')
+ .send({
+ operationName: null,
+ variables: {},
+ query: `{
+ tags(filter: { id: { in: [1, 2, 3] } }) {
+ ${pageInfoField}
+ ${edgeNodes(tagFields)}
+ totalCount
+ }
+ }`,
+ })
+ .expect(200)
+ .then(({ body }) => {
+ const { edges, pageInfo, totalCount }: CursorConnectionType = body.data.tags;
+ expect(pageInfo).toEqual({
+ endCursor: 'YXJyYXljb25uZWN0aW9uOjI=',
+ hasNextPage: false,
+ hasPreviousPage: false,
+ startCursor: 'YXJyYXljb25uZWN0aW9uOjA=',
+ });
+ expect(totalCount).toBe(3);
+ expect(edges).toHaveLength(3);
+ expect(edges.map((e) => e.node)).toEqual(tags.slice(0, 3));
+ });
+ });
+
+ it(`should allow querying on todoItems`, () => {
+ return request(app.getHttpServer())
+ .post('/graphql')
+ .send({
+ operationName: null,
+ variables: {},
+ query: `{
+ tags(filter: { todoItems: { title: { like: "Create Entity%" } } }, sorting: [{field: id, direction: ASC}]) {
+ ${pageInfoField}
+ ${edgeNodes(tagFields)}
+ totalCount
+ }
+ }`,
+ })
+ .expect(200)
+ .then(({ body }) => {
+ const { edges, pageInfo, totalCount }: CursorConnectionType = body.data.tags;
+ expect(pageInfo).toEqual({
+ endCursor: 'YXJyYXljb25uZWN0aW9uOjI=',
+ hasNextPage: false,
+ hasPreviousPage: false,
+ startCursor: 'YXJyYXljb25uZWN0aW9uOjA=',
+ });
+ expect(totalCount).toBe(3);
+ expect(edges).toHaveLength(3);
+ expect(edges.map((e) => e.node)).toEqual([tags[0], tags[2], tags[4]]);
+ });
+ });
+
+ it(`should allow sorting`, () => {
+ return request(app.getHttpServer())
+ .post('/graphql')
+ .send({
+ operationName: null,
+ variables: {},
+ query: `{
+ tags(sorting: [{field: id, direction: DESC}]) {
+ ${pageInfoField}
+ ${edgeNodes(tagFields)}
+ totalCount
+ }
+ }`,
+ })
+ .expect(200)
+ .then(({ body }) => {
+ const { edges, pageInfo, totalCount }: CursorConnectionType = body.data.tags;
+ expect(pageInfo).toEqual({
+ endCursor: 'YXJyYXljb25uZWN0aW9uOjQ=',
+ hasNextPage: false,
+ hasPreviousPage: false,
+ startCursor: 'YXJyYXljb25uZWN0aW9uOjA=',
+ });
+ expect(totalCount).toBe(5);
+ expect(edges).toHaveLength(5);
+ expect(edges.map((e) => e.node)).toEqual(tags.slice().reverse());
+ });
+ });
+
+ describe('paging', () => {
+ it(`should allow paging with the 'first' field`, () => {
+ return request(app.getHttpServer())
+ .post('/graphql')
+ .send({
+ operationName: null,
+ variables: {},
+ query: `{
+ tags(paging: {first: 2}) {
+ ${pageInfoField}
+ ${edgeNodes(tagFields)}
+ totalCount
+ }
+ }`,
+ })
+ .expect(200)
+ .then(({ body }) => {
+ const { edges, pageInfo, totalCount }: CursorConnectionType = body.data.tags;
+ expect(pageInfo).toEqual({
+ endCursor: 'YXJyYXljb25uZWN0aW9uOjE=',
+ hasNextPage: true,
+ hasPreviousPage: false,
+ startCursor: 'YXJyYXljb25uZWN0aW9uOjA=',
+ });
+ expect(totalCount).toBe(5);
+ expect(edges).toHaveLength(2);
+ expect(edges.map((e) => e.node)).toEqual(tags.slice(0, 2));
+ });
+ });
+
+ it(`should allow paging with the 'first' field and 'after'`, () => {
+ return request(app.getHttpServer())
+ .post('/graphql')
+ .send({
+ operationName: null,
+ variables: {},
+ query: `{
+ tags(paging: {first: 2, after: "YXJyYXljb25uZWN0aW9uOjE="}) {
+ ${pageInfoField}
+ ${edgeNodes(tagFields)}
+ totalCount
+ }
+ }`,
+ })
+ .expect(200)
+ .then(({ body }) => {
+ const { edges, pageInfo, totalCount }: CursorConnectionType = body.data.tags;
+ expect(pageInfo).toEqual({
+ endCursor: 'YXJyYXljb25uZWN0aW9uOjM=',
+ hasNextPage: true,
+ hasPreviousPage: true,
+ startCursor: 'YXJyYXljb25uZWN0aW9uOjI=',
+ });
+ expect(totalCount).toBe(5);
+ expect(edges).toHaveLength(2);
+ expect(edges.map((e) => e.node)).toEqual(tags.slice(2, 4));
+ });
+ });
+ });
+ });
+
+ describe('aggregate', () => {
+ it(`should return a aggregate response`, () => {
+ return request(app.getHttpServer())
+ .post('/graphql')
+ .send({
+ operationName: null,
+ variables: {},
+ query: `{
+ tagAggregate {
+ ${tagAggregateFields}
+ }
+ }`,
+ })
+ .expect(200)
+ .then(({ body }) => {
+ const res: AggregateResponse = body.data.tagAggregate;
+ expect(res).toEqual({
+ count: { id: 5, name: 5, created: 5, updated: 5 },
+ sum: { id: 15 },
+ avg: { id: 3 },
+ min: { id: '1', name: 'Blocked' },
+ max: { id: '5', name: 'Work' },
+ });
+ });
+ });
+
+ it(`should allow filtering`, () => {
+ return request(app.getHttpServer())
+ .post('/graphql')
+ .send({
+ operationName: null,
+ variables: {},
+ query: `{
+ tagAggregate(filter: { name: { in: ["Urgent", "Blocked", "Work"] } }) {
+ ${tagAggregateFields}
+ }
+ }`,
+ })
+ .expect(200)
+ .then(({ body }) => {
+ const res: AggregateResponse = body.data.tagAggregate;
+ expect(res).toEqual({
+ count: { id: 3, name: 3, created: 3, updated: 3 },
+ sum: { id: 9 },
+ avg: { id: 3 },
+ min: { id: '1', name: 'Blocked' },
+ max: { id: '5', name: 'Work' },
+ });
+ });
+ });
+ });
+
+ describe('create one', () => {
+ it('should allow creating a tag', () => {
+ return request(app.getHttpServer())
+ .post('/graphql')
+ .send({
+ operationName: null,
+ variables: {},
+ query: `mutation {
+ createOneTag(
+ input: {
+ tag: { name: "Test Tag" }
+ }
+ ) {
+ ${tagFields}
+ }
+ }`,
+ })
+ .expect(200, {
+ data: {
+ createOneTag: {
+ id: '6',
+ name: 'Test Tag',
+ },
+ },
+ });
+ });
+
+ it('should call beforeCreateOne hook when creating a tag', () => {
+ return request(app.getHttpServer())
+ .post('/graphql')
+ .set(USER_HEADER_NAME, 'E2E Test')
+ .send({
+ operationName: null,
+ variables: {},
+ query: `mutation {
+ createOneTag(
+ input: {
+ tag: { name: "Before Create One Tag" }
+ }
+ ) {
+ ${tagFields}
+ createdBy
+ }
+ }`,
+ })
+ .expect(200, {
+ data: {
+ createOneTag: {
+ id: '7',
+ name: 'Before Create One Tag',
+ createdBy: 'E2E Test',
+ },
+ },
+ });
+ });
+
+ it('should validate a tag', () => {
+ return request(app.getHttpServer())
+ .post('/graphql')
+ .send({
+ operationName: null,
+ variables: {},
+ query: `mutation {
+ createOneTag(
+ input: {
+ tag: { name: "" }
+ }
+ ) {
+ ${tagFields}
+ }
+ }`,
+ })
+ .expect(200)
+ .then(({ body }) => {
+ expect(body.errors).toHaveLength(1);
+ expect(JSON.stringify(body.errors[0])).toContain('name should not be empty');
+ });
+ });
+ });
+
+ describe('create many', () => {
+ it('should allow creating a tag', () => {
+ return request(app.getHttpServer())
+ .post('/graphql')
+ .send({
+ operationName: null,
+ variables: {},
+ query: `mutation {
+ createManyTags(
+ input: {
+ tags: [
+ { name: "Create Many Tag - 1" },
+ { name: "Create Many Tag - 2" }
+ ]
+ }
+ ) {
+ ${tagFields}
+ }
+ }`,
+ })
+ .expect(200, {
+ data: {
+ createManyTags: [
+ { id: '8', name: 'Create Many Tag - 1' },
+ { id: '9', name: 'Create Many Tag - 2' },
+ ],
+ },
+ });
+ });
+
+ it('should call beforeCreateMany hook when creating multiple tags', () => {
+ return request(app.getHttpServer())
+ .post('/graphql')
+ .set(USER_HEADER_NAME, 'E2E Test')
+ .send({
+ operationName: null,
+ variables: {},
+ query: `mutation {
+ createManyTags(
+ input: {
+ tags: [
+ { name: "Before Create Many Tag - 1" },
+ { name: "Before Create Many Tag - 2" }
+ ]
+ }
+ ) {
+ ${tagFields}
+ createdBy
+ }
+ }`,
+ })
+ .expect(200, {
+ data: {
+ createManyTags: [
+ { id: '10', name: 'Before Create Many Tag - 1', createdBy: 'E2E Test' },
+ { id: '11', name: 'Before Create Many Tag - 2', createdBy: 'E2E Test' },
+ ],
+ },
+ });
+ });
+
+ it('should validate a tag', () => {
+ return request(app.getHttpServer())
+ .post('/graphql')
+ .send({
+ operationName: null,
+ variables: {},
+ query: `mutation {
+ createManyTags(
+ input: {
+ tags: [{ name: "" }]
+ }
+ ) {
+ ${tagFields}
+ }
+ }`,
+ })
+ .expect(200)
+ .then(({ body }) => {
+ expect(body.errors).toHaveLength(1);
+ expect(JSON.stringify(body.errors[0])).toContain('name should not be empty');
+ });
+ });
+ });
+
+ describe('update one', () => {
+ it('should allow updating a tag', () => {
+ return request(app.getHttpServer())
+ .post('/graphql')
+ .send({
+ operationName: null,
+ variables: {},
+ query: `mutation {
+ updateOneTag(
+ input: {
+ id: "6",
+ update: { name: "Update Test Tag" }
+ }
+ ) {
+ ${tagFields}
+ }
+ }`,
+ })
+ .expect(200, {
+ data: {
+ updateOneTag: {
+ id: '6',
+ name: 'Update Test Tag',
+ },
+ },
+ });
+ });
+
+ it('should call beforeUpdateOne hook when updating a tag', () => {
+ return request(app.getHttpServer())
+ .post('/graphql')
+ .set(USER_HEADER_NAME, 'E2E Test')
+ .send({
+ operationName: null,
+ variables: {},
+ query: `mutation {
+ updateOneTag(
+ input: {
+ id: "7",
+ update: { name: "Before Update One Test Tag" }
+ }
+ ) {
+ ${tagFields}
+ updatedBy
+ }
+ }`,
+ })
+ .expect(200, {
+ data: {
+ updateOneTag: {
+ id: '7',
+ name: 'Before Update One Test Tag',
+ updatedBy: 'E2E Test',
+ },
+ },
+ });
+ });
+
+ it('should require an id', () => {
+ return request(app.getHttpServer())
+ .post('/graphql')
+ .send({
+ operationName: null,
+ variables: {},
+ query: `mutation {
+ updateOneTag(
+ input: {
+ update: { name: "Update Test Tag" }
+ }
+ ) {
+ ${tagFields}
+ }
+ }`,
+ })
+ .expect(400)
+ .then(({ body }) => {
+ expect(body.errors).toHaveLength(1);
+ expect(body.errors[0].message).toBe('Field "UpdateOneTagInput.id" of required type "ID!" was not provided.');
+ });
+ });
+
+ it('should validate an update', () => {
+ return request(app.getHttpServer())
+ .post('/graphql')
+ .send({
+ operationName: null,
+ variables: {},
+ query: `mutation {
+ updateOneTag(
+ input: {
+ id: "6",
+ update: { name: "" }
+ }
+ ) {
+ ${tagFields}
+ }
+ }`,
+ })
+ .expect(200)
+ .then(({ body }) => {
+ expect(body.errors).toHaveLength(1);
+ expect(JSON.stringify(body.errors[0])).toContain('name should not be empty');
+ });
+ });
+ });
+
+ describe('update many', () => {
+ it('should allow updating a tag', () => {
+ return request(app.getHttpServer())
+ .post('/graphql')
+ .send({
+ operationName: null,
+ variables: {},
+ query: `mutation {
+ updateManyTags(
+ input: {
+ filter: {id: { in: ["8", "9"]} },
+ update: { name: "Update Many Tag" }
+ }
+ ) {
+ updatedCount
+ }
+ }`,
+ })
+ .expect(200, {
+ data: {
+ updateManyTags: {
+ updatedCount: 2,
+ },
+ },
+ });
+ });
+
+ it('should call beforeUpdateMany hook when updating multiple tags', () => {
+ return request(app.getHttpServer())
+ .post('/graphql')
+ .set(USER_HEADER_NAME, 'E2E Test')
+ .send({
+ operationName: null,
+ variables: {},
+ query: `mutation {
+ updateManyTags(
+ input: {
+ filter: {id: { in: ["10", "11"]} },
+ update: { name: "Before Update Many Tag" }
+ }
+ ) {
+ updatedCount
+ }
+ }`,
+ })
+ .expect(200, {
+ data: {
+ updateManyTags: {
+ updatedCount: 2,
+ },
+ },
+ })
+ .then(async () => {
+ const queryService = app.get>(getQueryServiceToken(TagEntity));
+ const todoItems = await queryService.query({ filter: { id: { in: [10, 11] } } });
+ expect(
+ todoItems.map((ti) => {
+ return {
+ id: ti.id,
+ name: ti.name,
+ updatedBy: ti.updatedBy,
+ };
+ }),
+ ).toEqual([
+ { id: 10, name: 'Before Update Many Tag', updatedBy: 'E2E Test' },
+ { id: 11, name: 'Before Update Many Tag', updatedBy: 'E2E Test' },
+ ]);
+ });
+ });
+
+ it('should require a filter', () => {
+ return request(app.getHttpServer())
+ .post('/graphql')
+ .send({
+ operationName: null,
+ variables: {},
+ query: `mutation {
+ updateManyTags(
+ input: {
+ update: { name: "Update Many Tag" }
+ }
+ ) {
+ updatedCount
+ }
+ }`,
+ })
+ .expect(400)
+ .then(({ body }) => {
+ expect(body.errors).toHaveLength(1);
+ expect(body.errors[0].message).toBe(
+ 'Field "UpdateManyTagsInput.filter" of required type "TagUpdateFilter!" was not provided.',
+ );
+ });
+ });
+
+ it('should require a non-empty filter', () => {
+ return request(app.getHttpServer())
+ .post('/graphql')
+ .send({
+ operationName: null,
+ variables: {},
+ query: `mutation {
+ updateManyTags(
+ input: {
+ filter: { },
+ update: { name: "Update Many Tag" }
+ }
+ ) {
+ updatedCount
+ }
+ }`,
+ })
+ .expect(200)
+ .then(({ body }) => {
+ expect(body.errors).toHaveLength(1);
+ expect(JSON.stringify(body.errors[0])).toContain('filter must be a non-empty object');
+ });
+ });
+ });
+
+ describe('delete one', () => {
+ it('should allow deleting a tag', () => {
+ return request(app.getHttpServer())
+ .post('/graphql')
+ .send({
+ operationName: null,
+ variables: {},
+ query: `mutation {
+ deleteOneTag(
+ input: { id: "6" }
+ ) {
+ ${tagFields}
+ }
+ }`,
+ })
+ .expect(200, {
+ data: {
+ deleteOneTag: {
+ id: null,
+ name: 'Update Test Tag',
+ },
+ },
+ });
+ });
+
+ it('should require an id', () => {
+ return request(app.getHttpServer())
+ .post('/graphql')
+ .send({
+ operationName: null,
+ variables: {},
+ query: `mutation {
+ deleteOneTag(
+ input: { }
+ ) {
+ ${tagFields}
+ }
+ }`,
+ })
+ .expect(400)
+ .then(({ body }) => {
+ expect(body.errors).toHaveLength(1);
+ expect(body.errors[0].message).toBe('Field "DeleteOneInput.id" of required type "ID!" was not provided.');
+ });
+ });
+ });
+
+ describe('delete many', () => {
+ it('should allow updating a tag', () => {
+ return request(app.getHttpServer())
+ .post('/graphql')
+ .send({
+ operationName: null,
+ variables: {},
+ query: `mutation {
+ deleteManyTags(
+ input: {
+ filter: {id: { in: ["7", "8"]} },
+ }
+ ) {
+ deletedCount
+ }
+ }`,
+ })
+ .expect(200, {
+ data: {
+ deleteManyTags: {
+ deletedCount: 2,
+ },
+ },
+ });
+ });
+
+ it('should require a filter', () => {
+ return request(app.getHttpServer())
+ .post('/graphql')
+ .send({
+ operationName: null,
+ variables: {},
+ query: `mutation {
+ deleteManyTags(
+ input: { }
+ ) {
+ deletedCount
+ }
+ }`,
+ })
+ .expect(400)
+ .then(({ body }) => {
+ expect(body.errors).toHaveLength(1);
+ expect(body.errors[0].message).toBe(
+ 'Field "DeleteManyTagsInput.filter" of required type "TagDeleteFilter!" was not provided.',
+ );
+ });
+ });
+
+ it('should require a non-empty filter', () => {
+ return request(app.getHttpServer())
+ .post('/graphql')
+ .send({
+ operationName: null,
+ variables: {},
+ query: `mutation {
+ deleteManyTags(
+ input: {
+ filter: { },
+ }
+ ) {
+ deletedCount
+ }
+ }`,
+ })
+ .expect(200)
+ .then(({ body }) => {
+ expect(body.errors).toHaveLength(1);
+ expect(JSON.stringify(body.errors[0])).toContain('filter must be a non-empty object');
+ });
+ });
+ });
+
+ describe('addTodoItemsToTag', () => {
+ it('allow adding subTasks to a tag', () => {
+ return request(app.getHttpServer())
+ .post('/graphql')
+ .send({
+ operationName: null,
+ variables: {},
+ query: `mutation {
+ addTodoItemsToTag(
+ input: {
+ id: 1,
+ relationIds: ["3", "4", "5"]
+ }
+ ) {
+ ${tagFields}
+ todoItems {
+ ${pageInfoField}
+ ${edgeNodes(todoItemFields)}
+ totalCount
+ }
+ }
+ }`,
+ })
+ .expect(200)
+ .then(({ body }) => {
+ const {
+ edges,
+ pageInfo,
+ totalCount,
+ }: CursorConnectionType = body.data.addTodoItemsToTag.todoItems;
+ expect(body.data.addTodoItemsToTag.id).toBe('1');
+ expect(pageInfo).toEqual({
+ endCursor: 'YXJyYXljb25uZWN0aW9uOjQ=',
+ hasNextPage: false,
+ hasPreviousPage: false,
+ startCursor: 'YXJyYXljb25uZWN0aW9uOjA=',
+ });
+ expect(totalCount).toBe(5);
+ expect(edges).toHaveLength(5);
+ expect(edges.map((e) => e.node.title).sort()).toEqual([
+ 'Add Todo Item Resolver',
+ 'Create Entity',
+ 'Create Entity Service',
+ 'Create Nest App',
+ 'How to create item With Sub Tasks',
+ ]);
+ });
+ });
+ });
+
+ describe('removeTodoItemsFromTag', () => {
+ it('allow removing todoItems from a tag', () => {
+ return request(app.getHttpServer())
+ .post('/graphql')
+ .send({
+ operationName: null,
+ variables: {},
+ query: `mutation {
+ removeTodoItemsFromTag(
+ input: {
+ id: 1,
+ relationIds: ["3", "4", "5"]
+ }
+ ) {
+ ${tagFields}
+ todoItems {
+ ${pageInfoField}
+ ${edgeNodes(todoItemFields)}
+ totalCount
+ }
+ }
+ }`,
+ })
+ .expect(200)
+ .then(({ body }) => {
+ const {
+ edges,
+ pageInfo,
+ totalCount,
+ }: CursorConnectionType = body.data.removeTodoItemsFromTag.todoItems;
+ expect(body.data.removeTodoItemsFromTag.id).toBe('1');
+ expect(pageInfo).toEqual({
+ endCursor: 'YXJyYXljb25uZWN0aW9uOjE=',
+ hasNextPage: false,
+ hasPreviousPage: false,
+ startCursor: 'YXJyYXljb25uZWN0aW9uOjA=',
+ });
+ expect(totalCount).toBe(2);
+ expect(edges).toHaveLength(2);
+ expect(edges.map((e) => e.node.title).sort()).toEqual(['Create Entity', 'Create Nest App']);
+ });
+ });
+ });
+
+ afterAll(async () => {
+ await app.close();
+ });
+});
diff --git a/examples/auth/e2e/todo-item.resolver.spec.ts b/examples/auth/e2e/todo-item.resolver.spec.ts
new file mode 100644
index 000000000..cac6f8013
--- /dev/null
+++ b/examples/auth/e2e/todo-item.resolver.spec.ts
@@ -0,0 +1,1382 @@
+import { AggregateResponse, getQueryServiceToken, QueryService } from '@nestjs-query/core';
+import { CursorConnectionType } from '@nestjs-query/query-graphql';
+import { Test } from '@nestjs/testing';
+import request from 'supertest';
+import { INestApplication, ValidationPipe } from '@nestjs/common';
+import { Connection } from 'typeorm';
+import { AppModule } from '../src/app.module';
+import { config } from '../src/config';
+import { AUTH_HEADER_NAME, USER_HEADER_NAME } from '../src/constants';
+import { SubTaskDTO } from '../src/sub-task/dto/sub-task.dto';
+import { TagDTO } from '../src/tag/dto/tag.dto';
+import { TodoItemDTO } from '../src/todo-item/dto/todo-item.dto';
+import { refresh } from './fixtures';
+import {
+ edgeNodes,
+ pageInfoField,
+ subTaskFields,
+ tagFields,
+ todoItemFields,
+ todoItemAggregateFields,
+ tagAggregateFields,
+ subTaskAggregateFields,
+} from './graphql-fragments';
+import { TodoItemEntity } from '../src/todo-item/todo-item.entity';
+
+describe('TodoItemResolver (auth - e2e)', () => {
+ let app: INestApplication;
+
+ beforeAll(async () => {
+ const moduleRef = await Test.createTestingModule({
+ imports: [AppModule],
+ }).compile();
+
+ app = moduleRef.createNestApplication();
+ app.useGlobalPipes(
+ new ValidationPipe({
+ transform: true,
+ whitelist: true,
+ forbidNonWhitelisted: true,
+ skipMissingProperties: false,
+ forbidUnknownValues: true,
+ }),
+ );
+
+ await app.init();
+ await refresh(app.get(Connection));
+ });
+
+ afterAll(() => refresh(app.get(Connection)));
+
+ describe('find one', () => {
+ it(`should find a todo item by id`, () => {
+ return request(app.getHttpServer())
+ .post('/graphql')
+ .send({
+ operationName: null,
+ variables: {},
+ query: `{
+ todoItem(id: 1) {
+ ${todoItemFields}
+ }
+ }`,
+ })
+ .expect(200)
+ .then(({ body }) => {
+ expect(body).toEqual({
+ data: {
+ todoItem: {
+ id: '1',
+ title: 'Create Nest App',
+ completed: true,
+ description: null,
+ age: expect.any(Number),
+ },
+ },
+ });
+ });
+ });
+
+ it(`should return null if the todo item is not found`, () => {
+ return request(app.getHttpServer())
+ .post('/graphql')
+ .send({
+ operationName: null,
+ variables: {},
+ query: `{
+ todoItem(id: 100) {
+ ${todoItemFields}
+ }
+ }`,
+ })
+ .expect(200, {
+ data: {
+ todoItem: null,
+ },
+ });
+ });
+
+ it(`should return subTasks as a connection`, () => {
+ return request(app.getHttpServer())
+ .post('/graphql')
+ .send({
+ operationName: null,
+ variables: {},
+ query: `{
+ todoItem(id: 1) {
+ subTasks(sorting: { field: id, direction: ASC }) {
+ ${pageInfoField}
+ ${edgeNodes(subTaskFields)}
+ totalCount
+ }
+ }
+ }`,
+ })
+ .expect(200)
+ .then(({ body }) => {
+ const { edges, pageInfo, totalCount }: CursorConnectionType = body.data.todoItem.subTasks;
+ expect(pageInfo).toEqual({
+ endCursor: 'YXJyYXljb25uZWN0aW9uOjI=',
+ hasNextPage: false,
+ hasPreviousPage: false,
+ startCursor: 'YXJyYXljb25uZWN0aW9uOjA=',
+ });
+ expect(totalCount).toBe(3);
+ expect(edges).toHaveLength(3);
+ edges.forEach((e) => expect(e.node.todoItemId).toBe('1'));
+ });
+ });
+
+ it(`should return subTasksAggregate`, () => {
+ return request(app.getHttpServer())
+ .post('/graphql')
+ .send({
+ operationName: null,
+ variables: {},
+ query: `{
+ todoItem(id: 1) {
+ subTasksAggregate {
+ ${subTaskAggregateFields}
+ }
+ }
+ }`,
+ })
+ .expect(200)
+ .then(({ body }) => {
+ const agg: AggregateResponse = body.data.todoItem.subTasksAggregate;
+ expect(agg).toEqual({
+ avg: { id: 2 },
+ count: { completed: 3, description: 0, id: 3, title: 3, todoItemId: 3 },
+ max: { description: null, id: '3', title: 'Create Nest App - Sub Task 3', todoItemId: '1' },
+ min: { description: null, id: '1', title: 'Create Nest App - Sub Task 1', todoItemId: '1' },
+ sum: { id: 6 },
+ });
+ });
+ });
+
+ it(`should return tags as a connection`, () => {
+ return request(app.getHttpServer())
+ .post('/graphql')
+ .send({
+ operationName: null,
+ variables: {},
+ query: `{
+ todoItem(id: 1) {
+ tags(sorting: [{ field: id, direction: ASC }]) {
+ ${pageInfoField}
+ ${edgeNodes(tagFields)}
+ totalCount
+ }
+ }
+ }`,
+ })
+ .expect(200)
+ .then(({ body }) => {
+ const { edges, pageInfo, totalCount }: CursorConnectionType = body.data.todoItem.tags;
+ expect(pageInfo).toEqual({
+ endCursor: 'YXJyYXljb25uZWN0aW9uOjE=',
+ hasNextPage: false,
+ hasPreviousPage: false,
+ startCursor: 'YXJyYXljb25uZWN0aW9uOjA=',
+ });
+ expect(totalCount).toBe(2);
+ expect(edges).toHaveLength(2);
+ expect(edges.map((e) => e.node.name)).toEqual(['Urgent', 'Home']);
+ });
+ });
+
+ it(`should return tagsAggregate`, () => {
+ return request(app.getHttpServer())
+ .post('/graphql')
+ .send({
+ operationName: null,
+ variables: {},
+ query: `{
+ todoItem(id: 1) {
+ tagsAggregate {
+ ${tagAggregateFields}
+ }
+ }
+ }`,
+ })
+ .expect(200)
+ .then(({ body }) => {
+ const agg: AggregateResponse = body.data.todoItem.tagsAggregate;
+ expect(agg).toEqual({
+ avg: { id: 1.5 },
+ count: { created: 2, id: 2, name: 2, updated: 2 },
+ max: { id: '2', name: 'Urgent' },
+ min: { id: '1', name: 'Home' },
+ sum: { id: 3 },
+ });
+ });
+ });
+ });
+
+ describe('query', () => {
+ it(`should return a connection`, () => {
+ return request(app.getHttpServer())
+ .post('/graphql')
+ .send({
+ operationName: null,
+ variables: {},
+ query: `{
+ todoItems {
+ ${pageInfoField}
+ ${edgeNodes(todoItemFields)}
+ totalCount
+ }
+ }`,
+ })
+ .expect(200)
+ .then(({ body }) => {
+ const { edges, pageInfo, totalCount }: CursorConnectionType = body.data.todoItems;
+ expect(pageInfo).toEqual({
+ endCursor: 'YXJyYXljb25uZWN0aW9uOjQ=',
+ hasNextPage: false,
+ hasPreviousPage: false,
+ startCursor: 'YXJyYXljb25uZWN0aW9uOjA=',
+ });
+ expect(totalCount).toBe(5);
+ expect(edges).toHaveLength(5);
+ expect(edges.map((e) => e.node)).toEqual([
+ { id: '1', title: 'Create Nest App', completed: true, description: null, age: expect.any(Number) },
+ { id: '2', title: 'Create Entity', completed: false, description: null, age: expect.any(Number) },
+ { id: '3', title: 'Create Entity Service', completed: false, description: null, age: expect.any(Number) },
+ { id: '4', title: 'Add Todo Item Resolver', completed: false, description: null, age: expect.any(Number) },
+ {
+ id: '5',
+ title: 'How to create item With Sub Tasks',
+ completed: false,
+ description: null,
+ age: expect.any(Number),
+ },
+ ]);
+ });
+ });
+
+ it(`should allow querying`, () => {
+ return request(app.getHttpServer())
+ .post('/graphql')
+ .send({
+ operationName: null,
+ variables: {},
+ query: `{
+ todoItems(filter: { id: { in: [1, 2, 3] } }) {
+ ${pageInfoField}
+ ${edgeNodes(todoItemFields)}
+ totalCount
+ }
+ }`,
+ })
+ .expect(200)
+ .then(({ body }) => {
+ const { edges, pageInfo, totalCount }: CursorConnectionType = body.data.todoItems;
+ expect(pageInfo).toEqual({
+ endCursor: 'YXJyYXljb25uZWN0aW9uOjI=',
+ hasNextPage: false,
+ hasPreviousPage: false,
+ startCursor: 'YXJyYXljb25uZWN0aW9uOjA=',
+ });
+ expect(totalCount).toBe(3);
+ expect(edges).toHaveLength(3);
+ expect(edges.map((e) => e.node)).toEqual([
+ { id: '1', title: 'Create Nest App', completed: true, description: null, age: expect.any(Number) },
+ { id: '2', title: 'Create Entity', completed: false, description: null, age: expect.any(Number) },
+ { id: '3', title: 'Create Entity Service', completed: false, description: null, age: expect.any(Number) },
+ ]);
+ });
+ });
+
+ it(`should allow querying on subTasks`, () => {
+ return request(app.getHttpServer())
+ .post('/graphql')
+ .send({
+ operationName: null,
+ variables: {},
+ query: `{
+ todoItems(filter: { subTasks: { title: { in: ["Create Nest App - Sub Task 1", "Create Entity - Sub Task 1"] } } }) {
+ ${pageInfoField}
+ ${edgeNodes(todoItemFields)}
+ totalCount
+ }
+ }`,
+ })
+ .expect(200)
+ .then(({ body }) => {
+ const { edges, totalCount, pageInfo }: CursorConnectionType = body.data.todoItems;
+ expect(pageInfo).toEqual({
+ endCursor: 'YXJyYXljb25uZWN0aW9uOjE=',
+ hasNextPage: false,
+ hasPreviousPage: false,
+ startCursor: 'YXJyYXljb25uZWN0aW9uOjA=',
+ });
+ expect(totalCount).toBe(2);
+ expect(edges).toHaveLength(2);
+
+ expect(edges.map((e) => e.node)).toEqual([
+ { id: '1', title: 'Create Nest App', completed: true, description: null, age: expect.any(Number) },
+ { id: '2', title: 'Create Entity', completed: false, description: null, age: expect.any(Number) },
+ ]);
+ });
+ });
+
+ it(`should allow querying on tags`, () => {
+ return request(app.getHttpServer())
+ .post('/graphql')
+ .send({
+ operationName: null,
+ variables: {},
+ query: `{
+ todoItems(filter: { tags: { name: { eq: "Home" } } }, sorting: [{field: id, direction: ASC}]) {
+ ${pageInfoField}
+ ${edgeNodes(todoItemFields)}
+ totalCount
+ }
+ }`,
+ })
+ .expect(200)
+ .then(({ body }) => {
+ const { edges, totalCount, pageInfo }: CursorConnectionType = body.data.todoItems;
+ expect(pageInfo).toEqual({
+ endCursor: 'YXJyYXljb25uZWN0aW9uOjE=',
+ hasNextPage: false,
+ hasPreviousPage: false,
+ startCursor: 'YXJyYXljb25uZWN0aW9uOjA=',
+ });
+ expect(totalCount).toBe(2);
+ expect(edges).toHaveLength(2);
+
+ expect(edges.map((e) => e.node)).toEqual([
+ { id: '1', title: 'Create Nest App', completed: true, description: null, age: expect.any(Number) },
+ { id: '4', title: 'Add Todo Item Resolver', completed: false, description: null, age: expect.any(Number) },
+ ]);
+ });
+ });
+
+ it(`should allow sorting`, () => {
+ return request(app.getHttpServer())
+ .post('/graphql')
+ .send({
+ operationName: null,
+ variables: {},
+ query: `{
+ todoItems(sorting: [{field: id, direction: DESC}]) {
+ ${pageInfoField}
+ ${edgeNodes(todoItemFields)}
+ totalCount
+ }
+ }`,
+ })
+ .expect(200)
+ .then(({ body }) => {
+ const { edges, pageInfo, totalCount }: CursorConnectionType = body.data.todoItems;
+ expect(pageInfo).toEqual({
+ endCursor: 'YXJyYXljb25uZWN0aW9uOjQ=',
+ hasNextPage: false,
+ hasPreviousPage: false,
+ startCursor: 'YXJyYXljb25uZWN0aW9uOjA=',
+ });
+ expect(totalCount).toBe(5);
+ expect(edges).toHaveLength(5);
+ expect(edges.map((e) => e.node)).toEqual([
+ {
+ id: '5',
+ title: 'How to create item With Sub Tasks',
+ completed: false,
+ description: null,
+ age: expect.any(Number),
+ },
+ { id: '4', title: 'Add Todo Item Resolver', completed: false, description: null, age: expect.any(Number) },
+ { id: '3', title: 'Create Entity Service', completed: false, description: null, age: expect.any(Number) },
+ { id: '2', title: 'Create Entity', completed: false, description: null, age: expect.any(Number) },
+ { id: '1', title: 'Create Nest App', completed: true, description: null, age: expect.any(Number) },
+ ]);
+ });
+ });
+
+ describe('paging', () => {
+ it(`should allow paging with the 'first' field`, () => {
+ return request(app.getHttpServer())
+ .post('/graphql')
+ .send({
+ operationName: null,
+ variables: {},
+ query: `{
+ todoItems(paging: {first: 2}) {
+ ${pageInfoField}
+ ${edgeNodes(todoItemFields)}
+ totalCount
+ }
+ }`,
+ })
+ .expect(200)
+ .then(({ body }) => {
+ const { edges, pageInfo, totalCount }: CursorConnectionType = body.data.todoItems;
+ expect(pageInfo).toEqual({
+ endCursor: 'YXJyYXljb25uZWN0aW9uOjE=',
+ hasNextPage: true,
+ hasPreviousPage: false,
+ startCursor: 'YXJyYXljb25uZWN0aW9uOjA=',
+ });
+ expect(totalCount).toBe(5);
+ expect(edges).toHaveLength(2);
+ expect(edges.map((e) => e.node)).toEqual([
+ { id: '1', title: 'Create Nest App', completed: true, description: null, age: expect.any(Number) },
+ { id: '2', title: 'Create Entity', completed: false, description: null, age: expect.any(Number) },
+ ]);
+ });
+ });
+
+ it(`should allow paging with the 'first' field and 'after'`, () => {
+ return request(app.getHttpServer())
+ .post('/graphql')
+ .send({
+ operationName: null,
+ variables: {},
+ query: `{
+ todoItems(paging: {first: 2, after: "YXJyYXljb25uZWN0aW9uOjE="}) {
+ ${pageInfoField}
+ ${edgeNodes(todoItemFields)}
+ totalCount
+ }
+ }`,
+ })
+ .expect(200)
+ .then(({ body }) => {
+ const { edges, pageInfo, totalCount }: CursorConnectionType = body.data.todoItems;
+ expect(pageInfo).toEqual({
+ endCursor: 'YXJyYXljb25uZWN0aW9uOjM=',
+ hasNextPage: true,
+ hasPreviousPage: true,
+ startCursor: 'YXJyYXljb25uZWN0aW9uOjI=',
+ });
+ expect(totalCount).toBe(5);
+ expect(edges).toHaveLength(2);
+ expect(edges.map((e) => e.node)).toEqual([
+ { id: '3', title: 'Create Entity Service', completed: false, description: null, age: expect.any(Number) },
+ {
+ id: '4',
+ title: 'Add Todo Item Resolver',
+ completed: false,
+ description: null,
+ age: expect.any(Number),
+ },
+ ]);
+ });
+ });
+ });
+ });
+
+ describe('aggregate', () => {
+ it('should require a header secret', () => {
+ return request(app.getHttpServer())
+ .post('/graphql')
+ .send({
+ operationName: null,
+ variables: {},
+ query: `{
+ todoItemAggregate {
+ ${todoItemAggregateFields}
+ }
+ }`,
+ })
+ .then(({ body }) => {
+ expect(body.errors).toHaveLength(1);
+ expect(JSON.stringify(body.errors[0])).toContain('Forbidden resource');
+ });
+ });
+
+ it(`should return a aggregate response`, () => {
+ return request(app.getHttpServer())
+ .post('/graphql')
+ .set(AUTH_HEADER_NAME, config.auth.header)
+ .send({
+ operationName: null,
+ variables: {},
+ query: `{
+ todoItemAggregate {
+ ${todoItemAggregateFields}
+ }
+ }`,
+ })
+ .expect(200)
+ .then(({ body }) => {
+ const res: AggregateResponse = body.data.todoItemAggregate;
+ expect(res).toEqual({
+ avg: { id: 3 },
+ count: { completed: 5, created: 5, description: 0, id: 5, title: 5, updated: 5 },
+ max: { description: null, id: '5', title: 'How to create item With Sub Tasks' },
+ min: { description: null, id: '1', title: 'Add Todo Item Resolver' },
+ sum: { id: 15 },
+ });
+ });
+ });
+
+ it(`should allow filtering`, () => {
+ return request(app.getHttpServer())
+ .post('/graphql')
+ .set(AUTH_HEADER_NAME, config.auth.header)
+ .send({
+ operationName: null,
+ variables: {},
+ query: `{
+ todoItemAggregate(filter: { completed: { is: false } }) {
+ ${todoItemAggregateFields}
+ }
+ }`,
+ })
+ .expect(200)
+ .then(({ body }) => {
+ const res: AggregateResponse = body.data.todoItemAggregate;
+ expect(res).toEqual({
+ count: { id: 4, title: 4, description: 0, completed: 4, created: 4, updated: 4 },
+ sum: { id: 14 },
+ avg: { id: 3.5 },
+ min: { id: '2', title: 'Add Todo Item Resolver', description: null },
+ max: { id: '5', title: 'How to create item With Sub Tasks', description: null },
+ });
+ });
+ });
+ });
+
+ describe('create one', () => {
+ it('should require a header secret', () => {
+ return request(app.getHttpServer())
+ .post('/graphql')
+ .send({
+ operationName: null,
+ variables: {},
+ query: `mutation {
+ createOneTodoItem(
+ input: {
+ todoItem: { title: "Test Todo", completed: false }
+ }
+ ) {
+ id
+ title
+ completed
+ }
+ }`,
+ })
+ .then(({ body }) => {
+ expect(body.errors).toHaveLength(1);
+ expect(JSON.stringify(body.errors[0])).toContain('Forbidden resource');
+ });
+ });
+ it('should allow creating a todoItem', () => {
+ return request(app.getHttpServer())
+ .post('/graphql')
+ .set(AUTH_HEADER_NAME, config.auth.header)
+ .send({
+ operationName: null,
+ variables: {},
+ query: `mutation {
+ createOneTodoItem(
+ input: {
+ todoItem: { title: "Test Todo", completed: false }
+ }
+ ) {
+ id
+ title
+ completed
+ }
+ }`,
+ })
+ .expect(200, {
+ data: {
+ createOneTodoItem: {
+ id: '6',
+ title: 'Test Todo',
+ completed: false,
+ },
+ },
+ });
+ });
+
+ it('should call the beforeCreateOne hook', () => {
+ return request(app.getHttpServer())
+ .post('/graphql')
+ .set({
+ [AUTH_HEADER_NAME]: config.auth.header,
+ [USER_HEADER_NAME]: 'E2E Test',
+ })
+ .send({
+ operationName: null,
+ variables: {},
+ query: `mutation {
+ createOneTodoItem(
+ input: {
+ todoItem: { title: "Create One Hook Todo", completed: false }
+ }
+ ) {
+ id
+ title
+ completed
+ createdBy
+ }
+ }`,
+ })
+ .expect(200, {
+ data: {
+ createOneTodoItem: {
+ id: '7',
+ title: 'Create One Hook Todo',
+ completed: false,
+ createdBy: 'E2E Test',
+ },
+ },
+ });
+ });
+
+ it('should validate a todoItem', () => {
+ return request(app.getHttpServer())
+ .post('/graphql')
+ .set(AUTH_HEADER_NAME, config.auth.header)
+ .send({
+ operationName: null,
+ variables: {},
+ query: `mutation {
+ createOneTodoItem(
+ input: {
+ todoItem: { title: "Test Todo with a too long title!", completed: false }
+ }
+ ) {
+ id
+ title
+ completed
+ }
+ }`,
+ })
+ .expect(200)
+ .then(({ body }) => {
+ expect(body.errors).toHaveLength(1);
+ expect(JSON.stringify(body.errors[0])).toContain('title must be shorter than or equal to 20 characters');
+ });
+ });
+ });
+
+ describe('create many', () => {
+ it('should require a header secret', () => {
+ return request(app.getHttpServer())
+ .post('/graphql')
+ .send({
+ operationName: null,
+ variables: {},
+ query: `mutation {
+ createManyTodoItems(
+ input: {
+ todoItems: [{ title: "Test Todo", completed: false }]
+ }
+ ) {
+ id
+ title
+ completed
+ }
+ }`,
+ })
+ .expect(200)
+ .then(({ body }) => {
+ expect(body.errors).toHaveLength(1);
+ expect(JSON.stringify(body.errors[0])).toContain('Forbidden resource');
+ });
+ });
+
+ it('should allow creating multiple todoItems', () => {
+ return request(app.getHttpServer())
+ .post('/graphql')
+ .set(AUTH_HEADER_NAME, config.auth.header)
+ .send({
+ operationName: null,
+ variables: {},
+ query: `mutation {
+ createManyTodoItems(
+ input: {
+ todoItems: [
+ { title: "Many Test Todo 1", completed: false },
+ { title: "Many Test Todo 2", completed: true }
+ ]
+ }
+ ) {
+ id
+ title
+ completed
+ }
+ }`,
+ })
+ .expect(200, {
+ data: {
+ createManyTodoItems: [
+ { id: '8', title: 'Many Test Todo 1', completed: false },
+ { id: '9', title: 'Many Test Todo 2', completed: true },
+ ],
+ },
+ });
+ });
+
+ it('should call the beforeCreateMany hook when creating multiple todoItems', () => {
+ return request(app.getHttpServer())
+ .post('/graphql')
+ .set({
+ [AUTH_HEADER_NAME]: config.auth.header,
+ [USER_HEADER_NAME]: 'E2E Test',
+ })
+ .send({
+ operationName: null,
+ variables: {},
+ query: `mutation {
+ createManyTodoItems(
+ input: {
+ todoItems: [
+ { title: "Many Create Hook 1", completed: false },
+ { title: "Many Create Hook 2", completed: true }
+ ]
+ }
+ ) {
+ id
+ title
+ completed
+ createdBy
+ }
+ }`,
+ })
+ .expect(200, {
+ data: {
+ createManyTodoItems: [
+ { id: '10', title: 'Many Create Hook 1', completed: false, createdBy: 'E2E Test' },
+ { id: '11', title: 'Many Create Hook 2', completed: true, createdBy: 'E2E Test' },
+ ],
+ },
+ });
+ });
+
+ it('should validate a todoItem', () => {
+ return request(app.getHttpServer())
+ .post('/graphql')
+ .set(AUTH_HEADER_NAME, config.auth.header)
+ .send({
+ operationName: null,
+ variables: {},
+ query: `mutation {
+ createManyTodoItems(
+ input: {
+ todoItems: [{ title: "Test Todo With A Really Long Title", completed: false }]
+ }
+ ) {
+ id
+ title
+ completed
+ }
+ }`,
+ })
+ .expect(200)
+ .then(({ body }) => {
+ expect(body.errors).toHaveLength(1);
+ expect(JSON.stringify(body.errors[0])).toContain('title must be shorter than or equal to 20 characters');
+ });
+ });
+ });
+
+ describe('update one', () => {
+ it('should require a header secret', () => {
+ return request(app.getHttpServer())
+ .post('/graphql')
+ .send({
+ operationName: null,
+ variables: {},
+ query: `mutation {
+ updateOneTodoItem(
+ input: {
+ id: "6",
+ update: { title: "Update Test Todo", completed: true }
+ }
+ ) {
+ id
+ title
+ completed
+ }
+ }`,
+ })
+ .then(({ body }) => {
+ expect(body.errors).toHaveLength(1);
+ expect(JSON.stringify(body.errors[0])).toContain('Forbidden resource');
+ });
+ });
+ it('should allow updating a todoItem', () => {
+ return request(app.getHttpServer())
+ .post('/graphql')
+ .set(AUTH_HEADER_NAME, config.auth.header)
+ .send({
+ operationName: null,
+ variables: {},
+ query: `mutation {
+ updateOneTodoItem(
+ input: {
+ id: "6",
+ update: { title: "Update Test Todo", completed: true }
+ }
+ ) {
+ id
+ title
+ completed
+ }
+ }`,
+ })
+ .expect(200, {
+ data: {
+ updateOneTodoItem: {
+ id: '6',
+ title: 'Update Test Todo',
+ completed: true,
+ },
+ },
+ });
+ });
+
+ it('should call the beforeUpdateOne hook', () => {
+ return request(app.getHttpServer())
+ .post('/graphql')
+ .set({
+ [AUTH_HEADER_NAME]: config.auth.header,
+ [USER_HEADER_NAME]: 'E2E Test',
+ })
+ .send({
+ operationName: null,
+ variables: {},
+ query: `mutation {
+ updateOneTodoItem(
+ input: {
+ id: "7",
+ update: { title: "Update One Hook Todo", completed: true }
+ }
+ ) {
+ id
+ title
+ completed
+ updatedBy
+ }
+ }`,
+ })
+ .expect(200, {
+ data: {
+ updateOneTodoItem: {
+ id: '7',
+ title: 'Update One Hook Todo',
+ completed: true,
+ updatedBy: 'E2E Test',
+ },
+ },
+ });
+ });
+
+ it('should require an id', () => {
+ return request(app.getHttpServer())
+ .post('/graphql')
+ .set(AUTH_HEADER_NAME, config.auth.header)
+ .send({
+ operationName: null,
+ variables: {},
+ query: `mutation {
+ updateOneTodoItem(
+ input: {
+ update: { title: "Update Test Todo With A Really Long Title" }
+ }
+ ) {
+ id
+ title
+ completed
+ }
+ }`,
+ })
+ .expect(400)
+ .then(({ body }) => {
+ expect(body.errors).toHaveLength(1);
+ expect(body.errors[0].message).toBe(
+ 'Field "UpdateOneTodoItemInput.id" of required type "ID!" was not provided.',
+ );
+ });
+ });
+
+ it('should validate an update', () => {
+ return request(app.getHttpServer())
+ .post('/graphql')
+ .set(AUTH_HEADER_NAME, config.auth.header)
+ .send({
+ operationName: null,
+ variables: {},
+ query: `mutation {
+ updateOneTodoItem(
+ input: {
+ id: "6",
+ update: { title: "Update Test Todo With A Really Long Title" }
+ }
+ ) {
+ id
+ title
+ completed
+ }
+ }`,
+ })
+ .expect(200)
+ .then(({ body }) => {
+ expect(body.errors).toHaveLength(1);
+ expect(JSON.stringify(body.errors[0])).toContain('title must be shorter than or equal to 20 characters');
+ });
+ });
+ });
+
+ describe('update many', () => {
+ it('should require a header secret', () => {
+ return request(app.getHttpServer())
+ .post('/graphql')
+ .send({
+ operationName: null,
+ variables: {},
+ query: `mutation {
+ updateManyTodoItems(
+ input: {
+ filter: {id: { in: ["7", "8"]} },
+ update: { title: "Update Test Todo", completed: true }
+ }
+ ) {
+ updatedCount
+ }
+ }`,
+ })
+ .then(({ body }) => {
+ expect(body.errors).toHaveLength(1);
+ expect(JSON.stringify(body.errors[0])).toContain('Forbidden resource');
+ });
+ });
+ it('should allow updating a todoItem', () => {
+ return request(app.getHttpServer())
+ .post('/graphql')
+ .set(AUTH_HEADER_NAME, config.auth.header)
+ .send({
+ operationName: null,
+ variables: {},
+ query: `mutation {
+ updateManyTodoItems(
+ input: {
+ filter: {id: { in: ["7", "8"]} },
+ update: { title: "Update Many Test", completed: true }
+ }
+ ) {
+ updatedCount
+ }
+ }`,
+ })
+ .expect(200, {
+ data: {
+ updateManyTodoItems: {
+ updatedCount: 2,
+ },
+ },
+ });
+ });
+
+ it('should call the beforeUpdateMany hook when updating todoItem', () => {
+ return request(app.getHttpServer())
+ .post('/graphql')
+ .set({
+ [AUTH_HEADER_NAME]: config.auth.header,
+ [USER_HEADER_NAME]: 'E2E Test',
+ })
+ .send({
+ operationName: null,
+ variables: {},
+ query: `mutation {
+ updateManyTodoItems(
+ input: {
+ filter: {id: { in: ["10", "11"]} },
+ update: { title: "Update Many Hook", completed: true }
+ }
+ ) {
+ updatedCount
+ }
+ }`,
+ })
+ .expect(200, {
+ data: {
+ updateManyTodoItems: {
+ updatedCount: 2,
+ },
+ },
+ })
+ .then(async () => {
+ const queryService = app.get>(getQueryServiceToken(TodoItemEntity));
+ const todoItems = await queryService.query({ filter: { id: { in: [10, 11] } } });
+ expect(
+ todoItems.map((ti) => {
+ return {
+ id: ti.id,
+ title: ti.title,
+ completed: ti.completed,
+ updatedBy: ti.updatedBy,
+ };
+ }),
+ ).toEqual([
+ { id: 10, title: 'Update Many Hook', completed: true, updatedBy: 'E2E Test' },
+ { id: 11, title: 'Update Many Hook', completed: true, updatedBy: 'E2E Test' },
+ ]);
+ });
+ });
+
+ it('should require a filter', () => {
+ return request(app.getHttpServer())
+ .post('/graphql')
+ .set(AUTH_HEADER_NAME, config.auth.header)
+ .send({
+ operationName: null,
+ variables: {},
+ query: `mutation {
+ updateManyTodoItems(
+ input: {
+ update: { title: "Update Many Test", completed: true }
+ }
+ ) {
+ updatedCount
+ }
+ }`,
+ })
+ .expect(400)
+ .then(({ body }) => {
+ expect(body.errors).toHaveLength(1);
+ expect(body.errors[0].message).toBe(
+ 'Field "UpdateManyTodoItemsInput.filter" of required type "TodoItemUpdateFilter!" was not provided.',
+ );
+ });
+ });
+
+ it('should require a non-empty filter', () => {
+ return request(app.getHttpServer())
+ .post('/graphql')
+ .set(AUTH_HEADER_NAME, config.auth.header)
+ .send({
+ operationName: null,
+ variables: {},
+ query: `mutation {
+ updateManyTodoItems(
+ input: {
+ filter: { },
+ update: { title: "Update Many Test", completed: true }
+ }
+ ) {
+ updatedCount
+ }
+ }`,
+ })
+ .expect(200)
+ .then(({ body }) => {
+ expect(body.errors).toHaveLength(1);
+ expect(JSON.stringify(body.errors[0])).toContain('filter must be a non-empty object');
+ });
+ });
+ });
+
+ describe('delete one', () => {
+ it('should require a header secret', () => {
+ return request(app.getHttpServer())
+ .post('/graphql')
+ .send({
+ operationName: null,
+ variables: {},
+ query: `mutation {
+ deleteOneTodoItem(
+ input: { id: "6" }
+ ) {
+ id
+ title
+ completed
+ }
+ }`,
+ })
+ .then(({ body }) => {
+ expect(body.errors).toHaveLength(1);
+ expect(JSON.stringify(body.errors[0])).toContain('Forbidden resource');
+ });
+ });
+ it('should allow deleting a todoItem', () => {
+ return request(app.getHttpServer())
+ .post('/graphql')
+ .set(AUTH_HEADER_NAME, config.auth.header)
+ .send({
+ operationName: null,
+ variables: {},
+ query: `mutation {
+ deleteOneTodoItem(
+ input: { id: "6" }
+ ) {
+ id
+ title
+ completed
+ }
+ }`,
+ })
+ .expect(200, {
+ data: {
+ deleteOneTodoItem: {
+ id: null,
+ title: 'Update Test Todo',
+ completed: true,
+ },
+ },
+ });
+ });
+
+ it('should require an id', () => {
+ return request(app.getHttpServer())
+ .post('/graphql')
+ .set(AUTH_HEADER_NAME, config.auth.header)
+ .send({
+ operationName: null,
+ variables: {},
+ query: `mutation {
+ deleteOneTodoItem(
+ input: { }
+ ) {
+ id
+ title
+ completed
+ }
+ }`,
+ })
+ .expect(400)
+ .then(({ body }) => {
+ expect(body.errors).toHaveLength(1);
+ expect(body.errors[0].message).toBe('Field "DeleteOneInput.id" of required type "ID!" was not provided.');
+ });
+ });
+ });
+
+ describe('delete many', () => {
+ it('should require a header secret', () => {
+ return request(app.getHttpServer())
+ .post('/graphql')
+ .send({
+ operationName: null,
+ variables: {},
+ query: `mutation {
+ deleteManyTodoItems(
+ input: {
+ filter: {id: { in: ["7", "8"]} },
+ }
+ ) {
+ deletedCount
+ }
+ }`,
+ })
+ .then(({ body }) => {
+ expect(body.errors).toHaveLength(1);
+ expect(JSON.stringify(body.errors[0])).toContain('Forbidden resource');
+ });
+ });
+ it('should allow updating a todoItem', () => {
+ return request(app.getHttpServer())
+ .post('/graphql')
+ .set(AUTH_HEADER_NAME, config.auth.header)
+ .send({
+ operationName: null,
+ variables: {},
+ query: `mutation {
+ deleteManyTodoItems(
+ input: {
+ filter: {id: { in: ["7", "8"]} },
+ }
+ ) {
+ deletedCount
+ }
+ }`,
+ })
+ .expect(200, {
+ data: {
+ deleteManyTodoItems: {
+ deletedCount: 2,
+ },
+ },
+ });
+ });
+
+ it('should require a filter', () => {
+ return request(app.getHttpServer())
+ .post('/graphql')
+ .set(AUTH_HEADER_NAME, config.auth.header)
+ .send({
+ operationName: null,
+ variables: {},
+ query: `mutation {
+ deleteManyTodoItems(
+ input: { }
+ ) {
+ deletedCount
+ }
+ }`,
+ })
+ .expect(400)
+ .then(({ body }) => {
+ expect(body.errors).toHaveLength(1);
+ expect(body.errors[0].message).toBe(
+ 'Field "DeleteManyTodoItemsInput.filter" of required type "TodoItemDeleteFilter!" was not provided.',
+ );
+ });
+ });
+
+ it('should require a non-empty filter', () => {
+ return request(app.getHttpServer())
+ .post('/graphql')
+ .set(AUTH_HEADER_NAME, config.auth.header)
+ .send({
+ operationName: null,
+ variables: {},
+ query: `mutation {
+ deleteManyTodoItems(
+ input: {
+ filter: { },
+ }
+ ) {
+ deletedCount
+ }
+ }`,
+ })
+ .expect(200)
+ .then(({ body }) => {
+ expect(body.errors).toHaveLength(1);
+ expect(JSON.stringify(body.errors[0])).toContain('filter must be a non-empty object');
+ });
+ });
+ });
+
+ describe('addSubTasksToTodoItem', () => {
+ it('allow adding subTasks to a todoItem', () => {
+ return request(app.getHttpServer())
+ .post('/graphql')
+ .set(AUTH_HEADER_NAME, config.auth.header)
+ .send({
+ operationName: null,
+ variables: {},
+ query: `mutation {
+ addSubTasksToTodoItem(
+ input: {
+ id: 1,
+ relationIds: ["4", "5", "6"]
+ }
+ ) {
+ id
+ title
+ subTasks {
+ ${pageInfoField}
+ ${edgeNodes(subTaskFields)}
+ totalCount
+ }
+ }
+ }`,
+ })
+ .expect(200)
+ .then(({ body }) => {
+ const {
+ edges,
+ pageInfo,
+ totalCount,
+ }: CursorConnectionType = body.data.addSubTasksToTodoItem.subTasks;
+ expect(body.data.addSubTasksToTodoItem.id).toBe('1');
+ expect(pageInfo).toEqual({
+ endCursor: 'YXJyYXljb25uZWN0aW9uOjU=',
+ hasNextPage: false,
+ hasPreviousPage: false,
+ startCursor: 'YXJyYXljb25uZWN0aW9uOjA=',
+ });
+ expect(totalCount).toBe(6);
+ expect(edges).toHaveLength(6);
+ edges.forEach((e) => expect(e.node.todoItemId).toBe('1'));
+ });
+ });
+ });
+
+ describe('addTagsToTodoItem', () => {
+ it('allow adding subTasks to a todoItem', () => {
+ return request(app.getHttpServer())
+ .post('/graphql')
+ .set(AUTH_HEADER_NAME, config.auth.header)
+ .send({
+ operationName: null,
+ variables: {},
+ query: `mutation {
+ addTagsToTodoItem(
+ input: {
+ id: 1,
+ relationIds: ["3", "4", "5"]
+ }
+ ) {
+ id
+ title
+ tags(sorting: [{ field: id, direction: ASC }]) {
+ ${pageInfoField}
+ ${edgeNodes(tagFields)}
+ totalCount
+ }
+ }
+ }`,
+ })
+ .expect(200)
+ .then(({ body }) => {
+ const { edges, pageInfo, totalCount }: CursorConnectionType = body.data.addTagsToTodoItem.tags;
+ expect(body.data.addTagsToTodoItem.id).toBe('1');
+ expect(pageInfo).toEqual({
+ endCursor: 'YXJyYXljb25uZWN0aW9uOjQ=',
+ hasNextPage: false,
+ hasPreviousPage: false,
+ startCursor: 'YXJyYXljb25uZWN0aW9uOjA=',
+ });
+ expect(totalCount).toBe(5);
+ expect(edges).toHaveLength(5);
+ expect(edges.map((e) => e.node.name)).toEqual(['Urgent', 'Home', 'Work', 'Question', 'Blocked']);
+ });
+ });
+ });
+
+ describe('removeTagsFromTodoItem', () => {
+ it('allow adding subTasks to a todoItem', () => {
+ return request(app.getHttpServer())
+ .post('/graphql')
+ .set(AUTH_HEADER_NAME, config.auth.header)
+ .send({
+ operationName: null,
+ variables: {},
+ query: `mutation {
+ removeTagsFromTodoItem(
+ input: {
+ id: 1,
+ relationIds: ["3", "4", "5"]
+ }
+ ) {
+ id
+ title
+ tags(sorting: [{ field: id, direction: ASC }]) {
+ ${pageInfoField}
+ ${edgeNodes(tagFields)}
+ totalCount
+ }
+ }
+ }`,
+ })
+ .expect(200)
+ .then(({ body }) => {
+ const { edges, pageInfo, totalCount }: CursorConnectionType = body.data.removeTagsFromTodoItem.tags;
+ expect(body.data.removeTagsFromTodoItem.id).toBe('1');
+ expect(pageInfo).toEqual({
+ endCursor: 'YXJyYXljb25uZWN0aW9uOjE=',
+ hasNextPage: false,
+ hasPreviousPage: false,
+ startCursor: 'YXJyYXljb25uZWN0aW9uOjA=',
+ });
+ expect(totalCount).toBe(2);
+ expect(edges).toHaveLength(2);
+ expect(edges.map((e) => e.node.name)).toEqual(['Urgent', 'Home']);
+ });
+ });
+ });
+
+ afterAll(async () => {
+ await app.close();
+ });
+});
diff --git a/examples/auth/src/app.module.ts b/examples/auth/src/app.module.ts
new file mode 100644
index 000000000..ef8d49bda
--- /dev/null
+++ b/examples/auth/src/app.module.ts
@@ -0,0 +1,25 @@
+import { Module } from '@nestjs/common';
+import { GraphQLModule } from '@nestjs/graphql';
+import { TypeOrmModule } from '@nestjs/typeorm';
+import { TagModule } from './tag/tag.module';
+import { TodoItemModule } from './todo-item/todo-item.module';
+import { SubTaskModule } from './sub-task/sub-task.module';
+import { typeormOrmConfig } from '../../helpers';
+import { AuthModule } from './auth/auth.module';
+import { UserModule } from './user/user.module';
+
+@Module({
+ imports: [
+ TypeOrmModule.forRoot(typeormOrmConfig('auth')),
+ GraphQLModule.forRoot({
+ autoSchemaFile: 'schema.gql',
+ context: ({ req }: { req: { headers: Record } }) => ({ req }),
+ }),
+ AuthModule,
+ UserModule,
+ SubTaskModule,
+ TodoItemModule,
+ TagModule,
+ ],
+})
+export class AppModule {}
diff --git a/examples/auth/src/auth.guard.ts b/examples/auth/src/auth.guard.ts
new file mode 100644
index 000000000..a513ba0a1
--- /dev/null
+++ b/examples/auth/src/auth.guard.ts
@@ -0,0 +1,18 @@
+import { CanActivate, ExecutionContext, Injectable, Logger } from '@nestjs/common';
+import { Observable } from 'rxjs';
+import { GqlExecutionContext } from '@nestjs/graphql';
+import { AUTH_HEADER_NAME } from './constants';
+import { config } from './config';
+
+export type GqlContext = { request: { headers: Record } };
+
+@Injectable()
+export class AuthGuard implements CanActivate {
+ private logger = new Logger(AuthGuard.name);
+
+ canActivate(context: ExecutionContext): boolean | Promise | Observable {
+ const { headers } = GqlExecutionContext.create(context).getContext().request;
+ this.logger.log(`Req = ${JSON.stringify(headers)}`);
+ return headers[AUTH_HEADER_NAME] === config.auth.header;
+ }
+}
diff --git a/examples/auth/src/auth/auth.constants.ts b/examples/auth/src/auth/auth.constants.ts
new file mode 100644
index 000000000..141b04828
--- /dev/null
+++ b/examples/auth/src/auth/auth.constants.ts
@@ -0,0 +1,3 @@
+export const jwtConstants = {
+ secret: 'nestjs-query-secret!!!',
+};
diff --git a/examples/auth/src/auth/auth.interfaces.ts b/examples/auth/src/auth/auth.interfaces.ts
new file mode 100644
index 000000000..e42d0f000
--- /dev/null
+++ b/examples/auth/src/auth/auth.interfaces.ts
@@ -0,0 +1,13 @@
+import { UserEntity } from '../user/user.entity';
+
+export type AuthenticatedUser = Pick;
+export type JwtPayload = {
+ sub: number;
+ username: string;
+};
+
+export type UserContext = {
+ req: {
+ user: AuthenticatedUser;
+ };
+};
diff --git a/examples/auth/src/auth/auth.module.ts b/examples/auth/src/auth/auth.module.ts
new file mode 100644
index 000000000..bc0248a06
--- /dev/null
+++ b/examples/auth/src/auth/auth.module.ts
@@ -0,0 +1,22 @@
+import { Module } from '@nestjs/common';
+import { PassportModule } from '@nestjs/passport';
+import { JwtModule } from '@nestjs/jwt';
+import { AuthService } from './auth.service';
+import { UserModule } from '../user/user.module';
+import { jwtConstants } from './auth.constants';
+import { AuthResolver } from './auth.resolver';
+import { JwtStrategy } from './jwt.strategy';
+
+@Module({
+ imports: [
+ UserModule,
+ PassportModule,
+ JwtModule.register({
+ secret: jwtConstants.secret,
+ signOptions: { expiresIn: '10m' },
+ }),
+ ],
+ providers: [AuthService, AuthResolver, JwtStrategy],
+ exports: [AuthService],
+})
+export class AuthModule {}
diff --git a/examples/auth/src/auth/auth.resolver.ts b/examples/auth/src/auth/auth.resolver.ts
new file mode 100644
index 000000000..2c7baf11e
--- /dev/null
+++ b/examples/auth/src/auth/auth.resolver.ts
@@ -0,0 +1,29 @@
+import { Resolver, Mutation, Args, Query } from '@nestjs/graphql';
+import { UnauthorizedException, UseGuards } from '@nestjs/common';
+import { AuthService } from './auth.service';
+import { LoginResponseDto } from './login-response.dto';
+import { LoginInputDTO } from './login-input.dto';
+import { UserDTO } from '../user/user.dto';
+import { JwtAuthGuard } from './jwt-auth.guard';
+import { CurrentUser } from './current-user.decorator';
+import { AuthenticatedUser } from './auth.interfaces';
+
+@Resolver()
+export class AuthResolver {
+ constructor(private authService: AuthService) {}
+
+ @Mutation(() => LoginResponseDto)
+ async login(@Args('input') input: LoginInputDTO): Promise {
+ const user = await this.authService.validateUser(input.username, input.password);
+ if (!user) {
+ throw new UnauthorizedException();
+ }
+ return this.authService.login(user);
+ }
+
+ @UseGuards(JwtAuthGuard)
+ @Query(() => UserDTO)
+ me(@CurrentUser() user: AuthenticatedUser): Promise {
+ return this.authService.currentUser(user);
+ }
+}
diff --git a/examples/auth/src/auth/auth.service.ts b/examples/auth/src/auth/auth.service.ts
new file mode 100644
index 000000000..d6d437cfb
--- /dev/null
+++ b/examples/auth/src/auth/auth.service.ts
@@ -0,0 +1,44 @@
+import { Injectable, UnauthorizedException } from '@nestjs/common';
+import { JwtService } from '@nestjs/jwt';
+import { InjectQueryService, QueryService } from '@nestjs-query/core';
+import { UserEntity } from '../user/user.entity';
+import { LoginResponseDto } from './login-response.dto';
+import { AuthenticatedUser, JwtPayload } from './auth.interfaces';
+import { UserDTO } from '../user/user.dto';
+
+@Injectable()
+export class AuthService {
+ constructor(
+ @InjectQueryService(UserEntity) private usersService: QueryService,
+ private jwtService: JwtService,
+ ) {}
+
+ async validateUser(username: string, pass: string): Promise {
+ const [user] = await this.usersService.query({ filter: { username: { eq: username } }, paging: { limit: 1 } });
+ // dont use plain text passwords in production!
+ if (user && user.password === pass) {
+ const { password, ...result } = user;
+ return result;
+ }
+ return null;
+ }
+
+ async currentUser(authUser: AuthenticatedUser): Promise {
+ const [user] = await this.usersService.query({
+ filter: { username: { eq: authUser.username } },
+ paging: { limit: 1 },
+ });
+ if (!user) {
+ throw new UnauthorizedException();
+ }
+ return user;
+ }
+
+ login(user: AuthenticatedUser): Promise {
+ const payload: JwtPayload = { username: user.username, sub: user.id };
+ return Promise.resolve({
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+ accessToken: this.jwtService.sign(payload),
+ });
+ }
+}
diff --git a/examples/auth/src/auth/current-user.decorator.ts b/examples/auth/src/auth/current-user.decorator.ts
new file mode 100644
index 000000000..f9eba7d35
--- /dev/null
+++ b/examples/auth/src/auth/current-user.decorator.ts
@@ -0,0 +1,8 @@
+import { createParamDecorator, ExecutionContext } from '@nestjs/common';
+import { GqlExecutionContext } from '@nestjs/graphql';
+import { UserContext } from './auth.interfaces';
+
+export const CurrentUser = createParamDecorator((data: unknown, context: ExecutionContext) => {
+ const ctx = GqlExecutionContext.create(context);
+ return ctx.getContext().req.user;
+});
diff --git a/examples/auth/src/auth/jwt-auth.guard.ts b/examples/auth/src/auth/jwt-auth.guard.ts
new file mode 100644
index 000000000..3ce564500
--- /dev/null
+++ b/examples/auth/src/auth/jwt-auth.guard.ts
@@ -0,0 +1,12 @@
+import { Injectable, ExecutionContext } from '@nestjs/common';
+import { AuthGuard } from '@nestjs/passport';
+import { GqlExecutionContext } from '@nestjs/graphql';
+
+@Injectable()
+export class JwtAuthGuard extends AuthGuard('jwt') {
+ getRequest(context: ExecutionContext) {
+ const ctx = GqlExecutionContext.create(context);
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-return,@typescript-eslint/no-unsafe-member-access
+ return ctx.getContext().req;
+ }
+}
diff --git a/examples/auth/src/auth/jwt.strategy.ts b/examples/auth/src/auth/jwt.strategy.ts
new file mode 100644
index 000000000..2b47d11e4
--- /dev/null
+++ b/examples/auth/src/auth/jwt.strategy.ts
@@ -0,0 +1,20 @@
+import { ExtractJwt, Strategy } from 'passport-jwt';
+import { PassportStrategy } from '@nestjs/passport';
+import { Injectable } from '@nestjs/common';
+import { jwtConstants } from './auth.constants';
+import { AuthenticatedUser, JwtPayload } from './auth.interfaces';
+
+@Injectable()
+export class JwtStrategy extends PassportStrategy(Strategy) {
+ constructor() {
+ super({
+ jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
+ ignoreExpiration: false,
+ secretOrKey: jwtConstants.secret,
+ });
+ }
+
+ validate(payload: JwtPayload): Promise {
+ return Promise.resolve({ id: payload.sub, username: payload.username });
+ }
+}
diff --git a/examples/auth/src/auth/login-input.dto.ts b/examples/auth/src/auth/login-input.dto.ts
new file mode 100644
index 000000000..3a73ab923
--- /dev/null
+++ b/examples/auth/src/auth/login-input.dto.ts
@@ -0,0 +1,13 @@
+import { Field, InputType } from '@nestjs/graphql';
+import { IsString } from 'class-validator';
+
+@InputType()
+export class LoginInputDTO {
+ @Field()
+ @IsString()
+ username!: string;
+
+ @Field()
+ @IsString()
+ password!: string;
+}
diff --git a/examples/auth/src/auth/login-response.dto.ts b/examples/auth/src/auth/login-response.dto.ts
new file mode 100644
index 000000000..8c7453f7f
--- /dev/null
+++ b/examples/auth/src/auth/login-response.dto.ts
@@ -0,0 +1,7 @@
+import { Field, ObjectType } from '@nestjs/graphql';
+
+@ObjectType('LoginResponse')
+export class LoginResponseDto {
+ @Field()
+ accessToken!: string;
+}
diff --git a/examples/auth/src/config.ts b/examples/auth/src/config.ts
new file mode 100644
index 000000000..f40ace34e
--- /dev/null
+++ b/examples/auth/src/config.ts
@@ -0,0 +1,12 @@
+export interface AuthConfig {
+ header: string;
+}
+export interface Config {
+ auth: AuthConfig;
+}
+
+export const config: Config = {
+ auth: {
+ header: 'super-secret',
+ },
+};
diff --git a/examples/auth/src/constants.ts b/examples/auth/src/constants.ts
new file mode 100644
index 000000000..4d8111276
--- /dev/null
+++ b/examples/auth/src/constants.ts
@@ -0,0 +1,2 @@
+export const AUTH_HEADER_NAME = 'authorization';
+export const USER_HEADER_NAME = 'user-name';
diff --git a/examples/auth/src/helpers.ts b/examples/auth/src/helpers.ts
new file mode 100644
index 000000000..d92b7a70e
--- /dev/null
+++ b/examples/auth/src/helpers.ts
@@ -0,0 +1,6 @@
+import { USER_HEADER_NAME } from './constants';
+import { GqlContext } from './auth.guard';
+
+export const getUserName = (context: GqlContext): string => {
+ return context.request.headers[USER_HEADER_NAME];
+};
diff --git a/examples/auth/src/main.ts b/examples/auth/src/main.ts
new file mode 100644
index 000000000..1e648c23e
--- /dev/null
+++ b/examples/auth/src/main.ts
@@ -0,0 +1,22 @@
+import { NestFactory } from '@nestjs/core';
+import { ValidationPipe } from '@nestjs/common';
+import { AppModule } from './app.module';
+
+async function bootstrap(): Promise {
+ const app = await NestFactory.create(AppModule);
+
+ app.useGlobalPipes(
+ new ValidationPipe({
+ transform: true,
+ whitelist: true,
+ forbidNonWhitelisted: true,
+ skipMissingProperties: false,
+ forbidUnknownValues: true,
+ }),
+ );
+
+ await app.listen(3000);
+}
+
+// eslint-disable-next-line no-void
+void bootstrap();
diff --git a/examples/auth/src/sub-task/dto/sub-task.dto.ts b/examples/auth/src/sub-task/dto/sub-task.dto.ts
new file mode 100644
index 000000000..d41f981d9
--- /dev/null
+++ b/examples/auth/src/sub-task/dto/sub-task.dto.ts
@@ -0,0 +1,34 @@
+import { FilterableField, FilterableRelation } from '@nestjs-query/query-graphql';
+import { ObjectType, ID, GraphQLISODateTime } from '@nestjs/graphql';
+import { TodoItemDTO } from '../../todo-item/dto/todo-item.dto';
+
+@ObjectType('SubTask')
+@FilterableRelation('todoItem', () => TodoItemDTO, { disableRemove: true })
+export class SubTaskDTO {
+ @FilterableField(() => ID)
+ id!: number;
+
+ @FilterableField()
+ title!: string;
+
+ @FilterableField({ nullable: true })
+ description?: string;
+
+ @FilterableField()
+ completed!: boolean;
+
+ @FilterableField(() => GraphQLISODateTime)
+ created!: Date;
+
+ @FilterableField(() => GraphQLISODateTime)
+ updated!: Date;
+
+ @FilterableField()
+ todoItemId!: string;
+
+ @FilterableField({ nullable: true })
+ createdBy?: string;
+
+ @FilterableField({ nullable: true })
+ updatedBy?: string;
+}
diff --git a/examples/auth/src/sub-task/dto/subtask-input.dto.ts b/examples/auth/src/sub-task/dto/subtask-input.dto.ts
new file mode 100644
index 000000000..0bbaeb689
--- /dev/null
+++ b/examples/auth/src/sub-task/dto/subtask-input.dto.ts
@@ -0,0 +1,44 @@
+import { Field, InputType, ID } from '@nestjs/graphql';
+import { IsOptional, IsString, IsBoolean, IsNotEmpty } from 'class-validator';
+import {
+ BeforeCreateMany,
+ BeforeCreateOne,
+ CreateManyInputType,
+ CreateOneInputType,
+} from '@nestjs-query/query-graphql';
+import { GqlContext } from '../../auth.guard';
+import { getUserName } from '../../helpers';
+import { SubTaskDTO } from './sub-task.dto';
+
+@InputType('SubTaskInput')
+@BeforeCreateOne((input: CreateOneInputType, context: GqlContext) => {
+ // eslint-disable-next-line no-param-reassign
+ input.input.createdBy = getUserName(context);
+ return input;
+})
+@BeforeCreateMany((input: CreateManyInputType, context: GqlContext) => {
+ const createdBy = getUserName(context);
+ // eslint-disable-next-line no-param-reassign
+ input.input = input.input.map((c) => ({ ...c, createdBy }));
+ return input;
+})
+export class CreateSubTaskDTO {
+ @Field()
+ @IsString()
+ @IsNotEmpty()
+ title!: string;
+
+ @Field({ nullable: true })
+ @IsOptional()
+ @IsString()
+ @IsNotEmpty()
+ description?: string;
+
+ @Field()
+ @IsBoolean()
+ completed!: boolean;
+
+ @Field(() => ID)
+ @IsNotEmpty()
+ todoItemId!: string;
+}
diff --git a/examples/auth/src/sub-task/dto/subtask-update.dto.ts b/examples/auth/src/sub-task/dto/subtask-update.dto.ts
new file mode 100644
index 000000000..e515877e5
--- /dev/null
+++ b/examples/auth/src/sub-task/dto/subtask-update.dto.ts
@@ -0,0 +1,46 @@
+import { Field, InputType } from '@nestjs/graphql';
+import { IsOptional, IsBoolean, IsString, IsNotEmpty } from 'class-validator';
+import {
+ BeforeUpdateMany,
+ BeforeUpdateOne,
+ UpdateManyInputType,
+ UpdateOneInputType,
+} from '@nestjs-query/query-graphql';
+import { GqlContext } from '../../auth.guard';
+import { getUserName } from '../../helpers';
+import { SubTaskDTO } from './sub-task.dto';
+
+@InputType('SubTaskUpdate')
+@BeforeUpdateOne((input: UpdateOneInputType, context: GqlContext) => {
+ // eslint-disable-next-line no-param-reassign
+ input.update.updatedBy = getUserName(context);
+ return input;
+})
+@BeforeUpdateMany((input: UpdateManyInputType, context: GqlContext) => {
+ // eslint-disable-next-line no-param-reassign
+ input.update.updatedBy = getUserName(context);
+ return input;
+})
+export class SubTaskUpdateDTO {
+ @Field()
+ @IsOptional()
+ @IsNotEmpty()
+ @IsString()
+ title?: string;
+
+ @Field({ nullable: true })
+ @IsOptional()
+ @IsNotEmpty()
+ @IsString()
+ description?: string;
+
+ @Field({ nullable: true })
+ @IsOptional()
+ @IsBoolean()
+ completed?: boolean;
+
+ @Field({ nullable: true })
+ @IsOptional()
+ @IsNotEmpty()
+ todoItemId?: string;
+}
diff --git a/examples/auth/src/sub-task/sub-task.entity.ts b/examples/auth/src/sub-task/sub-task.entity.ts
new file mode 100644
index 000000000..a44863f0e
--- /dev/null
+++ b/examples/auth/src/sub-task/sub-task.entity.ts
@@ -0,0 +1,48 @@
+import {
+ Entity,
+ PrimaryGeneratedColumn,
+ Column,
+ CreateDateColumn,
+ UpdateDateColumn,
+ ObjectType,
+ ManyToOne,
+ JoinColumn,
+} from 'typeorm';
+import { TodoItemEntity } from '../todo-item/todo-item.entity';
+
+@Entity({ name: 'sub_task' })
+export class SubTaskEntity {
+ @PrimaryGeneratedColumn()
+ id!: number;
+
+ @Column()
+ title!: string;
+
+ @Column({ nullable: true })
+ description?: string;
+
+ @Column()
+ completed!: boolean;
+
+ @Column({ nullable: false, name: 'todo_item_id' })
+ todoItemId!: string;
+
+ @ManyToOne((): ObjectType => TodoItemEntity, (td) => td.subTasks, {
+ onDelete: 'CASCADE',
+ nullable: false,
+ })
+ @JoinColumn({ name: 'todo_item_id' })
+ todoItem!: TodoItemEntity;
+
+ @CreateDateColumn()
+ created!: Date;
+
+ @UpdateDateColumn()
+ updated!: Date;
+
+ @Column({ nullable: true })
+ createdBy?: string;
+
+ @Column({ nullable: true })
+ updatedBy?: string;
+}
diff --git a/examples/auth/src/sub-task/sub-task.module.ts b/examples/auth/src/sub-task/sub-task.module.ts
new file mode 100644
index 000000000..9ac9cf33d
--- /dev/null
+++ b/examples/auth/src/sub-task/sub-task.module.ts
@@ -0,0 +1,26 @@
+import { NestjsQueryGraphQLModule } from '@nestjs-query/query-graphql';
+import { NestjsQueryTypeOrmModule } from '@nestjs-query/query-typeorm';
+import { Module } from '@nestjs/common';
+import { SubTaskDTO } from './dto/sub-task.dto';
+import { CreateSubTaskDTO } from './dto/subtask-input.dto';
+import { SubTaskUpdateDTO } from './dto/subtask-update.dto';
+import { SubTaskEntity } from './sub-task.entity';
+
+@Module({
+ imports: [
+ NestjsQueryGraphQLModule.forFeature({
+ imports: [NestjsQueryTypeOrmModule.forFeature([SubTaskEntity])],
+ resolvers: [
+ {
+ DTOClass: SubTaskDTO,
+ EntityClass: SubTaskEntity,
+ CreateDTOClass: CreateSubTaskDTO,
+ UpdateDTOClass: SubTaskUpdateDTO,
+ enableTotalCount: true,
+ enableAggregate: true,
+ },
+ ],
+ }),
+ ],
+})
+export class SubTaskModule {}
diff --git a/examples/auth/src/tag/dto/tag-input.dto.ts b/examples/auth/src/tag/dto/tag-input.dto.ts
new file mode 100644
index 000000000..7549644d1
--- /dev/null
+++ b/examples/auth/src/tag/dto/tag-input.dto.ts
@@ -0,0 +1,10 @@
+import { Field, InputType } from '@nestjs/graphql';
+import { IsString, IsNotEmpty } from 'class-validator';
+
+@InputType('TagInput')
+export class TagInputDTO {
+ @Field()
+ @IsString()
+ @IsNotEmpty()
+ name!: string;
+}
diff --git a/examples/auth/src/tag/dto/tag.dto.ts b/examples/auth/src/tag/dto/tag.dto.ts
new file mode 100644
index 000000000..b3ff4c846
--- /dev/null
+++ b/examples/auth/src/tag/dto/tag.dto.ts
@@ -0,0 +1,56 @@
+/* eslint-disable no-param-reassign */
+import {
+ FilterableField,
+ FilterableConnection,
+ BeforeCreateOne,
+ CreateOneInputType,
+ BeforeCreateMany,
+ CreateManyInputType,
+ BeforeUpdateOne,
+ UpdateOneInputType,
+ BeforeUpdateMany,
+ UpdateManyInputType,
+} from '@nestjs-query/query-graphql';
+import { ObjectType, ID, GraphQLISODateTime } from '@nestjs/graphql';
+import { TodoItemDTO } from '../../todo-item/dto/todo-item.dto';
+import { GqlContext } from '../../auth.guard';
+import { getUserName } from '../../helpers';
+
+@ObjectType('Tag')
+@FilterableConnection('todoItems', () => TodoItemDTO)
+@BeforeCreateOne((input: CreateOneInputType, context: GqlContext) => {
+ input.input.createdBy = getUserName(context);
+ return input;
+})
+@BeforeCreateMany((input: CreateManyInputType, context: GqlContext) => {
+ const createdBy = getUserName(context);
+ input.input = input.input.map((c) => ({ ...c, createdBy }));
+ return input;
+})
+@BeforeUpdateOne((input: UpdateOneInputType, context: GqlContext) => {
+ input.update.updatedBy = getUserName(context);
+ return input;
+})
+@BeforeUpdateMany((input: UpdateManyInputType, context: GqlContext) => {
+ input.update.updatedBy = getUserName(context);
+ return input;
+})
+export class TagDTO {
+ @FilterableField(() => ID)
+ id!: number;
+
+ @FilterableField()
+ name!: string;
+
+ @FilterableField(() => GraphQLISODateTime)
+ created!: Date;
+
+ @FilterableField(() => GraphQLISODateTime)
+ updated!: Date;
+
+ @FilterableField({ nullable: true })
+ createdBy?: string;
+
+ @FilterableField({ nullable: true })
+ updatedBy?: string;
+}
diff --git a/examples/auth/src/tag/tag.entity.ts b/examples/auth/src/tag/tag.entity.ts
new file mode 100644
index 000000000..591ad4878
--- /dev/null
+++ b/examples/auth/src/tag/tag.entity.ts
@@ -0,0 +1,34 @@
+import {
+ Entity,
+ PrimaryGeneratedColumn,
+ Column,
+ CreateDateColumn,
+ UpdateDateColumn,
+ ObjectType,
+ ManyToMany,
+} from 'typeorm';
+import { TodoItemEntity } from '../todo-item/todo-item.entity';
+
+@Entity({ name: 'tag' })
+export class TagEntity {
+ @PrimaryGeneratedColumn()
+ id!: number;
+
+ @Column()
+ name!: string;
+
+ @CreateDateColumn()
+ created!: Date;
+
+ @UpdateDateColumn()
+ updated!: Date;
+
+ @ManyToMany((): ObjectType => TodoItemEntity, (td) => td.tags)
+ todoItems!: TodoItemEntity[];
+
+ @Column({ nullable: true })
+ createdBy?: string;
+
+ @Column({ nullable: true })
+ updatedBy?: string;
+}
diff --git a/examples/auth/src/tag/tag.module.ts b/examples/auth/src/tag/tag.module.ts
new file mode 100644
index 000000000..dde56ca27
--- /dev/null
+++ b/examples/auth/src/tag/tag.module.ts
@@ -0,0 +1,25 @@
+import { NestjsQueryGraphQLModule } from '@nestjs-query/query-graphql';
+import { NestjsQueryTypeOrmModule } from '@nestjs-query/query-typeorm';
+import { Module } from '@nestjs/common';
+import { TagInputDTO } from './dto/tag-input.dto';
+import { TagDTO } from './dto/tag.dto';
+import { TagEntity } from './tag.entity';
+
+@Module({
+ imports: [
+ NestjsQueryGraphQLModule.forFeature({
+ imports: [NestjsQueryTypeOrmModule.forFeature([TagEntity])],
+ resolvers: [
+ {
+ DTOClass: TagDTO,
+ EntityClass: TagEntity,
+ CreateDTOClass: TagInputDTO,
+ UpdateDTOClass: TagInputDTO,
+ enableTotalCount: true,
+ enableAggregate: true,
+ },
+ ],
+ }),
+ ],
+})
+export class TagModule {}
diff --git a/examples/auth/src/todo-item/dto/todo-item-input.dto.ts b/examples/auth/src/todo-item/dto/todo-item-input.dto.ts
new file mode 100644
index 000000000..1fc4a3566
--- /dev/null
+++ b/examples/auth/src/todo-item/dto/todo-item-input.dto.ts
@@ -0,0 +1,34 @@
+import { IsString, MaxLength, IsBoolean } from 'class-validator';
+import { Field, InputType } from '@nestjs/graphql';
+import {
+ BeforeCreateMany,
+ BeforeCreateOne,
+ CreateManyInputType,
+ CreateOneInputType,
+} from '@nestjs-query/query-graphql';
+import { GqlContext } from '../../auth.guard';
+import { getUserName } from '../../helpers';
+import { TodoItemDTO } from './todo-item.dto';
+
+@InputType('TodoItemInput')
+@BeforeCreateOne((input: CreateOneInputType, context: GqlContext) => {
+ // eslint-disable-next-line no-param-reassign
+ input.input.createdBy = getUserName(context);
+ return input;
+})
+@BeforeCreateMany((input: CreateManyInputType, context: GqlContext) => {
+ const createdBy = getUserName(context);
+ // eslint-disable-next-line no-param-reassign
+ input.input = input.input.map((c) => ({ ...c, createdBy }));
+ return input;
+})
+export class TodoItemInputDTO {
+ @IsString()
+ @MaxLength(20)
+ @Field()
+ title!: string;
+
+ @IsBoolean()
+ @Field()
+ completed!: boolean;
+}
diff --git a/examples/auth/src/todo-item/dto/todo-item-update.dto.ts b/examples/auth/src/todo-item/dto/todo-item-update.dto.ts
new file mode 100644
index 000000000..d13a17832
--- /dev/null
+++ b/examples/auth/src/todo-item/dto/todo-item-update.dto.ts
@@ -0,0 +1,40 @@
+import { Field, InputType } from '@nestjs/graphql';
+import { IsBoolean, IsNumber, IsOptional, IsString, MaxLength } from 'class-validator';
+import {
+ BeforeUpdateMany,
+ BeforeUpdateOne,
+ UpdateManyInputType,
+ UpdateOneInputType,
+} from '@nestjs-query/query-graphql';
+import { GqlContext } from '../../auth.guard';
+import { getUserName } from '../../helpers';
+import { TodoItemDTO } from './todo-item.dto';
+
+@InputType('TodoItemUpdate')
+@BeforeUpdateOne((input: UpdateOneInputType, context: GqlContext) => {
+ // eslint-disable-next-line no-param-reassign
+ input.update.updatedBy = getUserName(context);
+ return input;
+})
+@BeforeUpdateMany((input: UpdateManyInputType, context: GqlContext) => {
+ // eslint-disable-next-line no-param-reassign
+ input.update.updatedBy = getUserName(context);
+ return input;
+})
+export class TodoItemUpdateDTO {
+ @IsOptional()
+ @IsString()
+ @MaxLength(20)
+ @Field({ nullable: true })
+ title?: string;
+
+ @IsOptional()
+ @IsBoolean()
+ @Field({ nullable: true })
+ completed?: boolean;
+
+ @IsOptional()
+ @IsNumber()
+ @Field({ nullable: true })
+ priority?: number;
+}
diff --git a/examples/auth/src/todo-item/dto/todo-item.dto.ts b/examples/auth/src/todo-item/dto/todo-item.dto.ts
new file mode 100644
index 000000000..ace84cc7e
--- /dev/null
+++ b/examples/auth/src/todo-item/dto/todo-item.dto.ts
@@ -0,0 +1,42 @@
+import { FilterableField, FilterableConnection, Relation } from '@nestjs-query/query-graphql';
+import { ObjectType, ID, GraphQLISODateTime, Field } from '@nestjs/graphql';
+import { AuthGuard } from '../../auth.guard';
+import { SubTaskDTO } from '../../sub-task/dto/sub-task.dto';
+import { TagDTO } from '../../tag/dto/tag.dto';
+import { UserDTO } from '../../user/user.dto';
+
+@ObjectType('TodoItem')
+@Relation('owner', () => UserDTO, { disableRemove: true, guards: [AuthGuard] })
+@FilterableConnection('subTasks', () => SubTaskDTO, { disableRemove: true, guards: [AuthGuard] })
+@FilterableConnection('tags', () => TagDTO, { guards: [AuthGuard] })
+export class TodoItemDTO {
+ @FilterableField(() => ID)
+ id!: number;
+
+ @FilterableField()
+ title!: string;
+
+ @FilterableField({ nullable: true })
+ description?: string;
+
+ @FilterableField()
+ completed!: boolean;
+
+ @FilterableField(() => GraphQLISODateTime)
+ created!: Date;
+
+ @FilterableField(() => GraphQLISODateTime)
+ updated!: Date;
+
+ @Field()
+ age!: number;
+
+ @FilterableField()
+ priority!: number;
+
+ @FilterableField({ nullable: true })
+ createdBy?: string;
+
+ @FilterableField({ nullable: true })
+ updatedBy?: string;
+}
diff --git a/examples/auth/src/todo-item/todo-item.assembler.ts b/examples/auth/src/todo-item/todo-item.assembler.ts
new file mode 100644
index 000000000..3d8dc5b70
--- /dev/null
+++ b/examples/auth/src/todo-item/todo-item.assembler.ts
@@ -0,0 +1,12 @@
+import { Assembler, ClassTransformerAssembler } from '@nestjs-query/core';
+import { TodoItemDTO } from './dto/todo-item.dto';
+import { TodoItemEntity } from './todo-item.entity';
+
+@Assembler(TodoItemDTO, TodoItemEntity)
+export class TodoItemAssembler extends ClassTransformerAssembler {
+ convertToDTO(entity: TodoItemEntity): TodoItemDTO {
+ const dto = super.convertToDTO(entity);
+ dto.age = Date.now() - entity.created.getMilliseconds();
+ return dto;
+ }
+}
diff --git a/examples/auth/src/todo-item/todo-item.entity.ts b/examples/auth/src/todo-item/todo-item.entity.ts
new file mode 100644
index 000000000..804c89969
--- /dev/null
+++ b/examples/auth/src/todo-item/todo-item.entity.ts
@@ -0,0 +1,57 @@
+import {
+ Column,
+ CreateDateColumn,
+ Entity,
+ PrimaryGeneratedColumn,
+ UpdateDateColumn,
+ OneToMany,
+ ManyToMany,
+ JoinTable,
+ ManyToOne,
+} from 'typeorm';
+import { SubTaskEntity } from '../sub-task/sub-task.entity';
+import { TagEntity } from '../tag/tag.entity';
+import { UserEntity } from '../user/user.entity';
+
+@Entity({ name: 'todo_item' })
+export class TodoItemEntity {
+ @PrimaryGeneratedColumn()
+ id!: number;
+
+ @Column()
+ title!: string;
+
+ @Column({ nullable: true })
+ description?: string;
+
+ @Column()
+ completed!: boolean;
+
+ @ManyToOne(() => UserEntity, (u) => u.todoItems, {
+ onDelete: 'CASCADE',
+ nullable: false,
+ })
+ owner!: UserEntity;
+
+ @OneToMany(() => SubTaskEntity, (subTask) => subTask.todoItem)
+ subTasks!: SubTaskEntity[];
+
+ @CreateDateColumn()
+ created!: Date;
+
+ @UpdateDateColumn()
+ updated!: Date;
+
+ @ManyToMany(() => TagEntity, (tag) => tag.todoItems)
+ @JoinTable()
+ tags!: TagEntity[];
+
+ @Column({ type: 'integer', nullable: false, default: 0 })
+ priority!: number;
+
+ @Column({ nullable: true })
+ createdBy?: string;
+
+ @Column({ nullable: true })
+ updatedBy?: string;
+}
diff --git a/examples/auth/src/todo-item/todo-item.module.ts b/examples/auth/src/todo-item/todo-item.module.ts
new file mode 100644
index 000000000..390980814
--- /dev/null
+++ b/examples/auth/src/todo-item/todo-item.module.ts
@@ -0,0 +1,36 @@
+import { NestjsQueryGraphQLModule } from '@nestjs-query/query-graphql';
+import { NestjsQueryTypeOrmModule } from '@nestjs-query/query-typeorm';
+import { Module } from '@nestjs/common';
+import { TodoItemInputDTO } from './dto/todo-item-input.dto';
+import { TodoItemUpdateDTO } from './dto/todo-item-update.dto';
+import { TodoItemDTO } from './dto/todo-item.dto';
+import { TodoItemAssembler } from './todo-item.assembler';
+import { TodoItemEntity } from './todo-item.entity';
+import { TodoItemResolver } from './todo-item.resolver';
+import { JwtAuthGuard } from '../auth/jwt-auth.guard';
+
+const guards = [JwtAuthGuard];
+@Module({
+ providers: [TodoItemResolver],
+ imports: [
+ NestjsQueryGraphQLModule.forFeature({
+ imports: [NestjsQueryTypeOrmModule.forFeature([TodoItemEntity])],
+ assemblers: [TodoItemAssembler],
+ resolvers: [
+ {
+ DTOClass: TodoItemDTO,
+ AssemblerClass: TodoItemAssembler,
+ CreateDTOClass: TodoItemInputDTO,
+ UpdateDTOClass: TodoItemUpdateDTO,
+ enableTotalCount: true,
+ enableAggregate: true,
+ aggregate: { guards },
+ create: { guards },
+ update: { guards },
+ delete: { guards },
+ },
+ ],
+ }),
+ ],
+})
+export class TodoItemModule {}
diff --git a/examples/auth/src/todo-item/todo-item.resolver.ts b/examples/auth/src/todo-item/todo-item.resolver.ts
new file mode 100644
index 000000000..d593747bd
--- /dev/null
+++ b/examples/auth/src/todo-item/todo-item.resolver.ts
@@ -0,0 +1,35 @@
+import { Filter, InjectAssemblerQueryService, QueryService } from '@nestjs-query/core';
+import { ConnectionType } from '@nestjs-query/query-graphql';
+import { Args, Query, Resolver } from '@nestjs/graphql';
+import { TodoItemDTO } from './dto/todo-item.dto';
+import { TodoItemAssembler } from './todo-item.assembler';
+import { TodoItemConnection, TodoItemQuery } from './types';
+
+@Resolver(() => TodoItemDTO)
+export class TodoItemResolver {
+ constructor(@InjectAssemblerQueryService(TodoItemAssembler) readonly service: QueryService) {}
+
+ // Set the return type to the TodoItemConnection
+ @Query(() => TodoItemConnection)
+ completedTodoItems(@Args() query: TodoItemQuery): Promise> {
+ // add the completed filter the user provided filter
+ const filter: Filter = {
+ ...query.filter,
+ ...{ completed: { is: true } },
+ };
+
+ return TodoItemConnection.createFromPromise((q) => this.service.query(q), { ...query, ...{ filter } });
+ }
+
+ // Set the return type to the TodoItemConnection
+ @Query(() => TodoItemConnection)
+ uncompletedTodoItems(@Args() query: TodoItemQuery): Promise> {
+ // add the completed filter the user provided filter
+ const filter: Filter = {
+ ...query.filter,
+ ...{ completed: { is: false } },
+ };
+
+ return TodoItemConnection.createFromPromise((q) => this.service.query(q), { ...query, ...{ filter } });
+ }
+}
diff --git a/examples/auth/src/todo-item/types.ts b/examples/auth/src/todo-item/types.ts
new file mode 100644
index 000000000..bd5158a1e
--- /dev/null
+++ b/examples/auth/src/todo-item/types.ts
@@ -0,0 +1,8 @@
+import { ConnectionType, QueryArgsType } from '@nestjs-query/query-graphql';
+import { ArgsType } from '@nestjs/graphql';
+import { TodoItemDTO } from './dto/todo-item.dto';
+
+export const TodoItemConnection = ConnectionType(TodoItemDTO, { enableTotalCount: true });
+
+@ArgsType()
+export class TodoItemQuery extends QueryArgsType(TodoItemDTO, { defaultResultSize: 2 }) {}
diff --git a/examples/auth/src/user/user.dto.ts b/examples/auth/src/user/user.dto.ts
new file mode 100644
index 000000000..1fdf15f0c
--- /dev/null
+++ b/examples/auth/src/user/user.dto.ts
@@ -0,0 +1,13 @@
+import { Field, ObjectType } from '@nestjs/graphql';
+
+@ObjectType('User')
+export class UserDTO {
+ @Field()
+ username!: string;
+
+ @Field()
+ created!: Date;
+
+ @Field()
+ updated!: Date;
+}
diff --git a/examples/auth/src/user/user.entity.ts b/examples/auth/src/user/user.entity.ts
new file mode 100644
index 000000000..dd0ada706
--- /dev/null
+++ b/examples/auth/src/user/user.entity.ts
@@ -0,0 +1,32 @@
+import {
+ Column,
+ CreateDateColumn,
+ Entity,
+ PrimaryGeneratedColumn,
+ UpdateDateColumn,
+ OneToMany,
+ JoinTable,
+} from 'typeorm';
+import { TodoItemEntity } from '../todo-item/todo-item.entity';
+
+@Entity({ name: 'user' })
+export class UserEntity {
+ @PrimaryGeneratedColumn()
+ id!: number;
+
+ @Column()
+ username!: string;
+
+ @Column({ nullable: true })
+ password?: string;
+
+ @CreateDateColumn()
+ created!: Date;
+
+ @UpdateDateColumn()
+ updated!: Date;
+
+ @OneToMany(() => TodoItemEntity, (todo) => todo.owner)
+ @JoinTable()
+ todoItems!: TodoItemEntity[];
+}
diff --git a/examples/auth/src/user/user.module.ts b/examples/auth/src/user/user.module.ts
new file mode 100644
index 000000000..67f19a331
--- /dev/null
+++ b/examples/auth/src/user/user.module.ts
@@ -0,0 +1,9 @@
+import { NestjsQueryTypeOrmModule } from '@nestjs-query/query-typeorm';
+import { Module } from '@nestjs/common';
+import { UserEntity } from './user.entity';
+
+@Module({
+ imports: [NestjsQueryTypeOrmModule.forFeature([UserEntity])],
+ exports: [NestjsQueryTypeOrmModule.forFeature([UserEntity])],
+})
+export class UserModule {}
diff --git a/examples/init-scripts/mysql/init-auth.sql b/examples/init-scripts/mysql/init-auth.sql
new file mode 100644
index 000000000..4787e135b
--- /dev/null
+++ b/examples/init-scripts/mysql/init-auth.sql
@@ -0,0 +1,3 @@
+CREATE USER auth;
+CREATE DATABASE auth;
+GRANT ALL PRIVILEGES ON auth.* TO auth;
diff --git a/examples/init-scripts/postgres/init-auth.sql b/examples/init-scripts/postgres/init-auth.sql
new file mode 100644
index 000000000..0337d45cb
--- /dev/null
+++ b/examples/init-scripts/postgres/init-auth.sql
@@ -0,0 +1,3 @@
+CREATE USER auth WITH SUPERUSER;
+CREATE DATABASE auth;
+GRANT ALL PRIVILEGES ON DATABASE auth TO auth;
diff --git a/examples/nest-cli.json b/examples/nest-cli.json
index cb3242972..e3f8eaa64 100644
--- a/examples/nest-cli.json
+++ b/examples/nest-cli.json
@@ -2,6 +2,15 @@
"collection": "@nestjs/schematics",
"monorepo": true,
"projects": {
+ "auth": {
+ "type": "application",
+ "root": "auth",
+ "entryFile": "main",
+ "sourceRoot": "auth/src",
+ "compilerOptions": {
+ "tsConfigPath": "./tsconfig.build.json"
+ }
+ },
"basic": {
"type": "application",
"root": "basic",
diff --git a/examples/package.json b/examples/package.json
index e51184168..60922095d 100644
--- a/examples/package.json
+++ b/examples/package.json
@@ -22,6 +22,11 @@
"@nestjs/platform-express": "7.4.2",
"@nestjs/sequelize": "0.1.1",
"@nestjs/typeorm": "7.1.0",
+ "@nestjs/passport": "7.1.0",
+ "@nestjs/jwt": "7.1.0",
+ "passport": "0.4.1",
+ "passport-jwt": "4.0.0",
+ "passport-local": "1.0.0",
"apollo-server-express": "2.16.1",
"apollo-server-plugin-base": "0.9.1",
"class-validator": "0.12.2",
@@ -45,6 +50,8 @@
"@types/jest": "26.0.10",
"@types/node": "13.13.5",
"@types/supertest": "2.0.10",
+ "@types/passport-jwt": "3.0.3",
+ "@types/passport-local": "1.0.33",
"jest": "26.4.2",
"prettier": "2.0.5",
"supertest": "4.0.2",