diff --git a/.run/start - complexity.run.xml b/.run/start - complexity.run.xml
new file mode 100644
index 000000000..502672495
--- /dev/null
+++ b/.run/start - complexity.run.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/documentation/docs/faq.md b/documentation/docs/faq.md
index 5b2340385..f93ef996a 100644
--- a/documentation/docs/faq.md
+++ b/documentation/docs/faq.md
@@ -57,3 +57,15 @@ To read more and see examples read the following docs.
* [`@FilterableRelation`](./graphql/relations.mdx#filterablerelation-decorator)
* [`@FilterableConnection`](./graphql/relations.mdx#filterableconnection-decorator)
+
+
+## Does nestjs-query support specifying complexity.
+
+Yes!
+
+The `@FilterableField` decorator accepts the same arguments as the `@Field` decorator from `@nestjs/graphql`
+
+The `@Relation` `@FilterableRelation`, `@Connection` and `@FilterableConnection` decorators also accept a complexity option.
+
+To read more about complexity [see the nestjs docs](https://docs.nestjs.com/graphql/complexity)
+
diff --git a/documentation/docs/graphql/relations.mdx b/documentation/docs/graphql/relations.mdx
index c43c027d0..4b66c0c8f 100644
--- a/documentation/docs/graphql/relations.mdx
+++ b/documentation/docs/graphql/relations.mdx
@@ -662,6 +662,7 @@ The following options can be passed to the `@Relation` or `@Connection` decorato
* `relationName` - The name of the relation to use when looking up the relation from the `QueryService`
* `nullable` - Set to `true` if the relation is nullable.
+* `complexity` - Set to specify relation complexity. For more info see [complexity docs](https://docs.nestjs.com/graphql/complexity)
* `disableRead` - Set to `true` to disable read operations.
* `disableUpdate` - Set to `true` to disable update operations.
* `disableRemove` - Set to `true` to disable remove operations.
diff --git a/examples/complexity/e2e/fixtures.ts b/examples/complexity/e2e/fixtures.ts
new file mode 100644
index 000000000..33f8b86d5
--- /dev/null
+++ b/examples/complexity/e2e/fixtures.ts
@@ -0,0 +1,49 @@
+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';
+
+const tables = ['todo_item', 'sub_task', 'tag'];
+export const truncate = async (connection: Connection): Promise => {
+ await tables.reduce(async (prev, table) => {
+ await prev;
+ await connection.query(`TRUNCATE ${table} RESTART IDENTITY CASCADE`);
+ }, Promise.resolve());
+};
+
+export const refresh = async (connection: Connection): Promise => {
+ await truncate(connection);
+
+ const todoRepo = connection.getRepository(TodoItemEntity);
+ const subTaskRepo = connection.getRepository(SubTaskEntity);
+ const tagsRepo = connection.getRepository(TagEntity);
+
+ 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 = await todoRepo.save([
+ { title: 'Create Nest App', completed: true, tags: [urgentTag, homeTag] },
+ { title: 'Create Entity', completed: false, tags: [urgentTag, workTag] },
+ { title: 'Create Entity Service', completed: false, tags: [blockedTag, workTag] },
+ { title: 'Add Todo Item Resolver', completed: false, tags: [blockedTag, homeTag] },
+ {
+ title: 'How to create item With Sub Tasks',
+ completed: false,
+ tags: [questionTag, blockedTag],
+ },
+ ]);
+
+ 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/complexity/e2e/graphql-fragments.ts b/examples/complexity/e2e/graphql-fragments.ts
new file mode 100644
index 000000000..1feb340c2
--- /dev/null
+++ b/examples/complexity/e2e/graphql-fragments.ts
@@ -0,0 +1,39 @@
+export const todoItemFields = `
+ id
+ title
+ completed
+ description
+ `;
+
+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
+ }
+ `;
+};
diff --git a/examples/complexity/e2e/sub-task.resolver.spec.ts b/examples/complexity/e2e/sub-task.resolver.spec.ts
new file mode 100644
index 000000000..344877f12
--- /dev/null
+++ b/examples/complexity/e2e/sub-task.resolver.spec.ts
@@ -0,0 +1,785 @@
+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 { refresh } from './fixtures';
+import { edgeNodes, pageInfoField, subTaskFields, todoItemFields } from './graphql-fragments';
+
+describe('SubTaskResolver (complexity - 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 },
+ },
+ },
+ });
+ });
+ });
+ });
+
+ describe('query', () => {
+ it(`should return a connection`, () => {
+ return request(app.getHttpServer())
+ .post('/graphql')
+ .send({
+ operationName: null,
+ variables: {},
+ query: `{
+ subTasks {
+ ${pageInfoField}
+ ${edgeNodes(subTaskFields)}
+ }
+ }`,
+ })
+ .expect(200)
+ .then(({ body }) => {
+ const { edges, pageInfo }: CursorConnectionType = body.data.subTasks;
+ expect(pageInfo).toEqual({
+ endCursor: 'YXJyYXljb25uZWN0aW9uOjk=',
+ hasNextPage: true,
+ hasPreviousPage: false,
+ startCursor: 'YXJyYXljb25uZWN0aW9uOjA=',
+ });
+ 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)}
+ }
+ }`,
+ })
+ .expect(200)
+ .then(({ body }) => {
+ const { edges, pageInfo }: CursorConnectionType = body.data.subTasks;
+ expect(pageInfo).toEqual({
+ endCursor: 'YXJyYXljb25uZWN0aW9uOjI=',
+ hasNextPage: false,
+ hasPreviousPage: false,
+ startCursor: 'YXJyYXljb25uZWN0aW9uOjA=',
+ });
+ expect(edges).toHaveLength(3);
+ expect(edges.map((e) => e.node)).toEqual(subTasks.slice(0, 3));
+ });
+ });
+
+ it(`should fail if the query complexity is too high`, () => {
+ return request(app.getHttpServer())
+ .post('/graphql')
+ .send({
+ operationName: null,
+ variables: {},
+ query: `{
+ subTasks(filter: { id: { in: [1, 2, 3] } }) {
+ ${pageInfoField}
+ ${edgeNodes(`
+ ${subTaskFields}
+ todoItem {
+ ${todoItemFields}
+ subTasks {
+ ${pageInfoField}
+ ${edgeNodes(subTaskFields)}
+ }
+ }
+ `)}
+ }
+ }`,
+ })
+ .expect(400)
+ .then(({ body }) => {
+ expect(body).toEqual({
+ errors: [
+ {
+ extensions: { code: 'INTERNAL_SERVER_ERROR' },
+ message: 'Query is too complex: 41. Maximum allowed complexity: 30',
+ },
+ ],
+ });
+ });
+ });
+
+ it(`should allow sorting`, () => {
+ return request(app.getHttpServer())
+ .post('/graphql')
+ .send({
+ operationName: null,
+ variables: {},
+ query: `{
+ subTasks(sorting: [{field: id, direction: DESC}]) {
+ ${pageInfoField}
+ ${edgeNodes(subTaskFields)}
+ }
+ }`,
+ })
+ .expect(200)
+ .then(({ body }) => {
+ const { edges, pageInfo }: CursorConnectionType = body.data.subTasks;
+ expect(pageInfo).toEqual({
+ endCursor: 'YXJyYXljb25uZWN0aW9uOjk=',
+ hasNextPage: true,
+ hasPreviousPage: false,
+ startCursor: 'YXJyYXljb25uZWN0aW9uOjA=',
+ });
+ 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)}
+ }
+ }`,
+ })
+ .expect(200)
+ .then(({ body }) => {
+ const { edges, pageInfo }: CursorConnectionType = body.data.subTasks;
+ expect(pageInfo).toEqual({
+ endCursor: 'YXJyYXljb25uZWN0aW9uOjE=',
+ hasNextPage: true,
+ hasPreviousPage: false,
+ startCursor: 'YXJyYXljb25uZWN0aW9uOjA=',
+ });
+ 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)}
+ }
+ }`,
+ })
+ .expect(200)
+ .then(({ body }) => {
+ const { edges, pageInfo }: CursorConnectionType = body.data.subTasks;
+ expect(pageInfo).toEqual({
+ endCursor: 'YXJyYXljb25uZWN0aW9uOjM=',
+ hasNextPage: true,
+ hasPreviousPage: true,
+ startCursor: 'YXJyYXljb25uZWN0aW9uOjI=',
+ });
+ expect(edges).toHaveLength(2);
+ expect(edges.map((e) => e.node)).toEqual(subTasks.slice(2, 4));
+ });
+ });
+ });
+ });
+
+ 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 },
+ },
+ },
+ });
+ });
+ });
+ });
+
+ afterAll(async () => {
+ await app.close();
+ });
+});
diff --git a/examples/complexity/e2e/tag.resolver.spec.ts b/examples/complexity/e2e/tag.resolver.spec.ts
new file mode 100644
index 000000000..2ed92561e
--- /dev/null
+++ b/examples/complexity/e2e/tag.resolver.spec.ts
@@ -0,0 +1,744 @@
+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 } from './graphql-fragments';
+
+describe('TagResolver (complexity - 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')}
+ }
+ }
+ }`,
+ })
+ .expect(200)
+ .then(({ body }) => {
+ const { edges, pageInfo }: CursorConnectionType = body.data.tag.todoItems;
+ expect(pageInfo).toEqual({
+ endCursor: 'YXJyYXljb25uZWN0aW9uOjE=',
+ hasNextPage: false,
+ hasPreviousPage: false,
+ startCursor: 'YXJyYXljb25uZWN0aW9uOjA=',
+ });
+ expect(edges).toHaveLength(2);
+ expect(edges.map((e) => e.node.id)).toEqual(['1', '2']);
+ });
+ });
+ });
+
+ describe('query', () => {
+ it(`should return a connection`, () => {
+ return request(app.getHttpServer())
+ .post('/graphql')
+ .send({
+ operationName: null,
+ variables: {},
+ query: `{
+ tags {
+ ${pageInfoField}
+ ${edgeNodes(tagFields)}
+ }
+ }`,
+ })
+ .expect(200)
+ .then(({ body }) => {
+ const { edges, pageInfo }: CursorConnectionType = body.data.tags;
+ expect(pageInfo).toEqual({
+ endCursor: 'YXJyYXljb25uZWN0aW9uOjQ=',
+ hasNextPage: false,
+ hasPreviousPage: false,
+ startCursor: 'YXJyYXljb25uZWN0aW9uOjA=',
+ });
+ 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)}
+ }
+ }`,
+ })
+ .expect(200)
+ .then(({ body }) => {
+ const { edges, pageInfo }: CursorConnectionType = body.data.tags;
+ expect(pageInfo).toEqual({
+ endCursor: 'YXJyYXljb25uZWN0aW9uOjI=',
+ hasNextPage: false,
+ hasPreviousPage: false,
+ startCursor: 'YXJyYXljb25uZWN0aW9uOjA=',
+ });
+ expect(edges).toHaveLength(3);
+ expect(edges.map((e) => e.node)).toEqual(tags.slice(0, 3));
+ });
+ });
+
+ it(`should fail if query complexity is too high`, () => {
+ return request(app.getHttpServer())
+ .post('/graphql')
+ .send({
+ operationName: null,
+ variables: {},
+ query: `{
+ tags(filter: { id: { in: [1, 2, 3] } }) {
+ ${pageInfoField}
+ ${edgeNodes(`
+ ${tagFields}
+ todoItems {
+ ${pageInfoField}
+ ${edgeNodes(todoItemFields)}
+ }
+ `)}
+ }
+ }`,
+ })
+ .expect(400)
+ .then(({ body }) => {
+ expect(body).toEqual({
+ errors: [
+ {
+ extensions: { code: 'INTERNAL_SERVER_ERROR' },
+ message: 'Query is too complex: 33. Maximum allowed complexity: 30',
+ },
+ ],
+ });
+ });
+ });
+
+ it(`should allow sorting`, () => {
+ return request(app.getHttpServer())
+ .post('/graphql')
+ .send({
+ operationName: null,
+ variables: {},
+ query: `{
+ tags(sorting: [{field: id, direction: DESC}]) {
+ ${pageInfoField}
+ ${edgeNodes(tagFields)}
+ }
+ }`,
+ })
+ .expect(200)
+ .then(({ body }) => {
+ const { edges, pageInfo }: CursorConnectionType = body.data.tags;
+ expect(pageInfo).toEqual({
+ endCursor: 'YXJyYXljb25uZWN0aW9uOjQ=',
+ hasNextPage: false,
+ hasPreviousPage: false,
+ startCursor: 'YXJyYXljb25uZWN0aW9uOjA=',
+ });
+ 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)}
+ }
+ }`,
+ })
+ .expect(200)
+ .then(({ body }) => {
+ const { edges, pageInfo }: CursorConnectionType = body.data.tags;
+ expect(pageInfo).toEqual({
+ endCursor: 'YXJyYXljb25uZWN0aW9uOjE=',
+ hasNextPage: true,
+ hasPreviousPage: false,
+ startCursor: 'YXJyYXljb25uZWN0aW9uOjA=',
+ });
+ 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)}
+ }
+ }`,
+ })
+ .expect(200)
+ .then(({ body }) => {
+ const { edges, pageInfo }: CursorConnectionType = body.data.tags;
+ expect(pageInfo).toEqual({
+ endCursor: 'YXJyYXljb25uZWN0aW9uOjM=',
+ hasNextPage: true,
+ hasPreviousPage: true,
+ startCursor: 'YXJyYXljb25uZWN0aW9uOjI=',
+ });
+ expect(edges).toHaveLength(2);
+ expect(edges.map((e) => e.node)).toEqual(tags.slice(2, 4));
+ });
+ });
+ });
+ });
+
+ 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 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: '7', name: 'Create Many Tag - 1' },
+ { id: '8', name: 'Create Many Tag - 2' },
+ ],
+ },
+ });
+ });
+
+ 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 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: ["7", "8"]} },
+ update: { name: "Update Many Tag" }
+ }
+ ) {
+ updatedCount
+ }
+ }`,
+ })
+ .expect(200, {
+ data: {
+ updateManyTags: {
+ updatedCount: 2,
+ },
+ },
+ });
+ });
+
+ 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)}
+ }
+ }
+ }`,
+ })
+ .expect(200)
+ .then(({ body }) => {
+ const { edges, pageInfo }: CursorConnectionType = body.data.addTodoItemsToTag.todoItems;
+ expect(body.data.addTodoItemsToTag.id).toBe('1');
+ expect(edges).toHaveLength(5);
+ expect(pageInfo).toEqual({
+ endCursor: 'YXJyYXljb25uZWN0aW9uOjQ=',
+ hasNextPage: false,
+ hasPreviousPage: false,
+ startCursor: 'YXJyYXljb25uZWN0aW9uOjA=',
+ });
+ expect(edges.map((e) => e.node.title)).toEqual([
+ 'Create Nest App',
+ 'Create Entity',
+ 'Create Entity Service',
+ 'Add Todo Item Resolver',
+ '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)}
+ }
+ }
+ }`,
+ })
+ .expect(200)
+ .then(({ body }) => {
+ const { edges, pageInfo }: CursorConnectionType = body.data.removeTodoItemsFromTag.todoItems;
+ expect(body.data.removeTodoItemsFromTag.id).toBe('1');
+ expect(edges).toHaveLength(2);
+ expect(pageInfo).toEqual({
+ endCursor: 'YXJyYXljb25uZWN0aW9uOjE=',
+ hasNextPage: false,
+ hasPreviousPage: false,
+ startCursor: 'YXJyYXljb25uZWN0aW9uOjA=',
+ });
+ expect(edges.map((e) => e.node.title)).toEqual(['Create Nest App', 'Create Entity']);
+ });
+ });
+ });
+
+ afterAll(async () => {
+ await app.close();
+ });
+});
diff --git a/examples/complexity/e2e/todo-item.resolver.spec.ts b/examples/complexity/e2e/todo-item.resolver.spec.ts
new file mode 100644
index 000000000..9ecfd743d
--- /dev/null
+++ b/examples/complexity/e2e/todo-item.resolver.spec.ts
@@ -0,0 +1,860 @@
+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 { 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 } from './graphql-fragments';
+
+describe('TodoItemResolver (complexity - 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 },
+ },
+ });
+ });
+ });
+
+ 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 {
+ ${pageInfoField}
+ ${edgeNodes(subTaskFields)}
+ }
+ }
+ }`,
+ })
+ .expect(200)
+ .then(({ body }) => {
+ const { edges, pageInfo }: CursorConnectionType = body.data.todoItem.subTasks;
+ expect(edges).toHaveLength(3);
+ expect(pageInfo).toEqual({
+ endCursor: 'YXJyYXljb25uZWN0aW9uOjI=',
+ hasNextPage: false,
+ hasPreviousPage: false,
+ startCursor: 'YXJyYXljb25uZWN0aW9uOjA=',
+ });
+
+ edges.forEach((e) => expect(e.node.todoItemId).toBe('1'));
+ });
+ });
+
+ 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)}
+ }
+ }
+ }`,
+ })
+ .expect(200)
+ .then(({ body }) => {
+ const { edges, pageInfo }: CursorConnectionType = body.data.todoItem.tags;
+ expect(pageInfo).toEqual({
+ endCursor: 'YXJyYXljb25uZWN0aW9uOjE=',
+ hasNextPage: false,
+ hasPreviousPage: false,
+ startCursor: 'YXJyYXljb25uZWN0aW9uOjA=',
+ });
+ expect(edges).toHaveLength(2);
+
+ expect(edges.map((e) => e.node.name)).toEqual(['Urgent', 'Home']);
+ });
+ });
+ });
+
+ describe('query', () => {
+ it(`should return a connection`, () => {
+ return request(app.getHttpServer())
+ .post('/graphql')
+ .send({
+ operationName: null,
+ variables: {},
+ query: `{
+ todoItems {
+ ${pageInfoField}
+ ${edgeNodes(todoItemFields)}
+ }
+ }`,
+ })
+ .expect(200)
+ .then(({ body }) => {
+ const { edges, pageInfo }: CursorConnectionType = body.data.todoItems;
+ expect(pageInfo).toEqual({
+ endCursor: 'YXJyYXljb25uZWN0aW9uOjQ=',
+ hasNextPage: false,
+ hasPreviousPage: false,
+ startCursor: 'YXJyYXljb25uZWN0aW9uOjA=',
+ });
+ expect(edges).toHaveLength(5);
+
+ expect(edges.map((e) => e.node)).toEqual([
+ { id: '1', title: 'Create Nest App', completed: true, description: null },
+ { id: '2', title: 'Create Entity', completed: false, description: null },
+ { id: '3', title: 'Create Entity Service', completed: false, description: null },
+ { id: '4', title: 'Add Todo Item Resolver', completed: false, description: null },
+ { id: '5', title: 'How to create item With Sub Tasks', completed: false, description: null },
+ ]);
+ });
+ });
+
+ 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)}
+ }
+ }`,
+ })
+ .expect(200)
+ .then(({ body }) => {
+ const { edges, pageInfo }: CursorConnectionType = body.data.todoItems;
+ expect(pageInfo).toEqual({
+ endCursor: 'YXJyYXljb25uZWN0aW9uOjI=',
+ hasNextPage: false,
+ hasPreviousPage: false,
+ startCursor: 'YXJyYXljb25uZWN0aW9uOjA=',
+ });
+ expect(edges).toHaveLength(3);
+
+ expect(edges.map((e) => e.node)).toEqual([
+ { id: '1', title: 'Create Nest App', completed: true, description: null },
+ { id: '2', title: 'Create Entity', completed: false, description: null },
+ { id: '3', title: 'Create Entity Service', completed: false, description: null },
+ ]);
+ });
+ });
+
+ it(`should fail if the complexity is too high`, () => {
+ return request(app.getHttpServer())
+ .post('/graphql')
+ .send({
+ operationName: null,
+ variables: {},
+ query: `{
+ todoItems(filter: { id: { in: [1, 2, 3] } }) {
+ ${pageInfoField}
+ ${edgeNodes(`
+ ${todoItemFields}
+ subTasks {
+ ${pageInfoField}
+ ${edgeNodes(subTaskFields)}
+ }
+ `)}
+ }
+ }`,
+ })
+ .expect(400)
+ .then(({ body }) => {
+ expect(body).toEqual({
+ errors: [
+ {
+ extensions: { code: 'INTERNAL_SERVER_ERROR' },
+ message: 'Query is too complex: 31. Maximum allowed complexity: 30',
+ },
+ ],
+ });
+ });
+ });
+
+ it(`should allow sorting`, () => {
+ return request(app.getHttpServer())
+ .post('/graphql')
+ .send({
+ operationName: null,
+ variables: {},
+ query: `{
+ todoItems(sorting: [{field: id, direction: DESC}]) {
+ ${pageInfoField}
+ ${edgeNodes(todoItemFields)}
+ }
+ }`,
+ })
+ .expect(200)
+ .then(({ body }) => {
+ const { edges, pageInfo }: CursorConnectionType = body.data.todoItems;
+ expect(pageInfo).toEqual({
+ endCursor: 'YXJyYXljb25uZWN0aW9uOjQ=',
+ hasNextPage: false,
+ hasPreviousPage: false,
+ startCursor: 'YXJyYXljb25uZWN0aW9uOjA=',
+ });
+ 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 },
+ { id: '4', title: 'Add Todo Item Resolver', completed: false, description: null },
+ { id: '3', title: 'Create Entity Service', completed: false, description: null },
+ { id: '2', title: 'Create Entity', completed: false, description: null },
+ { id: '1', title: 'Create Nest App', completed: true, description: null },
+ ]);
+ });
+ });
+
+ 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)}
+ }
+ }`,
+ })
+ .expect(200)
+ .then(({ body }) => {
+ const { edges, pageInfo }: CursorConnectionType = body.data.todoItems;
+ expect(pageInfo).toEqual({
+ endCursor: 'YXJyYXljb25uZWN0aW9uOjE=',
+ hasNextPage: true,
+ hasPreviousPage: false,
+ startCursor: 'YXJyYXljb25uZWN0aW9uOjA=',
+ });
+ expect(edges).toHaveLength(2);
+
+ expect(edges.map((e) => e.node)).toEqual([
+ { id: '1', title: 'Create Nest App', completed: true, description: null },
+ { id: '2', title: 'Create Entity', completed: false, description: null },
+ ]);
+ });
+ });
+
+ 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)}
+ }
+ }`,
+ })
+ .expect(200)
+ .then(({ body }) => {
+ const { edges, pageInfo }: CursorConnectionType = body.data.todoItems;
+ expect(pageInfo).toEqual({
+ endCursor: 'YXJyYXljb25uZWN0aW9uOjM=',
+ hasNextPage: true,
+ hasPreviousPage: true,
+ startCursor: 'YXJyYXljb25uZWN0aW9uOjI=',
+ });
+ expect(edges).toHaveLength(2);
+
+ expect(edges.map((e) => e.node)).toEqual([
+ { id: '3', title: 'Create Entity Service', completed: false, description: null },
+ { id: '4', title: 'Add Todo Item Resolver', completed: false, description: null },
+ ]);
+ });
+ });
+ });
+ });
+
+ describe('create one', () => {
+ it('should allow creating a todoItem', () => {
+ return request(app.getHttpServer())
+ .post('/graphql')
+ .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 validate a todoItem', () => {
+ return request(app.getHttpServer())
+ .post('/graphql')
+ .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 allow creating a todoItem', () => {
+ return request(app.getHttpServer())
+ .post('/graphql')
+ .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: '7', title: 'Many Test Todo 1', completed: false },
+ { id: '8', title: 'Many Test Todo 2', completed: true },
+ ],
+ },
+ });
+ });
+
+ it('should validate a todoItem', () => {
+ return request(app.getHttpServer())
+ .post('/graphql')
+ .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 allow updating a todoItem', () => {
+ 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
+ }
+ }`,
+ })
+ .expect(200, {
+ data: {
+ updateOneTodoItem: {
+ id: '6',
+ title: 'Update Test Todo',
+ completed: true,
+ },
+ },
+ });
+ });
+
+ it('should require an id', () => {
+ return request(app.getHttpServer())
+ .post('/graphql')
+ .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')
+ .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 allow updating a todoItem', () => {
+ return request(app.getHttpServer())
+ .post('/graphql')
+ .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 require a filter', () => {
+ return request(app.getHttpServer())
+ .post('/graphql')
+ .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')
+ .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 allow deleting a todoItem', () => {
+ return request(app.getHttpServer())
+ .post('/graphql')
+ .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')
+ .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 allow updating a todoItem', () => {
+ return request(app.getHttpServer())
+ .post('/graphql')
+ .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')
+ .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')
+ .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')
+ .send({
+ operationName: null,
+ variables: {},
+ query: `mutation {
+ addSubTasksToTodoItem(
+ input: {
+ id: 1,
+ relationIds: ["4", "5", "6"]
+ }
+ ) {
+ id
+ title
+ subTasks {
+ ${pageInfoField}
+ ${edgeNodes(subTaskFields)}
+ }
+ }
+ }`,
+ })
+ .expect(200)
+ .then(({ body }) => {
+ const { edges, pageInfo }: CursorConnectionType = body.data.addSubTasksToTodoItem.subTasks;
+ expect(body.data.addSubTasksToTodoItem.id).toBe('1');
+ expect(edges).toHaveLength(6);
+ expect(pageInfo).toEqual({
+ endCursor: 'YXJyYXljb25uZWN0aW9uOjU=',
+ hasNextPage: false,
+ hasPreviousPage: false,
+ startCursor: 'YXJyYXljb25uZWN0aW9uOjA=',
+ });
+ edges.forEach((e) => expect(e.node.todoItemId).toBe('1'));
+ });
+ });
+ });
+
+ describe('addTagsToTodoItem', () => {
+ it('allow adding subTasks to a todoItem', () => {
+ return request(app.getHttpServer())
+ .post('/graphql')
+ .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)}
+ }
+ }
+ }`,
+ })
+ .expect(200)
+ .then(({ body }) => {
+ const { edges, pageInfo }: CursorConnectionType = body.data.addTagsToTodoItem.tags;
+ expect(body.data.addTagsToTodoItem.id).toBe('1');
+ expect(edges).toHaveLength(5);
+ expect(pageInfo).toEqual({
+ endCursor: 'YXJyYXljb25uZWN0aW9uOjQ=',
+ hasNextPage: false,
+ hasPreviousPage: false,
+ startCursor: 'YXJyYXljb25uZWN0aW9uOjA=',
+ });
+ 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')
+ .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)}
+ }
+ }
+ }`,
+ })
+ .expect(200)
+ .then(({ body }) => {
+ const { edges, pageInfo }: CursorConnectionType = body.data.removeTagsFromTodoItem.tags;
+ expect(body.data.removeTagsFromTodoItem.id).toBe('1');
+ expect(edges).toHaveLength(2);
+ expect(pageInfo).toEqual({
+ endCursor: 'YXJyYXljb25uZWN0aW9uOjE=',
+ hasNextPage: false,
+ hasPreviousPage: false,
+ startCursor: 'YXJyYXljb25uZWN0aW9uOjA=',
+ });
+ expect(edges.map((e) => e.node.name)).toEqual(['Urgent', 'Home']);
+ });
+ });
+ });
+
+ afterAll(async () => {
+ await app.close();
+ });
+});
diff --git a/examples/complexity/ormconfig.json b/examples/complexity/ormconfig.json
new file mode 100644
index 000000000..e55342aa0
--- /dev/null
+++ b/examples/complexity/ormconfig.json
@@ -0,0 +1,9 @@
+{
+ "type": "postgres",
+ "host": "localhost",
+ "port": 5436,
+ "username": "complexity",
+ "database": "complexity",
+ "autoLoadEntities": true,
+ "synchronize": true
+}
diff --git a/examples/complexity/src/app.module.ts b/examples/complexity/src/app.module.ts
new file mode 100644
index 000000000..3eeaed138
--- /dev/null
+++ b/examples/complexity/src/app.module.ts
@@ -0,0 +1,22 @@
+import { Module } from '@nestjs/common';
+import { GraphQLModule } from '@nestjs/graphql';
+import { TypeOrmModule, TypeOrmModuleOptions } from '@nestjs/typeorm';
+import { ComplexityPlugin } from './complexity.plugin';
+import { TagModule } from './tag/tag.module';
+import { TodoItemModule } from './todo-item/todo-item.module';
+import { SubTaskModule } from './sub-task/sub-task.module';
+import * as ormconfig from '../ormconfig.json';
+
+@Module({
+ providers: [ComplexityPlugin],
+ imports: [
+ TypeOrmModule.forRoot(ormconfig as TypeOrmModuleOptions),
+ GraphQLModule.forRoot({
+ autoSchemaFile: 'schema.gql',
+ }),
+ SubTaskModule,
+ TodoItemModule,
+ TagModule,
+ ],
+})
+export class AppModule {}
diff --git a/examples/complexity/src/complexity.plugin.ts b/examples/complexity/src/complexity.plugin.ts
new file mode 100644
index 000000000..8930609dc
--- /dev/null
+++ b/examples/complexity/src/complexity.plugin.ts
@@ -0,0 +1,38 @@
+import { GraphQLSchemaHost, Plugin } from '@nestjs/graphql';
+import { ApolloServerPlugin, GraphQLRequestListener } from 'apollo-server-plugin-base';
+import { GraphQLError } from 'graphql';
+import { fieldExtensionsEstimator, getComplexity, simpleEstimator } from 'graphql-query-complexity';
+import { Logger, LoggerService } from '@nestjs/common';
+
+@Plugin()
+export class ComplexityPlugin implements ApolloServerPlugin {
+ readonly logger: LoggerService;
+
+ private maxComplexity = 30;
+
+ constructor(private gqlSchemaHost: GraphQLSchemaHost) {
+ this.logger = new Logger('complexity-plugin');
+ }
+
+ requestDidStart(): GraphQLRequestListener {
+ const { schema } = this.gqlSchemaHost;
+
+ return {
+ didResolveOperation: ({ request, document }) => {
+ const complexity = getComplexity({
+ schema,
+ operationName: request.operationName,
+ query: document,
+ variables: request.variables,
+ estimators: [fieldExtensionsEstimator(), simpleEstimator({ defaultComplexity: 1 })],
+ });
+ if (complexity >= this.maxComplexity) {
+ throw new GraphQLError(
+ `Query is too complex: ${complexity}. Maximum allowed complexity: ${this.maxComplexity}`,
+ );
+ }
+ this.logger.log(`Query Complexity: ${complexity}`);
+ },
+ };
+ }
+}
diff --git a/examples/complexity/src/main.ts b/examples/complexity/src/main.ts
new file mode 100644
index 000000000..1e648c23e
--- /dev/null
+++ b/examples/complexity/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/complexity/src/sub-task/dto/sub-task.dto.ts b/examples/complexity/src/sub-task/dto/sub-task.dto.ts
new file mode 100644
index 000000000..2d6ad4b45
--- /dev/null
+++ b/examples/complexity/src/sub-task/dto/sub-task.dto.ts
@@ -0,0 +1,28 @@
+import { FilterableField, Relation } from '@nestjs-query/query-graphql';
+import { ObjectType, ID, GraphQLISODateTime } from '@nestjs/graphql';
+import { TodoItemDTO } from '../../todo-item/dto/todo-item.dto';
+
+@ObjectType('SubTask')
+@Relation('todoItem', () => TodoItemDTO, { disableRemove: true, complexity: 5 })
+export class SubTaskDTO {
+ @FilterableField(() => ID, { complexity: 1 })
+ id!: number;
+
+ @FilterableField({ complexity: 1 })
+ title!: string;
+
+ @FilterableField({ nullable: true, complexity: 1 })
+ description?: string;
+
+ @FilterableField({ complexity: 1 })
+ completed!: boolean;
+
+ @FilterableField(() => GraphQLISODateTime, { complexity: 1 })
+ created!: Date;
+
+ @FilterableField(() => GraphQLISODateTime, { complexity: 1 })
+ updated!: Date;
+
+ @FilterableField({ complexity: 1 })
+ todoItemId!: string;
+}
diff --git a/examples/complexity/src/sub-task/dto/subtask-input.dto.ts b/examples/complexity/src/sub-task/dto/subtask-input.dto.ts
new file mode 100644
index 000000000..c82805568
--- /dev/null
+++ b/examples/complexity/src/sub-task/dto/subtask-input.dto.ts
@@ -0,0 +1,24 @@
+import { Field, InputType, ID } from '@nestjs/graphql';
+import { IsOptional, IsString, IsBoolean, IsNotEmpty } from 'class-validator';
+
+@InputType('SubTaskInput')
+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/complexity/src/sub-task/dto/subtask-update.dto.ts b/examples/complexity/src/sub-task/dto/subtask-update.dto.ts
new file mode 100644
index 000000000..341a93562
--- /dev/null
+++ b/examples/complexity/src/sub-task/dto/subtask-update.dto.ts
@@ -0,0 +1,27 @@
+import { Field, InputType } from '@nestjs/graphql';
+import { IsOptional, IsBoolean, IsString, IsNotEmpty } from 'class-validator';
+
+@InputType('SubTaskUpdate')
+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/complexity/src/sub-task/sub-task.entity.ts b/examples/complexity/src/sub-task/sub-task.entity.ts
new file mode 100644
index 000000000..eb60eab7e
--- /dev/null
+++ b/examples/complexity/src/sub-task/sub-task.entity.ts
@@ -0,0 +1,42 @@
+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;
+}
diff --git a/examples/complexity/src/sub-task/sub-task.module.ts b/examples/complexity/src/sub-task/sub-task.module.ts
new file mode 100644
index 000000000..a0c718eca
--- /dev/null
+++ b/examples/complexity/src/sub-task/sub-task.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 { 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,
+ },
+ ],
+ }),
+ ],
+})
+export class SubTaskModule {}
diff --git a/examples/complexity/src/tag/dto/tag-input.dto.ts b/examples/complexity/src/tag/dto/tag-input.dto.ts
new file mode 100644
index 000000000..7549644d1
--- /dev/null
+++ b/examples/complexity/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/complexity/src/tag/dto/tag.dto.ts b/examples/complexity/src/tag/dto/tag.dto.ts
new file mode 100644
index 000000000..33837e9c6
--- /dev/null
+++ b/examples/complexity/src/tag/dto/tag.dto.ts
@@ -0,0 +1,19 @@
+import { FilterableField, Connection } from '@nestjs-query/query-graphql';
+import { ObjectType, ID, GraphQLISODateTime } from '@nestjs/graphql';
+import { TodoItemDTO } from '../../todo-item/dto/todo-item.dto';
+
+@ObjectType('Tag')
+@Connection('todoItems', () => TodoItemDTO, { complexity: 10 })
+export class TagDTO {
+ @FilterableField(() => ID, { complexity: 1 })
+ id!: number;
+
+ @FilterableField({ complexity: 1 })
+ name!: string;
+
+ @FilterableField(() => GraphQLISODateTime, { complexity: 1 })
+ created!: Date;
+
+ @FilterableField(() => GraphQLISODateTime, { complexity: 1 })
+ updated!: Date;
+}
diff --git a/examples/complexity/src/tag/tag.entity.ts b/examples/complexity/src/tag/tag.entity.ts
new file mode 100644
index 000000000..52bcf3165
--- /dev/null
+++ b/examples/complexity/src/tag/tag.entity.ts
@@ -0,0 +1,28 @@
+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[];
+}
diff --git a/examples/complexity/src/tag/tag.module.ts b/examples/complexity/src/tag/tag.module.ts
new file mode 100644
index 000000000..cb12812f6
--- /dev/null
+++ b/examples/complexity/src/tag/tag.module.ts
@@ -0,0 +1,24 @@
+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,
+ },
+ ],
+ }),
+ ],
+})
+export class TagModule {}
diff --git a/examples/complexity/src/todo-item/dto/todo-item-input.dto.ts b/examples/complexity/src/todo-item/dto/todo-item-input.dto.ts
new file mode 100644
index 000000000..7fa13e9a5
--- /dev/null
+++ b/examples/complexity/src/todo-item/dto/todo-item-input.dto.ts
@@ -0,0 +1,14 @@
+import { IsString, MaxLength, IsBoolean } from 'class-validator';
+import { Field, InputType } from '@nestjs/graphql';
+
+@InputType('TodoItemInput')
+export class TodoItemInputDTO {
+ @IsString()
+ @MaxLength(20)
+ @Field()
+ title!: string;
+
+ @IsBoolean()
+ @Field()
+ completed!: boolean;
+}
diff --git a/examples/complexity/src/todo-item/dto/todo-item-update.dto.ts b/examples/complexity/src/todo-item/dto/todo-item-update.dto.ts
new file mode 100644
index 000000000..9b3b8cf18
--- /dev/null
+++ b/examples/complexity/src/todo-item/dto/todo-item-update.dto.ts
@@ -0,0 +1,16 @@
+import { IsString, MaxLength, IsBoolean, IsOptional } from 'class-validator';
+import { Field, InputType } from '@nestjs/graphql';
+
+@InputType('TodoItemUpdate')
+export class TodoItemUpdateDTO {
+ @IsOptional()
+ @IsString()
+ @MaxLength(20)
+ @Field({ nullable: true })
+ title?: string;
+
+ @IsOptional()
+ @IsBoolean()
+ @Field({ nullable: true })
+ completed?: boolean;
+}
diff --git a/examples/complexity/src/todo-item/dto/todo-item.dto.ts b/examples/complexity/src/todo-item/dto/todo-item.dto.ts
new file mode 100644
index 000000000..9074c99c2
--- /dev/null
+++ b/examples/complexity/src/todo-item/dto/todo-item.dto.ts
@@ -0,0 +1,27 @@
+import { FilterableField, Connection } from '@nestjs-query/query-graphql';
+import { ObjectType, ID, GraphQLISODateTime } from '@nestjs/graphql';
+import { SubTaskDTO } from '../../sub-task/dto/sub-task.dto';
+import { TagDTO } from '../../tag/dto/tag.dto';
+
+@ObjectType('TodoItem')
+@Connection('subTasks', () => SubTaskDTO, { disableRemove: true, complexity: 5 })
+@Connection('tags', () => TagDTO, { complexity: 10 })
+export class TodoItemDTO {
+ @FilterableField(() => ID, { complexity: 1 })
+ id!: number;
+
+ @FilterableField({ complexity: 1 })
+ title!: string;
+
+ @FilterableField({ nullable: true, complexity: 1 })
+ description?: string;
+
+ @FilterableField({ complexity: 1 })
+ completed!: boolean;
+
+ @FilterableField(() => GraphQLISODateTime, { complexity: 1 })
+ created!: Date;
+
+ @FilterableField(() => GraphQLISODateTime, { complexity: 1 })
+ updated!: Date;
+}
diff --git a/examples/complexity/src/todo-item/todo-item.entity.ts b/examples/complexity/src/todo-item/todo-item.entity.ts
new file mode 100644
index 000000000..4590b777c
--- /dev/null
+++ b/examples/complexity/src/todo-item/todo-item.entity.ts
@@ -0,0 +1,40 @@
+import {
+ Column,
+ CreateDateColumn,
+ Entity,
+ PrimaryGeneratedColumn,
+ UpdateDateColumn,
+ OneToMany,
+ ManyToMany,
+ JoinTable,
+} from 'typeorm';
+import { SubTaskEntity } from '../sub-task/sub-task.entity';
+import { TagEntity } from '../tag/tag.entity';
+
+@Entity({ name: 'todo_item' })
+export class TodoItemEntity {
+ @PrimaryGeneratedColumn()
+ id!: number;
+
+ @Column()
+ title!: string;
+
+ @Column({ nullable: true })
+ description?: string;
+
+ @Column()
+ completed!: boolean;
+
+ @OneToMany(() => SubTaskEntity, (subTask) => subTask.todoItem)
+ subTasks!: SubTaskEntity[];
+
+ @CreateDateColumn()
+ created!: Date;
+
+ @UpdateDateColumn()
+ updated!: Date;
+
+ @ManyToMany(() => TagEntity, (tag) => tag.todoItems)
+ @JoinTable()
+ tags!: TagEntity[];
+}
diff --git a/examples/complexity/src/todo-item/todo-item.module.ts b/examples/complexity/src/todo-item/todo-item.module.ts
new file mode 100644
index 000000000..6a40cf70e
--- /dev/null
+++ b/examples/complexity/src/todo-item/todo-item.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 { TodoItemInputDTO } from './dto/todo-item-input.dto';
+import { TodoItemUpdateDTO } from './dto/todo-item-update.dto';
+import { TodoItemDTO } from './dto/todo-item.dto';
+import { TodoItemEntity } from './todo-item.entity';
+
+@Module({
+ imports: [
+ NestjsQueryGraphQLModule.forFeature({
+ imports: [NestjsQueryTypeOrmModule.forFeature([TodoItemEntity])],
+ resolvers: [
+ {
+ DTOClass: TodoItemDTO,
+ EntityClass: TodoItemEntity,
+ CreateDTOClass: TodoItemInputDTO,
+ UpdateDTOClass: TodoItemUpdateDTO,
+ enableTotalCount: true,
+ },
+ ],
+ }),
+ ],
+})
+export class TodoItemModule {}
diff --git a/examples/init-scripts/init-complexity.sql b/examples/init-scripts/init-complexity.sql
new file mode 100644
index 000000000..036819aec
--- /dev/null
+++ b/examples/init-scripts/init-complexity.sql
@@ -0,0 +1,3 @@
+CREATE USER complexity WITH SUPERUSER;
+CREATE DATABASE complexity;
+GRANT ALL PRIVILEGES ON DATABASE complexity TO complexity;
diff --git a/examples/nest-cli.json b/examples/nest-cli.json
index 0f57ac37f..cb3242972 100644
--- a/examples/nest-cli.json
+++ b/examples/nest-cli.json
@@ -11,6 +11,15 @@
"tsConfigPath": "./tsconfig.build.json"
}
},
+ "complexity": {
+ "type": "application",
+ "root": "complexity",
+ "entryFile": "main",
+ "sourceRoot": "complexity/src",
+ "compilerOptions": {
+ "tsConfigPath": "./tsconfig.build.json"
+ }
+ },
"federation:gateway": {
"type": "application",
"root": "federation/gateway",
diff --git a/examples/package.json b/examples/package.json
index 38e993c76..955a4a62d 100644
--- a/examples/package.json
+++ b/examples/package.json
@@ -22,6 +22,8 @@
"@nestjs/platform-express": "7.3.2",
"@nestjs/sequelize": "0.1.0",
"@nestjs/typeorm": "7.1.0",
+ "graphql-query-complexity": "0.6.0",
+ "apollo-server-plugin-base": "0.9.1",
"apollo-server-express": "2.15.1",
"class-validator": "0.12.2",
"graphql": "15.3.0",
diff --git a/package-lock.json b/package-lock.json
index 3150f156a..e63a0a086 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8732,6 +8732,15 @@
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.1.0.tgz",
"integrity": "sha512-1Yj8h9Q+QDF5FzhMs/c9+6UntbD5MkRfRwac8DoEm9ZfUBZ7tZ55YcGVAzEe4bXsdQHEk+s9S5wsOKVdZrw0tQ=="
},
+ "bindings": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
+ "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
+ "optional": true,
+ "requires": {
+ "file-uri-to-path": "1.0.0"
+ }
+ },
"block-stream": {
"version": "0.0.9",
"resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz",
@@ -12280,6 +12289,12 @@
"flat-cache": "^2.0.1"
}
},
+ "file-uri-to-path": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
+ "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",
+ "optional": true
+ },
"fill-range": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz",
@@ -13263,6 +13278,14 @@
"apollo-server-types": "^0.5.1"
}
},
+ "graphql-query-complexity": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/graphql-query-complexity/-/graphql-query-complexity-0.6.0.tgz",
+ "integrity": "sha512-xYLLvN7PSeSYtiQ2nR53B7B/C6iGOWVPBrzcpbheVxE78GYdiNZffDRNCoMvYGCOAY0eQTpsnTfwncVszQsOJw==",
+ "requires": {
+ "lodash.get": "^4.4.2"
+ }
+ },
"graphql-relay": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/graphql-relay/-/graphql-relay-0.6.0.tgz",
@@ -17645,8 +17668,7 @@
"lodash.get": {
"version": "4.4.2",
"resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
- "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=",
- "dev": true
+ "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk="
},
"lodash.ismatch": {
"version": "4.4.0",
@@ -20653,9 +20675,9 @@
}
},
"sequelize": {
- "version": "5.22.1",
- "resolved": "https://registry.npmjs.org/sequelize/-/sequelize-5.22.1.tgz",
- "integrity": "sha512-G6PnNdx0dbUK6eN9rPGmNswXE8BhndAGuvgZADD/kE5Y9Wn3Rh073IR8C7AkEzvOGub4bf4CHJX/z8Mpg95TUA==",
+ "version": "5.22.3",
+ "resolved": "https://registry.npmjs.org/sequelize/-/sequelize-5.22.3.tgz",
+ "integrity": "sha512-+nxf4TzdrB+PRmoWhR05TP9ukLAurK7qtKcIFv5Vhxm5Z9v+d2PcTT6Ea3YAoIQVkZ47QlT9XWAIUevMT/3l8Q==",
"requires": {
"bluebird": "^3.5.0",
"cls-bluebird": "^2.1.0",
@@ -23047,6 +23069,7 @@
"integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==",
"optional": true,
"requires": {
+ "bindings": "^1.5.0",
"nan": "^2.12.1"
}
},
diff --git a/packages/query-graphql/src/resolvers/relations/read-relations.resolver.ts b/packages/query-graphql/src/resolvers/relations/read-relations.resolver.ts
index a5cf1b7cf..56c9bbc1a 100644
--- a/packages/query-graphql/src/resolvers/relations/read-relations.resolver.ts
+++ b/packages/query-graphql/src/resolvers/relations/read-relations.resolver.ts
@@ -31,7 +31,12 @@ const ReadOneRelationMixin = (DTOClass: Class, relation: Res
@Resolver(() => DTOClass, { isAbstract: true })
class ReadOneMixin extends Base {
- @ResolverField(baseNameLower, () => relationDTO, { nullable: relation.nullable }, commonResolverOpts)
+ @ResolverField(
+ baseNameLower,
+ () => relationDTO,
+ { nullable: relation.nullable, complexity: relation.complexity },
+ commonResolverOpts,
+ )
[`find${baseName}`](@Parent() dto: DTO, @Context() context: ExecutionContext): Promise {
return DataLoaderFactory.getOrCreateLoader(context, loaderName, findLoader.createLoader(this.service)).load(dto);
}
@@ -63,7 +68,12 @@ const ReadManyRelationMixin = (DTOClass: Class, relation: Re
const CT = ConnectionType(relationDTO, RelationQA, { ...relation, connectionName });
@Resolver(() => DTOClass, { isAbstract: true })
class ReadManyMixin extends Base {
- @ResolverField(pluralBaseNameLower, () => CT.resolveType, { nullable: relation.nullable }, commonResolverOpts)
+ @ResolverField(
+ pluralBaseNameLower,
+ () => CT.resolveType,
+ { nullable: relation.nullable, complexity: relation.complexity },
+ commonResolverOpts,
+ )
async [`query${pluralBaseName}`](
@Parent() dto: DTO,
@Args() q: RelationQA,
diff --git a/packages/query-graphql/src/resolvers/relations/references-relation.resolver.ts b/packages/query-graphql/src/resolvers/relations/references-relation.resolver.ts
index 458ddd8cf..d2908469c 100644
--- a/packages/query-graphql/src/resolvers/relations/references-relation.resolver.ts
+++ b/packages/query-graphql/src/resolvers/relations/references-relation.resolver.ts
@@ -27,7 +27,12 @@ const ReferencesMixin = (DTOClass: Class, reference: Resolve
@Resolver(() => DTOClass, { isAbstract: true })
class ReadOneMixin extends Base {
- @ResolverField(baseNameLower, () => relationDTO, { nullable: reference.nullable }, commonResolverOpts)
+ @ResolverField(
+ baseNameLower,
+ () => relationDTO,
+ { nullable: reference.nullable, complexity: reference.complexity },
+ commonResolverOpts,
+ )
[`${baseNameLower}Reference`](@Parent() dto: DTO): RepresentationType {
// eslint-disable-next-line @typescript-eslint/naming-convention
return { __typename: baseName, ...pluckFields(dto, reference.keys) };
diff --git a/packages/query-graphql/src/resolvers/relations/relations.interface.ts b/packages/query-graphql/src/resolvers/relations/relations.interface.ts
index 08991f22b..61d54b4fb 100644
--- a/packages/query-graphql/src/resolvers/relations/relations.interface.ts
+++ b/packages/query-graphql/src/resolvers/relations/relations.interface.ts
@@ -1,4 +1,5 @@
import { Class } from '@nestjs-query/core';
+import { Complexity } from '@nestjs/graphql';
import { DTONamesOpts } from '../../common';
import { ResolverMethodOpts } from '../../decorators';
import { QueryArgsTypeOpts } from '../../types';
@@ -23,6 +24,8 @@ export interface ResolverRelationReference extends DTONamesOpts,
* Set to true if the relation is nullable
*/
nullable?: boolean;
+
+ complexity?: Complexity;
}
export type ResolverRelation = {
@@ -63,6 +66,8 @@ export type ResolverRelation = {
* This will only work with relations defined through an ORM (typeorm or sequelize).
*/
allowFiltering?: boolean;
+
+ complexity?: Complexity;
} & DTONamesOpts &
ResolverMethodOpts &
QueryArgsTypeOpts &