Giter Site home page Giter Site logo

Comments (6)

TriPSs avatar TriPSs commented on May 28, 2024

Could you elaborate a bit more on this? I never use/used a cursor but would offset not defeat the purpose of it?

from nestjs-query.

smolinari avatar smolinari commented on May 28, 2024

My understanding is, there are two different types of pagination. Cursor allows for scroll and "get more" kind of pagination. Whereas, offset also allows for showing how many pages are available and navigating better between them. The offset method is good for very large datasets and the need to navigate within those sets. Thing is, and again as I understand nestjs-query, the offset method is possible. Maybe I'm missing something (too), cause to be honest, I've not set up pagination at all yet. So, I too would like more elaboration for my understanding. :)

Scott

from nestjs-query.

Goopil avatar Goopil commented on May 28, 2024

offset paging is built in. (I'm using it most of the time to be honest)

here is a full exemple. I've seen you have a lot of vuejs fork on your account so here is a somewhat full example of what i came up with. There is a lot to improve but this should get you started. I'm open to suggestions if you have any.

this example use Quasar framework and urql

module def with offset strategy

import {
  NestjsQueryGraphQLModule,
  PagingStrategies,
} from '@ptc-org/nestjs-query-graphql';
import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm';
import { Module } from '@nestjs/common';

import { EventEntity } from './event.entity';
import { EventDto } from './dto/event.dto';
import { CreateEventDto } from './dto/create-event.dto';
import { UpdateEventDto } from './dto/update-event.dto';

@Module({
  imports: [
    NestjsQueryGraphQLModule.forFeature({
      imports: [NestjsQueryTypeOrmModule.forFeature([EventEntity])],
      resolvers: [
        {
          pagingStrategy: PagingStrategies.OFFSET,
          enableTotalCount: true,
          enableSubscriptions: true,
          DTOClass: EventDto,
          EntityClass: EventEntity,
          CreateDTOClass: CreateEventDto,
          UpdateDTOClass: UpdateEventDto,
        },
      ],
    }),
  ],
})
export class EventModule {}
import { FilterableField } from '@ptc-org/nestjs-query-graphql';
import { GraphQLISODateTime, ID, ObjectType } from '@nestjs/graphql';

@ObjectType('Event')
export class EventDto {
  @FilterableField(() => ID)
  id!: number;

  @FilterableField()
  name!: string;

  @FilterableField({ nullable: true })
  description?: string;

  @FilterableField(() => GraphQLISODateTime, { filterOnly: true })
  created!: Date;

  @FilterableField(() => GraphQLISODateTime, { filterOnly: true })
  updated!: Date;
}

table component

<template>
  <q-table
    class="sticky-header-table"
    square
    :style="{height}"
    flat
    @request="onRequest"
    v-model:pagination="pagination"
    :rows="data"
    :columns="columns"
    row-key="id"
    virtual-scroll
  >
    <template #top-left>
      <slot name="top"></slot>
    </template>

    <template #top-right>
      <q-input
        standout
        dense
        debounce="300"
        v-model="search"
        placeholder="Search"
      >
        <template v-slot:append>
          <q-icon name="search"/>
        </template>
      </q-input>
    </template>

  </q-table>
</template>

<script lang="ts">
import {defineComponent, ref, watch, toRefs} from 'vue';
import {useQuery} from '@urql/vue';
import {useGqlPagination} from "../composables/pagination";

const FETCH_QUERY = `
query fetchMultipleEvent($paging: OffsetPaging!, $sorting: [EventSort!], $filter: EventFilter)  {
  events (paging: $paging, sorting: $sorting, filter: $filter) {
    totalCount
    nodes {
      __typename
      id
      name
      description
    }
  }
}
`;

export default defineComponent({
  name: 'EventTable',
  props: {height: {type: String, default: '100vh'}},
  setup(props) {
    const {height} = toRefs(props);
    const columns = ref([
      {name: 'id', label: 'id', field: 'id', sortable: true},
      {name: 'name', label: 'name', field: 'name', sortable: true},
      {name: 'description', label: 'description', field: 'description', sortable: true},
      {name: 'actions', label: 'actions'},
    ]);

    const data = ref([]);
    const {
      pagination,
      search,
      variables,
      onRequest
    } = useGqlPagination({filter: {fields: [['name', 'like'], ['description', 'like']]}})
    const result = useQuery({
      query: FETCH_QUERY,
      variables,
      requestPolicy: 'cache-and-network'
    });

    watch(result.data, () => {
      const payload = result.data?.value?.events ?? {};

      data.value = payload.nodes ?? [];
      pagination.value.rowsNumber = payload.totalCount ?? 0;
    }, {immediate: true});

    return {
      height,
      data,
      pagination,
      columns,
      search,
      onRequest
    };
  }
});
</script>

composable/pagination.ts

import {computed, ref, Ref} from "vue";

type PaginationProps = {
  sortBy: string,
  descending: boolean,
  page: number,
  rowsPerPage: number,
  rowsNumber: number
};

export type filterOptions = {
  fields: [string, string?][],
  minChar?: number,
};

type Options = {
  pagination?: PaginationProps,
  filter?: filterOptions
};

function formatPagination({
  page,
  rowsPerPage,
  rowsNumber,
  descending,
  sortBy
}: Partial<PaginationProps> = <Partial<PaginationProps>>{}) {
  return {
    sortBy: sortBy ?? 'id',
    descending: descending ?? false,
    page: page ?? 1,
    rowsPerPage: rowsPerPage ?? 15,
    rowsNumber: rowsNumber ?? 0
  };
}

function formatPage(pagination: Ref<PaginationProps>) {
  const {page, rowsPerPage} = pagination.value
  return {
    offset: (page - 1) * rowsPerPage,
    limit: rowsPerPage
  };
}

function formatSort(pagination: Ref<PaginationProps>) {
  const {sortBy, descending} = pagination.value
  return sortBy ? [{
    field: sortBy,
    direction: descending ? 'ASC' : 'DESC'
  }] : undefined;
}


function formatFilter(search: Ref<string>, {
  fields = [],
  minChar = 0
}: Partial<filterOptions> = <filterOptions>{}) {
  const trimmed = search.value.trim()

  if (
    trimmed.length <= minChar
    || fields.length === 0
  ) {
    return undefined;
  }

  return {
    or: fields.map(([field, comparator = 'like']) => {
      const value = ['like', 'ilike'].includes(comparator) ? `%${trimmed}%` : trimmed;
      return {
        [field]: {[comparator]: value}
      };
    })
  }
}

export const useGqlPagination = (options: Options = <Options>{}) => {
  const search = ref('');
  const pagination = ref<PaginationProps>(formatPagination(options?.pagination));

  const onRequest = (props: {pagination : PaginationProps}) => {
    pagination.value.page = props.pagination.page
    pagination.value.rowsPerPage = props.pagination.rowsPerPage
    pagination.value.sortBy = props.pagination.sortBy
    pagination.value.descending = props.pagination.descending
  };

  const variables = computed(() => ({
    paging: formatPage(pagination),
    sorting: formatSort(pagination),
    filter: formatFilter(search, options?.filter)
  }));

  return {
    pagination,
    search,
    variables,
    onRequest
  };
}

isomorphic urql client setup

// @ts-ignore
import 'isomorphic-unfetch';
import urql, {
    dedupExchange,
    errorExchange,
    fetchExchange,
    cacheExchange,
    ssrExchange
} from '@urql/vue';
// @ts-ignore
import {SSRData} from '@urql/core/dist/types/exchanges/ssr';
import {ClientOptions} from "@urql/core/dist/types/client";
export type GraphqlOptions = {
    app: any;
    ssrContext?: any;
    clientOptions: ClientOptions,
    ssr?: boolean;
    subscriptions?: boolean,
    onError?: (error: unknown) => void
    onUnauthorized?: (error: unknown) => void
}

export const createIsomorphicClient = (opts: GraphqlOptions) => {
    const exchanges = [
        dedupExchange,
        cacheExchange,
        errorExchange({
            onError: error => {
                if (
                    error.graphQLErrors.some(e => e.message === 'Unauthorized') &&
                    opts.onUnauthorized
                ) {
                    opts.onUnauthorized(error)
                } else if (opts.onError) {
                    opts.onError(error)
                }
            }
        }),
    ];

    if (opts.subscriptions && process.env.CLIENT) {
        const {createClient: createWSClient} = require('graphql-ws');
        const {subscriptionExchange} = require('@urql/vue');
        const wsClient = createWSClient({
            url: opts.clientOptions.url.replace('http', 'ws'),
            webSocketImpl: ws,
        });

        exchanges.push(
            subscriptionExchange({
                forwardSubscription: (operation: any) => ({
                    subscribe: (sink: any) => ({
                        unsubscribe: wsClient.subscribe(operation, sink),
                    }),
                }),
            }),
        );
    }

    if (opts.ssr) {
        const ssr = ssrExchange({
            //@ts-ignore
            isClient: process.env.CLIENT === true,
            initialState: !process.env.SERVER ? deserializeStore() : undefined
        });

        exchanges.push(ssr);

        if (opts.ssrContext) {
            opts.ssrContext?.onRendered(() => {
                opts.ssrContext.graphData = serializeStore(ssr.extractData());
            })
        }
    }

    exchanges.push(fetchExchange);

    opts.app.use(urql, {
        exchanges,
        ...opts.clientOptions,
    });
}

const dataKey = '__GRAPH_DATA__';

declare global {
    interface window {
        [dataKey]: string | unknown; // SSRData | undefined;
    }
}

// @ts-ignore
declare module '@quasar/app-webpack/types/ssr' {
    interface QSsrContext {
        graphData: string;
    }
}

function serializeStore(data: SSRData): string {
    return '"' + btoa(JSON.stringify(data)) + '"';
}

function deserializeStore(): SSRData | undefined {
    if (process.env.CLIENT) {
        // @ts-ignore
        if (typeof window !== "undefined" && window[dataKey]) {
            // @ts-ignore
            return JSON.parse(atob(window[dataKey] as string));
        } else {
            return undefined;
        }
    }

    return undefined;
}

boot file

import {boot} from 'quasar/wrappers'
import {createIsomorphicClient} from 'frontend-graphql-client';

export default boot(({app, ssrContext}) => {
  createIsomorphicClient({
    app,
    ssr: true,
    subscriptions: true,
    ssrContext,
    clientOptions: {
      url: 'http://localhost:8080/graphql'
    }
  });
});

from nestjs-query.

Diluka avatar Diluka commented on May 28, 2024

OFFSET strategy already has totalCount it can be closed @TriPSs

and this is follow the suggestion https://graphql.org/learn/pagination/#pagination-and-edges

and this issue when it was first proposed there is no OFFSET strategy and the early OFFSET strategy has no totalCount

from nestjs-query.

smolinari avatar smolinari commented on May 28, 2024

@Goopil - Awesome. I didn't expect to get that. I'm bookmarking for the example to use in the future, as I have to first get going with a working nestjs-query-typgoose package and then some other things, but I'll be working on pagination too at some point. So thanks. 😁

Scott

from nestjs-query.

Goopil avatar Goopil commented on May 28, 2024

Well it's no big deal. We are not all obligated to struggle and figure out the same thing. I wouldn't be the dev I'm today without the great work of others. It's a normal thing to share back some knowledge.

Hope it helps you get started.

Regarding this issue. I thing, all we need is already included right ? Might as well close it.

from nestjs-query.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.