Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Core Data: TypeScript definitions for entity records. #38666

Merged
merged 74 commits into from
Feb 22, 2022
Merged
Show file tree
Hide file tree
Changes from 68 commits
Commits
Show all changes
74 commits
Select commit Hold shift + click to select a range
8d9ab6d
Add types for core data entity records
adamziel Feb 9, 2022
190ccdf
Use string enums instead of numeric ones
adamziel Feb 9, 2022
6d9687b
Put each type in a separate file
adamziel Feb 10, 2022
de331e0
Rond 2 of autogenerating types
adamziel Feb 10, 2022
1300f77
Extract enum types
adamziel Feb 10, 2022
60d1c91
Extract common interfaces
adamziel Feb 10, 2022
9cb67ca
Add type definitions for posts and template parts
adamziel Feb 10, 2022
08081f7
Add Raw Data typings
adamziel Feb 10, 2022
cd9b910
Extract AvatarUrls to common.js
adamziel Feb 10, 2022
2449089
Try context-based entity types
adamziel Feb 10, 2022
5f35431
Experimenting with different ways of contextualizing data types
adamziel Feb 11, 2022
566c954
Remove EntityInContext – it isn't really needed
adamziel Feb 11, 2022
5969c12
Rename RawDataIsString to RawDataOverride
adamziel Feb 11, 2022
4d8ea95
Use Entity and EntityWithEdits without distinguishing between differe…
adamziel Feb 15, 2022
4ff84bb
Make all the fields contextual
adamziel Feb 16, 2022
b73646b
Refactir WithEdits to EditedRecord
adamziel Feb 16, 2022
a20c2a1
Rename "Edited" to "Updatable"
adamziel Feb 17, 2022
539f7c2
Flatten Nevers and WithoutNevers to OmitNevers
adamziel Feb 17, 2022
1c420c0
Remove dedicated updatable fields in favor of an Updateble type wrapper
adamziel Feb 17, 2022
942f152
Remove extra wrappers around types
adamziel Feb 17, 2022
f13eda1
Add missing definitions to NavMenu type
adamziel Feb 17, 2022
433be4f
Use RawField in Page and Post types
adamziel Feb 17, 2022
62963d9
Export UpdatableRecord type
adamziel Feb 17, 2022
6554342
Rename RawField to RenderedText
adamziel Feb 17, 2022
ec99125
Export updatable types
adamziel Feb 17, 2022
4292622
Rename UpdatableRecord to Updatable
adamziel Feb 17, 2022
dde6773
Remove atomic updatable types
adamziel Feb 18, 2022
e9ee4d0
Adjust Comment status and User locale modeling
adamziel Feb 18, 2022
dcdabe5
Remove the NestedWidget name, declare the type inline in Sidebar
adamziel Feb 18, 2022
31e1ea9
Make User.password optional
adamziel Feb 18, 2022
d81c259
Introduce StringWhenUpdatable type to model WP templates
adamziel Feb 18, 2022
e6193b2
Document the Updatable type
adamziel Feb 18, 2022
a95ba80
Document the RenderedText using @dmsnell's proposal
adamziel Feb 18, 2022
0e4c63e
Document types in common.ts
adamziel Feb 18, 2022
09fe23a
Flatten the content prop of WpTemplate and WpTemplatePart into a stri…
adamziel Feb 18, 2022
4974607
Type the remaining optional fields more strictly
adamziel Feb 18, 2022
c086bab
Wrap Type with OmitNevers
adamziel Feb 18, 2022
b4b0dd7
Wrap the type user with OmitNevers
adamziel Feb 18, 2022
f153a3a
Use consistent kebab case in type files
adamziel Feb 18, 2022
ee55c45
Wrap comment with OmitNevers
adamziel Feb 18, 2022
9ba701f
Add the missing OmitNevers to types with contextual fields
adamziel Feb 18, 2022
b5560c1
Add README.md
adamziel Feb 18, 2022
8bd4641
Rename common.ts to helpers.ts
adamziel Feb 18, 2022
184bbe4
Model WpTemplate.content and WpTemplatePart.content as RenderableText
adamziel Feb 21, 2022
185ecfd
Link to the REST API docs when explaining contexts
adamziel Feb 21, 2022
0486076
Explain the ContextualFields without meandering on the implementation…
adamziel Feb 21, 2022
493050e
Use the correct capitalization of the word javascript
adamziel Feb 21, 2022
222e2fa
Rename CommentStatus to CommentingStatus in context of comments conta…
adamziel Feb 21, 2022
fccae67
Use Post as an example illustrating the usage of ContextualField
adamziel Feb 21, 2022
d8522e8
Focus on the goal of the ContextualField in its documentation
adamziel Feb 21, 2022
3fe2297
Use a cleaner explanation of the Updatable type wrapper
adamziel Feb 21, 2022
c544fef
Explain why the types do not provide full type safety
adamziel Feb 21, 2022
1b7963e
Remove the TODO comments from the README and a section about extendab…
adamziel Feb 21, 2022
b855db9
Add extensible type prefix
adamziel Feb 21, 2022
5b2145d
a -> an
adamziel Feb 22, 2022
b23a08a
Use export type and import type declarations
adamziel Feb 22, 2022
c98911e
Note that Comment.id is still a field even after the Comment type has…
adamziel Feb 22, 2022
c02ddb3
Use CommentingStatus and PingStatus in the Attachment type
adamziel Feb 22, 2022
a0650ac
Use a correct snippet of code to illustrate interface extending in RE…
adamziel Feb 22, 2022
fec0432
Use namespaced base types for extenders
adamziel Feb 22, 2022
df1080c
Document the BaseTypes namespace
adamziel Feb 22, 2022
d1c4dfb
Lint
adamziel Feb 22, 2022
97bc553
Lint the docstring in base-types.ts
adamziel Feb 22, 2022
80b7610
Rename the BaseTypes namespace to WPBaseTypes
adamziel Feb 22, 2022
db67911
Add more commentary to the Extending section
adamziel Feb 22, 2022
4509bd2
Add a comma
adamziel Feb 22, 2022
466dd5f
Clarify the warning about type safety
adamziel Feb 22, 2022
a84b420
Restore the first sentence of the warning
adamziel Feb 22, 2022
0ab8ed7
Link to the types readme in core-data readme
adamziel Feb 22, 2022
2ce3ce0
Merge branch 'trunk' into ts/add-core-data-types
adamziel Feb 22, 2022
dde0888
Do not publish the data types
adamziel Feb 22, 2022
669f289
Rename WPBaseTypes to CoreBaseEntityTypes
adamziel Feb 22, 2022
adfd612
Rename CoreBaseEntityTypes to BaseEntityTypes
adamziel Feb 22, 2022
0fbd96f
Fix a typo (extends -> extend)
adamziel Feb 22, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/core-data/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"main": "build/index.js",
"module": "build-module/index.js",
"react-native": "src/index",
"types": "src/types/index.ts",
adamziel marked this conversation as resolved.
Show resolved Hide resolved
"sideEffects": [
"{src,build,build-module}/index.js"
],
Expand Down
1 change: 1 addition & 0 deletions packages/core-data/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,3 +76,4 @@ register( store );
export { default as EntityProvider } from './entity-provider';
export * from './entity-provider';
export * from './fetch';
export * from './types';
adamziel marked this conversation as resolved.
Show resolved Hide resolved
238 changes: 238 additions & 0 deletions packages/core-data/src/types/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
# Entity Records Types
adamziel marked this conversation as resolved.
Show resolved Hide resolved

## Overview

The types in this directory are designed to support the following use-cases:

* Provide type-hinting and documentation for entity records fetched in the various REST API contexts.
* Type-check the values we use to *edit* entity records, the values that are sent back to the server as updates.

**Warning:** The types model the expected API responses which is **not** the same as having a full type safety for the API-related operations. The API responses are type-cast to these definitions and therefore may not match those expectations; for example, a plugin could modify the response, or the API endpoint could have a nuanced implementation in which strings are sometimes used instead of numbers.

### Context-aware type checks for entity records

WordPress REST API returns different responses based on the `context` query parameter, which typically is one of `view`, `edit`, or `embed`. See the [REST API documentation](https://developer.wordpress.org/rest-api/) to learn more.

For example, requesting `/wp/v2/posts/1?context=view` yields:

```js
{
"content": {
"protected": false,
"rendered": "\n<p>Welcome to WordPress. This is your first post. Edit or delete it, then start writing!</p>\n"
},
"title": {
"rendered": "Hello world!"
}
// other fields
}
```

While requesting `/wp/v2/posts/1?context=edit`, yields:

```js
{
"content": {
"block_version": 1,
"protected": false,
"raw": "<!-- wp:paragraph -->\n<p>Welcome to WordPress. This is your first post. Edit or delete it, then start writing!</p>\n<!-- /wp:paragraph -->",
"rendered": "\n<p>Welcome to WordPress. This is your first post. Edit or delete it, then start writing!</p>\n"
},
"title": {
"raw": "Hello world!",
"rendered": "Hello world!"
}
// other fields
}
```

And, finally, requesting `/wp/v2/posts/1?context=embed` yields:

```js
{
// Note content is missing
"title": {
"rendered": "Hello world!"
}
// other fields
}
```

These contexts are supported by the core-data resolvers like `getEntityRecord()` and `getEntityRecords()` to retrieve the appropriate "flavor" of the data.

The types describing different entity records must thus be aware of the relevant API context. This is implemented using the `Context` type parameter. For example, the implementation of the `Post` type resembles the following snippet:

```ts
interface Post<C extends Context> {
/**
* A named status for the post.
*/
status: ContextualField< PostStatus, 'view' | 'edit', C >;

// ... other fields ...
}
```

The `status` field is a `PostStatus` when the requesting context is `view` or `edit`, but if requested with an `embed` context the field won't appear on the `Post` object at all.

### Static type checks for *edited* entity records, where certain fields become strings instead of objects.

When the `post` is retrieved using `getEntityRecord`, its `content` field is an object:

```js
const post = wp.data.select('core').getEntityRecord( 'postType', 'post', 1, { context: 'view' } )
// `post.content` is an object with two fields: protected and rendered
```

The block markup stored in `content` can only be rendered on the server so the REST API exposes both the raw markup and the rendered version. For example, `content.rendered` could used as a visual preview, and `content.raw` could be used to populate the code editor.
adamziel marked this conversation as resolved.
Show resolved Hide resolved

When updating that field from the JavaScript code, however, all we can set is the raw value that the server will eventually render. The API expects us to send a much simpler `string` form which is the raw form that needs to be stored in the database.

The types reflect this through the `Updatable<EntityRecord>` wrapper:

```ts
interface Post< C extends Context > {
title: {
raw: string;
rendered: string;
}
}

const post : Post< 'edit' > = ...
// post.title is an object with properties `raw` and `rendered`

const post : Updatable<Post< 'edit' >> = ...
// post.title is a string
```

The `getEditedEntityRecord` selector returns the Updatable version of the entity records:

```js
const post = wp.data.select('core').getEditedEntityRecord( 'postType', 'post', 1 );
// `post.content` is a string
```

## Helpers

### Context

The REST API context parameter.

### ContextualField

`ContextualField` makes the field available only in the specified given contexts, and ensure the field is absent from the object when in a different context.

Example:

```ts
interface Post< C extends Context > {
modified: ContextualField< string, 'edit' | 'view', C >;
password: ContextualField< string, 'edit', C >;
}

const post: Post<'edit'> = …
// post.modified exists as a string
// post.password exists as a string

const post: Post<'view'> = …
// post.modified still exists as a string
// post.password is missing, undefined, because we're not in the `edit` context.
```

### OmitNevers

Removes all the properties of type never, even the deeply nested ones.

```ts
type MyType = {
foo: string;
bar: never;
nested: {
foo: string;
bar: never;
}
}
const x = {} as OmitNevers<MyType>;
// x is of type { foo: string; nested: { foo: string; }}
// The `never` properties were removed entirely
```

### Updatable

Updatable<EntityRecord> is a type describing Edited Entity Records. They are like
regular Entity Records, but they have all the local edits applied on top of the REST API data.

This turns certain field from an object into a string.

Entities like Post have fields that only be rendered on the server, like title, excerpt,
and content. The REST API exposes both the raw markup and the rendered version of those fields.
For example, in the block editor, content.rendered could used as a visual preview, and
content.raw could be used to populate the code editor.

When updating these rendered fields, JavaScript is not be able to properly render arbitrary block
markup. Therefore, it stores only the raw markup without the rendered part. And since that's a string,
the entire field becomes a string.

```ts
type Post< C extends Context > {
title: RenderedText< C >;
}
const post = {} as Post;
// post.title is an object with raw and rendered properties

const updatablePost = {} as Updatable< Post >;
// updatablePost.title is a string
```

### RenderedText

A string that the server renders which often involves modifications from the raw source string.

For example, block HTML with the comment delimiters exists in `post_content` but those comments are stripped out when rendering to a page view. Similarly, plugins might modify content or replace shortcodes.

## Extending

You can extend the entity record definitions using [TypeScript's declaration merging](https://www.typescriptlang.org/docs/handbook/declaration-merging.html).

For example, if you're building a plugin that displays a number of views of each comment, you can add a new `numberOfViews` field to the `Comment` type like this:

```ts
// In core-data
export namespace WPBaseTypes {
// This is the parent interface – it can be extended
export interface Comment< C extends Context > {
id: number;
// ...
}
}

// This the child type used in function signatures – it cannot be extended
export type Comment< C extends Context > = OmitNevers<
WPBaseTypes.Comment< C >
>;

// In the plugin
import type { Context } from '@wordpress/core-data';
// Target the core-data module
declare module '@wordpress/core-data' {
// Extend the WPBaseTypes namespace through declaration merging
export namespace WPBaseTypes {
// Extend the parent Commet interface through declaration merging
export interface Comment< C extends Context > {
numberOfViews: number;
}
}
}

import type { Comment } from '@wordpress/core-data';
// Create an instance of the child type
const c : Comment< 'view' > = ...;

// Extending the parent had the desired effect on the child type:
// c.numberOfViews is a number
adamziel marked this conversation as resolved.
Show resolved Hide resolved
// c.id is still present
```

Of course, you will also need to extend the REST API to expose the numberOfViews property.
146 changes: 146 additions & 0 deletions packages/core-data/src/types/attachment.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
/**
* Internal dependencies
*/
import {
Context,
ContextualField,
MediaType,
PostStatus,
RenderedText,
OmitNevers,
CommentingStatus,
PingStatus,
} from './helpers';

import { WPBaseTypes as _WPBaseTypes } from './wp-base-types';

declare module './wp-base-types' {
export namespace WPBaseTypes {
export interface Attachment< C extends Context > {
/**
* The date the post was published, in the site's timezone.
*/
date: string | null;
/**
* The date the post was published, as GMT.
*/
date_gmt: ContextualField< string | null, 'view' | 'edit', C >;
/**
* The globally unique identifier for the post.
*/
guid: ContextualField< RenderedText< C >, 'view' | 'edit', C >;
/**
* Unique identifier for the post.
*/
id: number;
/**
* URL to the post.
*/
link: string;
/**
* The date the post was last modified, in the site's timezone.
*/
modified: ContextualField< string, 'view' | 'edit', C >;
/**
* The date the post was last modified, as GMT.
*/
modified_gmt: ContextualField< string, 'view' | 'edit', C >;
/**
* An alphanumeric identifier for the post unique to its type.
*/
slug: string;
/**
* A named status for the post.
*/
status: ContextualField< PostStatus, 'view' | 'edit', C >;
/**
* Type of post.
*/
type: string;
/**
* Permalink template for the post.
*/
permalink_template: ContextualField< string, 'edit', C >;
/**
* Slug automatically generated from the post title.
*/
generated_slug: ContextualField< string, 'edit', C >;
/**
* The title for the post.
*/
title: RenderedText< C >;
/**
* The ID for the author of the post.
*/
author: number;
/**
* Whether or not comments are open on the post.
*/
comment_status: ContextualField<
CommentingStatus,
'view' | 'edit',
C
>;
/**
* Whether or not the post can be pinged.
*/
ping_status: ContextualField< PingStatus, 'view' | 'edit', C >;
/**
* Meta fields.
*/
meta: ContextualField<
Record< string, string >,
'view' | 'edit',
C
>;
/**
* The theme file to use to display the post.
*/
template: ContextualField< string, 'view' | 'edit', C >;
/**
* Alternative text to display when attachment is not displayed.
*/
alt_text: string;
/**
* The attachment caption.
*/
caption: ContextualField< string, 'edit', C >;
/**
* The attachment description.
*/
description: ContextualField<
RenderedText< C >,
'view' | 'edit',
C
>;
/**
* Attachment type.
*/
media_type: MediaType;
/**
* The attachment MIME type.
*/
mime_type: string;
/**
* Details about the media file, specific to its type.
*/
media_details: Record< string, string >;
/**
* The ID for the associated post of the attachment.
*/
post: ContextualField< number, 'view' | 'edit', C >;
/**
* URL to the original attachment file.
*/
source_url: string;
/**
* List of the missing image sizes of the attachment.
*/
missing_image_sizes: ContextualField< string[], 'edit', C >;
}
}
}

export type Attachment< C extends Context > = OmitNevers<
_WPBaseTypes.Attachment< C >
>;
Loading