Skip to content

Commit

Permalink
docs: Add validation (#1357)
Browse files Browse the repository at this point in the history
  • Loading branch information
ntucker committed Oct 10, 2021
1 parent a58541f commit 5f479db
Show file tree
Hide file tree
Showing 8 changed files with 199 additions and 50 deletions.
183 changes: 164 additions & 19 deletions docs/getting-started/validation.md
Expand Up @@ -4,24 +4,147 @@ title: Validation

import HooksPlayground from '@site/src/components/HooksPlayground';

[Entity.validate()](../api/Entity#validate) is called during normalization and denormalization.
`undefined` indicates no error, and a string error message if there is an error.

## Field check

Validation happens after [Entity.process()](../api/Entity#process) but before [Entity.fromJS()](../api/Entity#fromJS),
thus operates on POJOs rather than an instance of the class.

Here we can make sure the title field is included, and of the expected type.

<HooksPlayground>

```tsx
class Article extends Entity {
readonly id: string = '';
readonly title: string = '';

pk() {
return this.id;
}

static validate(processedEntity) {
if (!Object.hasOwn(processedEntity, 'title')) return 'missing title field';
if (typeof processedEntity.title !== 'string') return 'title is wrong type';
}
}

const mockArticleDetail = mockFetch(
({ id }) =>
({
'1': { id: '1', title: 'first' },
'2': { id: '2' },
'3': { id: '3', title: { complex: 'second', object: 5 } },
}[id]),
'mockArticleDetail',
);
const articleDetail = new Endpoint(mockArticleDetail, {
schema: Article,
});

function ArticlePage({ id }: { id: string }) {
const article = useResource(articleDetail, { id });
return <div>{article.title}</div>;
}

render(<ArticlePage id="2" />);
```

</HooksPlayground>

### All fields check

Here's a recipe for checking that every defined field is present.

<HooksPlayground>

```tsx
class Article extends Entity {
readonly id: string = '';
readonly title: string = '';

pk() {
return this.id;
}

static validate(processedEntity) {
if (
!Object.keys(this.defaults).every(key => Object.hasOwn(processedEntity, key))
)
return 'a field is missing';
}
}

const mockArticleDetail = mockFetch(
({ id }) =>
({
'1': { id: '1', title: 'first' },
'2': { id: '2' },
}[id]),
'mockArticleDetail',
);
const articleDetail = new Endpoint(mockArticleDetail, {
schema: Article,
});

function ArticlePage({ id }: { id: string }) {
const article = useResource(articleDetail, { id });
return <div>{article.title}</div>;
}

render(<ArticlePage id="2" />);
```

</HooksPlayground>

<!---
## Partial results
Another great use of validation is mixing endpoints that return incomplete objects. This is often
useful when some fields consume lots of bandwidth or are computationally expensive for the backend.
<HooksPlayground>
```tsx
const mockArticleList = mockFetch(
() => [
{ id: '1', title: 'first' },
{ id: '2', title: 'second' },
],
'mockArticleList',
);
const mockArticleDetail = mockFetch(
({ id }) =>
({
'1': {
id: '1',
title: 'first',
content: 'long',
createdAt: '2011-10-05T14:48:00.000Z',
},
'2': {
id: '2',
title: 'second',
content: 'short',
createdAt: '2011-10-05T14:48:00.000Z',
},
}[id]),
'mockArticleDetail',
);
class ArticlePreview extends Entity {
readonly id: string = '';
readonly title: string = '';
pk() {
return this.id;
}
static key() {
static get key() {
return 'Article';
}
}
const mockArticleList = mockFetch(() => [
{ id: '1', title: 'first' },
{ id: '2', title: 'second' },
]);
const articleList = new Endpoint(mockArticleList, { schema: [ArticlePreview] });
class ArticleFull extends ArticlePreview {
Expand All @@ -36,26 +159,48 @@ class ArticleFull extends ArticlePreview {
if (!Object.hasOwn(processedEntity, 'content')) return 'Missing content';
}
}
const mockArticleDetail = mockFetch(
({ id }) =>
({
'1': { id: '1', title: 'first', content: 'long' },
'2': { id: '2', title: 'second', content: 'short' },
}[id]),
);
const articleDetail = new Endpoint(mockArticleDetail, {
schema: ArticleFull,
key({ id }) {
return `article ${id}`;
},
});
function ArticlePage() {
const article = useResource(articleDetail, { id: '1' });
return <div>{article.title}</div>;
function ArticleDetail({ id }: { id: string }) {
const article = useResource(articleDetail, { id });
return (
<div>
<h4>{article.title}</h4>
<div>
<p>{article.content}</p>
<div>
Created:{' '}
<time>
{Intl.DateTimeFormat('en-US', { dateStyle: 'medium' }).format(
article.createdAt,
)}
</time>
</div>
</div>
</div>
);
}
function ArticleList() {
const [route, setRoute] = React.useState<string>();
const articles = useResource(articleList, {});
if (!route) {
return (
<div>
{articles.map(article => (
<div key={article.pk()} onClick={() => setRoute(article.id)}>
{article.title}
</div>
))}
</div>
);
}
return <ArticleDetail id={route} />;
}
render(<ArticlePage />);
render(<ArticleList />);
```
</HooksPlayground>
-->
24 changes: 0 additions & 24 deletions docs/guides/README.md

This file was deleted.

4 changes: 4 additions & 0 deletions website/docusaurus.config.js
Expand Up @@ -187,6 +187,10 @@ module.exports = {
to: 'docs',
position: 'right',
items: [
{
label: 'Upgrade Guide',
to: 'docs/upgrade/upgrading-to-6',
},
{
label: '6.1',
to: 'docs/',
Expand Down
4 changes: 4 additions & 0 deletions website/sidebars.json
Expand Up @@ -28,6 +28,10 @@
{
"type": "doc",
"id": "getting-started/expiry-policy"
},
{
"type": "doc",
"id": "getting-started/validation"
}
]
},
Expand Down
9 changes: 6 additions & 3 deletions website/src/components/HooksPlayground.js
Expand Up @@ -7,12 +7,15 @@ import { default as BaseTodoResource } from 'todo-app/src/resources/TodoResource

import Playground from './Playground';

const mockFetch =
(getResponse, delay = 150) =>
(...args) =>
const mockFetch = (getResponse, name, delay = 150) => {
const fetch = (...args) =>
new Promise(resolve =>
setTimeout(() => resolve(getResponse(...args)), delay),
);
if (name)
Object.defineProperty(fetch, 'name', { value: name, writable: false });
return fetch;
};

const mockLastUpdated = ({ id, delay = 150 }) =>
new Promise(resolve =>
Expand Down
16 changes: 12 additions & 4 deletions website/src/components/Playground/index.js
Expand Up @@ -21,8 +21,8 @@ import styles from './styles.module.css';
const babelTransform = code => {
const transformed = ts.transpileModule(code, {
compilerOptions: {
module: ts.ModuleKind.CommonJS,
target: ts.ScriptTarget.ES2020,
module: ts.ModuleKind.ESNext,
target: ts.ScriptTarget.ES2017,
jsx: 'react',
},
});
Expand Down Expand Up @@ -141,8 +141,16 @@ export default function Playground({
code={isBrowser ? children.replace(/\n$/, '') : ''}
transformCode={transformCode || (code => babelTransform(`${code};`))}
transpileOptions={{
target: { chrome: 60 },
transforms: { classes: false, letConst: false },
target: { chrome: 71 },
transforms: {
classes: false,
letConst: false,
getterSetter: false,
generator: false,
asyncAwait: false,
moduleImport: false,
moduleExport: false,
},
}}
theme={prismTheme}
{...props}
Expand Down
8 changes: 8 additions & 0 deletions website/src/components/Playground/styles.module.css
Expand Up @@ -11,6 +11,9 @@
.playgroundContainer.row {
flex-direction: row;
}
.playgroundContainer.row .hidden {
display: none;
}
}
.playgroundContainer > div {
flex: 1 1 auto;
Expand Down Expand Up @@ -46,8 +49,13 @@ div:first-of-type > .playgroundHeader {
padding: 1rem;
background-color: var(--ifm-pre-background);
flex: 1 4 20%;
overflow: auto;
}
:global(.col) .playgroundPreview pre {
overflow: visible;
}


.playgroundResult {
display: flex;
height: 100%;
Expand Down
1 change: 1 addition & 0 deletions website/src/pages/index.js
Expand Up @@ -24,6 +24,7 @@ const ProjectTitle = () => {
src={'img/rest_hooks_logo_and_text_subtitle.svg'}
alt="Rest Hooks - An API client for dynamic applications"
height={110}
width={512}
/>
</div>

Expand Down

0 comments on commit 5f479db

Please sign in to comment.