Skip to content

Commit 2e7842d

Browse files
author
Julien Neuhart
committed
WIP lists guide
1 parent 0dd764c commit 2e7842d

File tree

1 file changed

+258
-51
lines changed

1 file changed

+258
-51
lines changed

docs/docs/5_Guides/5_5_Lists.md

Lines changed: 258 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -5,60 +5,24 @@ slug: /guides/lists
55

66
## API
77

8-
### Use case and result iterator
8+
In the API, a list means retrieving data from the database according to:
99

10-
It starts by creating a use case. For instance, *src/api/src/UseCase/User/GetUsers.php*.
10+
* Random filters (e.g., free search).
11+
* Predictable filters (e.g., dropdown values).
12+
* A sort direction on a field.
13+
* A limit and an offset.
1114

12-
Your use case have to return a `ResultIterator` to paginate efficiently your results.
13-
Let's take a look at the GraphQL query for the `GetUsers` use case:
14-
15-
```graphql title="GraphQL query"
16-
query users($search: String, $role: Role, $sortBy: UsersSortBy, $sortOrder: SortOrder, $limit: Int!, $offset: Int!) {
17-
users(search: $search, role: $role, sortBy: $sortBy, sortOrder: $sortOrder) {
18-
items(limit: $limit, offset: $offset) {
19-
id
20-
firstName
21-
lastName
22-
email
23-
locale
24-
role
25-
}
26-
count
27-
}
28-
}
29-
```
30-
31-
The `limit` and `offset` parameters from the `items` part do not exist in the use case parameters;
32-
it is the `ResultIterator` that takes these parameters.
33-
34-
Also, note the `count` value; that's the total of items.
35-
36-
We will see later how to use these values to create a pagination on the web application.
15+
There are two main PHP classes for that goal:
3716

38-
### Enumerators
39-
40-
For "sort by" and "sort order" values, we use enumerators (see *src/api/src/Domain/Enum/Filter* folder).
17+
1. The use case (and GraphQL entrypoint).
18+
2. The DAO.
4119

42-
GraphQLite recognizes an enumerator as a valid value for your GraphQL request.
43-
44-
Each enumerator's key (i.e., `FIRST_NAME`) is a GraphQL value, while each enumerator's value (i.e., `first_name`)
45-
is a valid SQL expression.
20+
Both [TDBM](https://github.com/thecodingmachine/tdbm) and [GraphQLite](https://graphqlite.thecodingmachine.io/)
21+
will also help us a lot.
4622

47-
For instance:
23+
### Use case
4824

49-
```graphql title="GraphQL query"
50-
users(search: "foo", role: ADMINISTATOR, sortBy: LAST_NAME, sortOrder: DESC) {
51-
items(limit: 10, offset: 0) {
52-
id
53-
firstName
54-
lastName
55-
email
56-
locale
57-
role
58-
}
59-
count
60-
}
61-
```
25+
Let's start by the use case. For instance, *src/api/src/UseCase/User/GetUsers.php*:
6226

6327
```php title="src/api/src/UseCase/User/GetUsers.php"
6428
/**
@@ -83,6 +47,86 @@ public function users(
8347
}
8448
```
8549

50+
If we split this use case, we have:
51+
52+
**The annotations**
53+
54+
* `@return`: it contains both an array of your type AND a `ResultIterator`. It will make both PHPStan and GraphLite happy!
55+
* `@Query`: it equivalent to a REST GET method.
56+
* `@Logged` and `@Security`: access control.
57+
58+
**The arguments**
59+
60+
* `$search`: a random filter.
61+
* `$role`: a predictable filter.
62+
* `$sortBy`: the sort field.
63+
* `$sortOrder`: the sort direction.
64+
65+
**The return value:** `ResultIterator`.
66+
67+
**The logic:** `UserDao`'s `search` method.
68+
69+
:::note
70+
71+
📣  The predictable filter, sort field and sort direction are enumerators.
72+
73+
:::
74+
75+
:::note
76+
77+
📣  There are no `$limit` or `$offset` arguments.
78+
79+
:::
80+
81+
Let's take a look at the GraphQL query for the `GetUsers` use case:
82+
83+
```graphql title="GraphQL query"
84+
query users($search: String, $role: Role, $sortBy: UsersSortBy, $sortOrder: SortOrder, $limit: Int!, $offset: Int!) {
85+
users(search: $search, role: $role, sortBy: $sortBy, sortOrder: $sortOrder) {
86+
items(limit: $limit, offset: $offset) {
87+
id
88+
firstName
89+
lastName
90+
email
91+
locale
92+
role
93+
}
94+
count
95+
}
96+
}
97+
```
98+
99+
Here we can see that:
100+
101+
1. The enumerators are valid GraphQL types.
102+
2. The `items` property with the `limit` and `offset` arguments plus the `count` property come from the `ResultIterator`.
103+
104+
### Enumerators
105+
106+
The folder *src/api/src/Domain/Enum* contains our enumerators.
107+
108+
Each enumerator's key (i.e., `FIRST_NAME`) is a GraphQL value, while each enumerator's value (i.e., `first_name`)
109+
is a valid SQL expression.
110+
111+
:::note
112+
113+
📣  The key is for your GraphQL query; it not valid, [GraphQLite](https://graphqlite.thecodingmachine.io/)
114+
throws an exception.
115+
116+
:::
117+
118+
:::note
119+
120+
📣  The value is for creating SQL statements like the sort clause.
121+
122+
:::
123+
124+
### Result iterator
125+
126+
In the API, you will not manage the limit and offset of your data. That's the role of the `ResultIterator`.
127+
128+
### DAO
129+
86130
```php title="src/api/src/Domain/Dao/UserDao.php"
87131
/**
88132
* @return User[]|ResultIterator
@@ -110,9 +154,172 @@ public function search(
110154
}
111155
```
112156

157+
The goal of the DAO is to:
158+
159+
* Initialize the sort values.
160+
* Build the SQL query.
161+
* Retrieve the data from the database.
162+
163+
:::note
164+
165+
📣  Reminder: an enumerator's value is for creating SQL statements like the sort clause.
166+
167+
:::
168+
113169
## Web application
114170

115-
> IF default TEXT filters === null, it will refresh the page the first time!!
171+
In the API, a list means displaying data according to user's inputs.
172+
173+
### List mixin
174+
175+
The boilerplate provides the *src/webapp/mixins/list.js* mixin.
176+
177+
:::note
178+
179+
📣  A mixin content merges with the content of your Vue component.
180+
181+
:::
182+
183+
This mixin contains useful data and methods to help you build a list.
184+
185+
### Initialization
186+
187+
The initialization of your page occurs in the `asyncData` attribute of your Vue Component:
188+
189+
```vue title="src/webapp/pages/admin/users/index.vue"
190+
async asyncData(context) {
191+
try {
192+
const result = await context.app.$graphql.request(UsersQuery, {
193+
search: context.route.query.search || '',
194+
role: context.route.query.role || null,
195+
sortBy: context.route.query.sortBy || null,
196+
sortOrder: context.route.query.sortOrder || null,
197+
limit: defaultItemsPerPage,
198+
offset: calculateOffset(
199+
context.route.query.page || 1,
200+
defaultItemsPerPage
201+
),
202+
})
203+
204+
return {
205+
items: result.users.items,
206+
count: result.users.count,
207+
}
208+
} catch (e) {
209+
context.error(e)
210+
}
211+
},
212+
```
213+
214+
:::note
215+
216+
📣  On page access, Nuxt.js always renders a Vue component with an `asyncData` attribute on the server; you don't have access to
217+
`this` but `context` instead.
218+
219+
:::
220+
221+
:::note
222+
223+
📣  `asyncData` will merge with the `data` of your Vue component.
224+
225+
:::
226+
227+
First, we do a GraphQL query to retrieve our data.
228+
229+
The parameters' values may come from query parameters (i.e., `?foo=bar,baz=2`) or use a default value.
230+
231+
:::note
232+
233+
📣  Don't use `null` as value for your scalar parameters as it will make your page reload. Prefer,
234+
for instance, an empty string.
235+
236+
:::
237+
238+
The `limit` parameter uses the `defaultItemsPerPage` constant from the list mixin, but you may define a custom constant
239+
in your Vue component.
240+
241+
The `offset` parameter uses the `calculateOffset` method from the list mixin.
242+
243+
Once the GraphQL query finishes, there are two possible outcomes:
244+
245+
1. No error: your fills the `items` and `count` data from the list mixin.
246+
2. An error occurs (access control mostly): you catch it and provide it to the `context`
247+
(see our [Security](/docs/guides/security) guide for more details).
248+
249+
:::note
250+
251+
📣&nbsp;&nbsp;You may display the data in your `<template>` block thanks to the `items`.
252+
253+
:::
254+
255+
### Filters
256+
257+
```vue title=""
258+
data() {
259+
return {
260+
filters: {
261+
search: this.$route.query.search || '',
262+
role: this.$route.query.role || null,
263+
},
264+
fields: [
265+
{ key: 'id', label: '#', sortable: false },
266+
{
267+
key: 'firstName',
268+
label: this.$t('common.first_name'),
269+
sortable: true,
270+
},
271+
{ key: 'lastName', label: this.$t('common.last_name'), sortable: true },
272+
{ key: 'email', label: this.$t('common.email'), sortable: true },
273+
{ key: 'locale', label: this.$t('common.locale'), sortable: false },
274+
{ key: 'role', label: this.$t('common.role'), sortable: false },
275+
],
276+
sortByMap: {
277+
firstName: FIRST_NAME,
278+
lastName: LAST_NAME,
279+
email: EMAIL,
280+
},
281+
}
282+
},
283+
```
284+
285+
### Fields / Headers
286+
287+
If you are using a Bootstrap Vue's `<b-table>`, you also need to define the `fields` (i.e., the headers).
288+
289+
You do that in the `data` attribute of your Vue component:
290+
291+
```vue title=""
292+
data() {
293+
return {
294+
fields: [
295+
{ key: 'id', label: '#', sortable: false },
296+
{
297+
key: 'firstName',
298+
label: this.$t('common.first_name'),
299+
sortable: true,
300+
},
301+
{ key: 'lastName', label: this.$t('common.last_name'), sortable: true },
302+
{ key: 'email', label: this.$t('common.email'), sortable: true },
303+
{ key: 'locale', label: this.$t('common.locale'), sortable: false },
304+
{ key: 'role', label: this.$t('common.role'), sortable: false },
305+
],
306+
}
307+
},
308+
```
309+
310+
```vue title="Bootstrap table"
311+
<b-table
312+
:items="items"
313+
:fields="fields"
314+
></b-table>
315+
```
316+
317+
### Search
318+
319+
### Sort
320+
321+
### Paginate
322+
323+
### Full example
116324

117-
> CLIENT need to try catch and call this.$nuxt.error
118-
> SERVER need to try catch and call context.error
325+
See *src/webapp/pages/admin/users/index.vue*.

0 commit comments

Comments
 (0)