diff --git a/rest/pagination/README.md b/rest/pagination/README.md index 91cf574..79269df 100644 --- a/rest/pagination/README.md +++ b/rest/pagination/README.md @@ -60,7 +60,27 @@ The field `Query.customersPageNumber` is an example of `PAGE_NUMBER` pagination. ## OFFSET pagination -TODO +A REST API using the OFFSET style pagination typically takes two arguments, limit and offset, +and returns a list of values and metadata indicating the total number of records. The response layout +varies, but is similar to: + +```json +{ + "meta": { + "records": 8 + }, + "values": [ + { + "name": "Bob" + }, + { + "name": "Charlie" + } + ] +} +``` + +The field `Query.customersOffset` is an example of `OFFSET` pagination. ## NEXT_CURSOR pagination diff --git a/rest/pagination/api.graphql b/rest/pagination/api.graphql index 4879679..21b90d8 100644 --- a/rest/pagination/api.graphql +++ b/rest/pagination/api.graphql @@ -117,4 +117,82 @@ type Query { } """ ) + + """ + `customersOffset` pages through twenty three generated `Customer` objects. + The data is supplied by a simulated REST API using offset pagination + but is exposed using standard GraphQL pagination. + + Typically the `endpoint` argument of `@rest` would have query + parameters that set the pagination arguments expected by the + REST API from the field arguments `first` and `after` (offset defaults to 0 if empty), + for example: `?limt=$first&offset=$after`. + """ + customersOffset(first: Int!, after: String = ""): CustomerConnection + @rest( + # pagination sets the type of pagination the REST API uses + # and for OFFSET requires a setter that declares + # where the total number of records are in the JSON response. + pagination: { + type: OFFSET + setters: [{ field: "total", path: "meta.records" }] + } + + # resultroot indicates where root in the JSON response + # for the values that will populate the nodes. + # Note this does not affect pagination setters, they + # are always relative to the root of the response. + resultroot: "values[]" + + # Ecmascript (with empty endpoint) is used to mimic the response from a REST api. + # Note ECMAScript is only used to generate a mock response with customer objects and page number metadata, + # using @rest against a real endpoint would not typically require any ECMAScript. + endpoint: "stepzen:empty" + ecmascript: """ + function transformREST() { + // A total of 23 items will be returned + const totalItems = 23; + + // Pagination field arguments + // Since this is OFFSET pagination + // "after" is decoded by StepZen from the opaque string value + // and passed into @rest as a integer offset value, + // with the offset of the first record being zero. + const limit = get('first'); + const offset = get('after'); + + // metadata - total number of records + const records = Math.ceil(totalItems / limit) + + // generate customers for nodes based on the limit and offset values + const startIndex = offset+1 || 1; + const endIndex = Math.min(startIndex + limit, totalItems+1); + var customers = [] + for (let i = startIndex; i < endIndex; i++) { + customers.push({ + id: i, + name: 'name-' + i, + email: 'user-' + i + '@example.com' + }); + } + + // This returns a typical layout of a REST response + // when pagination is through an offset. + // @rest must be configured to match the REST response layout. + // + // pagination setters defines that the page count + // is taken from meta.records + // + // resultroot corresponds to the location that contains the + // data values. Note the REST API returns the customer objects, + // StepZen automatically creates the connection/edges structure + // for the values. + return ({ + meta: { 'records': records }, + values: customers + } + ); + } + """ + ) } diff --git a/rest/pagination/requests.graphql b/rest/pagination/requests.graphql index be3b596..abdc61a 100644 --- a/rest/pagination/requests.graphql +++ b/rest/pagination/requests.graphql @@ -15,6 +15,23 @@ query CustomersPageNumber($first: Int!, $after: String) { } } +# Page through customers from a REST API using page numbers +query CustomersOffset($first: Int!, $after: String) { + customersOffset(first: $first, after: $after) { + edges { + node { + id + name + email + } + } + pageInfo { + endCursor + hasNextPage + } + } +} + # Return the first customer (using a reshape version of a paginated field) query FirstCustomer { firstCustomer { diff --git a/rest/pagination/tests/Test.js b/rest/pagination/tests/Test.js index 862826c..3adb59e 100644 --- a/rest/pagination/tests/Test.js +++ b/rest/pagination/tests/Test.js @@ -100,6 +100,61 @@ describe(testDescription, function () { nCustomers: generateNodes(1, 5).map(edge => edge.node), }, }, + { + label: "customersOffset-1-10", + query: requests, + operationName: 'CustomersOffset', + variables: { first: 10 }, + expected: { + customersOffset: { + edges: generateNodes(1, 10), + pageInfo: { + endCursor: + "eyJjIjoiTzpRdWVyeTpjdXN0b21lcnNPZmZzZXQiLCJvIjo5fQ==", + hasNextPage: false, + }, + }, + }, + }, + { + label: "customersOffset-10-20", + query: requests, + operationName: 'CustomersOffset', + variables: { + first: 10, + after: "eyJjIjoiTzpRdWVyeTpjdXN0b21lcnNPZmZzZXQiLCJvIjo5fQ==" + }, + expected: { + customersOffset: { + edges: generateNodes(11, 20), + pageInfo: { + endCursor: + "eyJjIjoiTzpRdWVyeTpjdXN0b21lcnNPZmZzZXQiLCJvIjoxOX0=", + hasNextPage: false, + }, + }, + }, + }, + { + label: "customersOffset-21-23", + query: requests, + operationName: 'CustomersOffset', + variables: { + first: 10, + after: + "eyJjIjoiTzpRdWVyeTpjdXN0b21lcnNPZmZzZXQiLCJvIjoxOX0=", + }, + expected: { + customersOffset: { + edges: generateNodes(21, 23), + pageInfo: { + endCursor: + "eyJjIjoiTzpRdWVyeTpjdXN0b21lcnNPZmZzZXQiLCJvIjoyMn0=", + hasNextPage: false, + }, + }, + }, + }, ]; return deployAndRun(__dirname, tests, stepzen.admin); });