-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add React Query to cache Contentful client-side queries
- Loading branch information
1 parent
020c4a8
commit 7c5b193
Showing
13 changed files
with
2,802 additions
and
59 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
generated |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,158 @@ | ||
// tslint:disable:max-classes-per-file | ||
// tslint:disable-next-line:interface-over-type-literal | ||
export type JsonObject = { [k: string]: any } | ||
|
||
export interface IEntry<TFields extends JsonObject> { | ||
sys: ISys<'Entry'>, | ||
fields: TFields | ||
} | ||
|
||
export class Entry<TFields extends JsonObject> implements IEntry<TFields> { | ||
public readonly sys!: ISys<'Entry'> | ||
public readonly fields!: TFields | ||
|
||
protected constructor(entryOrId: IEntry<TFields> | string, contentType?: string, fields?: TFields) { | ||
if (typeof entryOrId == 'string') { | ||
if (!fields) { | ||
throw new Error('No fields provided') | ||
} | ||
if (!contentType) { | ||
throw new Error('No contentType provided') | ||
} | ||
|
||
this.sys = { | ||
id: entryOrId, | ||
type: 'Entry', | ||
space: undefined, | ||
contentType: { | ||
sys: { | ||
type: 'Link', | ||
linkType: 'ContentType', | ||
id: contentType, | ||
}, | ||
}, | ||
} | ||
this.fields = fields | ||
} else { | ||
if (typeof entryOrId.sys == 'undefined') { | ||
throw new Error('Entry did not have a `sys`!') | ||
} | ||
if (typeof entryOrId.fields == 'undefined') { | ||
throw new Error('Entry did not have a `fields`!') | ||
} | ||
Object.assign(this, entryOrId) | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* Checks whether the given object is a Contentful entry | ||
* @param obj | ||
*/ | ||
export function isEntry(obj: any): obj is IEntry<any> { | ||
return obj && obj.sys && obj.sys.type === 'Entry' | ||
} | ||
|
||
interface IAssetFields { | ||
title?: string, | ||
description?: string, | ||
file: { | ||
url?: string, | ||
details?: { | ||
size?: number, | ||
}, | ||
fileName?: string, | ||
contentType?: string, | ||
}, | ||
} | ||
|
||
export interface IAsset { | ||
sys: ISys<'Asset'>, | ||
fields: IAssetFields | ||
} | ||
|
||
export class Asset implements IAsset { | ||
public readonly sys!: ISys<'Asset'> | ||
public readonly fields!: IAssetFields | ||
|
||
constructor(asset: IAsset) | ||
constructor(id: string, fields: IAssetFields) | ||
constructor(entryOrId: IAsset | string, fields?: IAssetFields) { | ||
if (typeof entryOrId == 'string') { | ||
if (!fields) { | ||
throw new Error('No fields provided') | ||
} | ||
|
||
this.sys = { | ||
id: entryOrId, | ||
type: 'Asset', | ||
contentType: undefined, | ||
} | ||
this.fields = fields | ||
} else { | ||
if (typeof entryOrId.sys == 'undefined') { | ||
throw new Error('Entry did not have a `sys`!') | ||
} | ||
if (typeof entryOrId.fields == 'undefined') { | ||
throw new Error('Entry did not have a `fields`!') | ||
} | ||
Object.assign(this, entryOrId) | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* Checks whether the given object is a Contentful asset | ||
* @param obj | ||
*/ | ||
export function isAsset(obj: any): obj is IAsset { | ||
return obj && obj.sys && obj.sys.type === 'Asset' | ||
} | ||
|
||
export interface ILink<Type extends string> { | ||
sys: { | ||
type: 'Link', | ||
linkType: Type, | ||
id: string, | ||
}, | ||
} | ||
|
||
export interface ISys<Type extends string> { | ||
space?: ILink<'Space'>, | ||
id: string, | ||
type: Type, | ||
createdAt?: string, | ||
updatedAt?: string, | ||
revision?: number, | ||
environment?: ILink<'Environment'>, | ||
contentType: Type extends 'Entry' ? ILink<'ContentType'> : undefined, | ||
locale?: string, | ||
} | ||
|
||
/** | ||
* Checks whether the given object is a Contentful link | ||
* @param obj | ||
*/ | ||
export function isLink(obj: any): obj is ILink<string> { | ||
return obj && obj.sys && obj.sys.type === 'Link' | ||
} | ||
|
||
/** | ||
* This complex type & associated helper allow us to mark an entry as | ||
* not including any links. The compiler will understand that even though | ||
* the entry type (i.e. 'IPage') contains links, a Resolved<IPage> will have | ||
* resolved the links into the actual entries or assets. | ||
*/ | ||
export type Resolved<TEntry> = | ||
TEntry extends IEntry<infer TProps> ? | ||
// TEntry is an entry and we know the type of it's props | ||
IEntry<{ | ||
[P in keyof TProps]: ResolvedField<Exclude<TProps[P], ILink<any>>> | ||
}> | ||
: never | ||
|
||
type ResolvedField<TField> = | ||
TField extends Array<infer TItem> ? | ||
// Array of entries - dive into the item type to remove links | ||
Array<Exclude<TItem, ILink<any>>> : | ||
TField |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
import { ContentfulClientApi, Entry as ContentfulEntry } from 'contentful' | ||
import { Entry, IAsset, IEntry, JsonObject, Resolved } from './base' | ||
|
||
// Comment this out if you do not have the `contentful` NPM module installed. | ||
declare module 'contentful' { | ||
// tslint:disable:interface-name | ||
export interface Entry<T> { | ||
toPlainObject(): IEntry<T> | ||
} | ||
|
||
export interface Asset { | ||
toPlainObject(): IAsset | ||
} | ||
|
||
export interface ContentfulClientApi { | ||
/** | ||
* Get an entry, casting to its content type. | ||
* | ||
* Since the Contentful API by default resolves to one layer, the resulting | ||
* type is a Resolved entry. | ||
*/ | ||
getEntry<T extends IEntry<any>>(id: string, query?: any): Promise<Resolved<T>> | ||
getEntry<T>(id: string, query?: any): Promise<Resolved<Entry<T>>> | ||
} | ||
} | ||
|
||
declare module './base' { | ||
export interface Entry<TFields extends JsonObject> { | ||
/** | ||
* Resolves this entry to the specified depth (less than 10), and returns the | ||
* raw object. | ||
* @param n The depth to resolve to. | ||
* @param client The client to use. | ||
*/ | ||
resolve(n: number, client: ContentfulClientApi, query?: any): Promise<Resolved<IEntry<TFields>>> | ||
} | ||
} | ||
|
||
Entry.prototype.resolve = async function(n, client, query?: any) { | ||
const id = this.sys.id | ||
const entry = await client.getEntry(id, Object.assign({}, query, { include: n })) | ||
const pojo = (entry as ContentfulEntry<any>).toPlainObject() | ||
|
||
Object.assign(this, pojo) | ||
return pojo | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
import { IEntry, ILink } from './base' | ||
import { TypeDirectory } from './generated' | ||
|
||
export * from './base' | ||
export {wrap} from './generated' | ||
|
||
export type KnownContentType = keyof TypeDirectory | ||
|
||
require('./ext') | ||
// include this to extend the generated objects | ||
// require('./ext/menu') |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
import { IEntry, isEntry, isLink, JsonObject, Resolved } from '.' | ||
|
||
/** | ||
* Returns a boolean indicating whether the given entry is resolved to a certain | ||
* depth. Typescript can understand the result of this within an if or switch | ||
* statement. | ||
* | ||
* @param entry The entry whose fields should be checked for links | ||
* @param depth how far down the tree to expect that the entry was resolved. | ||
* @returns a boolean indicating that the entry is a Resolved entry. | ||
*/ | ||
export function isResolved<TProps extends JsonObject>( | ||
entry: IEntry<TProps>, | ||
depth: number = 1, | ||
): entry is Resolved<IEntry<TProps>> { | ||
if (depth < 1) { throw new Error(`Depth cannot be less than 1 (was ${depth})`) } | ||
|
||
return Object.keys(entry.fields).every((field) => { | ||
const val = entry.fields[field] | ||
|
||
if (Array.isArray(val)) { | ||
return val.every(check) | ||
} else { | ||
return check(val) | ||
} | ||
}) | ||
|
||
function check(val: any): boolean { | ||
if (isLink(val)) { | ||
return false | ||
} | ||
if (depth > 1 && isEntry(val)) { | ||
return isResolved(val, depth - 1) | ||
} | ||
return true | ||
} | ||
} | ||
|
||
/** | ||
* Expects that an entry has been resolved to at least a depth of 1, | ||
* throwing an error if not. | ||
* | ||
* @param entry The entry whose fields should be checked for links | ||
* @returns the same entry object, declaring it as resolved. | ||
*/ | ||
export function expectResolved<TProps extends JsonObject>( | ||
entry: IEntry<TProps>, | ||
depth: number = 1, | ||
): Resolved<IEntry<TProps>> { | ||
if (isResolved(entry, depth)) { | ||
return entry | ||
} | ||
throw new Error(`${entry.sys.contentType.sys.id} ${entry.sys.id} was not fully resolved`) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.