Skip to content

Commit

Permalink
feat: support resource link fields (#246)
Browse files Browse the repository at this point in the history
  • Loading branch information
veu authored Apr 21, 2023
1 parent 9401a0c commit 6b4b721
Show file tree
Hide file tree
Showing 19 changed files with 213 additions and 32 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
"@oclif/config": "^1.18.3",
"@oclif/errors": "^1.3.5",
"@oclif/plugin-help": "^5.1.12",
"contentful": "^9.1.29",
"contentful": "^10.1.0",
"contentful-export": "^7.17.13",
"contentful-management": "^10.27.4",
"fs-extra": "^11.1.0",
Expand Down
4 changes: 2 additions & 2 deletions src/extract-validation.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { FieldItem, FieldValidation } from 'contentful';
import { FieldItem, ContentTypeFieldValidation } from 'contentful';

type WithValidations = Pick<FieldItem, 'validations'>;

const validation = (node: WithValidations, field: keyof FieldValidation): any => {
const validation = (node: WithValidations, field: keyof ContentTypeFieldValidation): any => {
if (node.validations && node.validations.length > 0) {
const linkContentValidation = node.validations.find((value) => value[field]);
if (linkContentValidation) {
Expand Down
4 changes: 2 additions & 2 deletions src/property-imports.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { Field } from 'contentful';
import { ContentTypeField } from 'contentful';
import { ImportDeclarationStructure, OptionalKind } from 'ts-morph';
import { linkContentTypeValidations } from './extract-validation';
import { RenderContext } from './renderer';

export const propertyImports = (
field: Field,
field: ContentTypeField,
context: RenderContext,
ignoreModule?: string,
): OptionalKind<ImportDeclarationStructure>[] => {
Expand Down
2 changes: 2 additions & 0 deletions src/renderer/field/default-renderers.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { renderPropAny } from './render-prop-any';
import { renderPropArray } from './render-prop-array';
import { renderPropLink } from './render-prop-link';
import { renderPropResourceLink } from './render-prop-resource-link';
import { renderRichText } from './render-prop-richtext';
import { FieldRenderers } from './render-types';

export const defaultRenderers: FieldRenderers = {
RichText: renderRichText,
Link: renderPropLink,
ResourceLink: renderPropResourceLink,
Array: renderPropArray,
Text: renderPropAny,
Symbol: renderPropAny,
Expand Down
1 change: 1 addition & 0 deletions src/renderer/field/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@ export * from './render-types';
export { renderPropAny } from './render-prop-any';
export { renderPropArray } from './render-prop-array';
export { renderPropLink } from './render-prop-link';
export { renderPropResourceLink } from './render-prop-resource-link';
export { renderRichText } from './render-prop-richtext';
export { defaultRenderers } from './default-renderers';
4 changes: 2 additions & 2 deletions src/renderer/field/render-prop-any.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { Field } from 'contentful';
import { ContentTypeField } from 'contentful';
import { renderTypeLiteral, renderTypeUnion } from '../generic';
import { RenderContext } from '../type';

export const renderPropAny = (field: Field, context: RenderContext): string => {
export const renderPropAny = (field: ContentTypeField, context: RenderContext): string => {
if (field.validations?.length > 0) {
const includesValidation = field.validations.find((validation) => validation.in);
if (includesValidation && includesValidation.in) {
Expand Down
8 changes: 6 additions & 2 deletions src/renderer/field/render-prop-array.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { Field } from 'contentful';
import { ContentTypeField } from 'contentful';
import { inValidations } from '../../extract-validation';
import { renderTypeArray, renderTypeLiteral, renderTypeUnion } from '../generic';
import { RenderContext } from '../type';

export const renderPropArray = (field: Field, context: RenderContext): string => {
export const renderPropArray = (field: ContentTypeField, context: RenderContext): string => {
if (!field.items) {
throw new Error(`missing items for ${field.id}`);
}
Expand All @@ -12,6 +12,10 @@ export const renderPropArray = (field: Field, context: RenderContext): string =>
return renderTypeArray(context.getFieldRenderer('Link')(field.items, context));
}

if (field.items.type === 'ResourceLink') {
return renderTypeArray(context.getFieldRenderer('ResourceLink')(field, context));
}

if (field.items.type === 'Symbol') {
const validation = inValidations(field.items);

Expand Down
9 changes: 6 additions & 3 deletions src/renderer/field/render-prop-link.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import { Field } from 'contentful';
import { ContentTypeField } from 'contentful';
import { linkContentTypeValidations } from '../../extract-validation';
import { renderTypeGeneric, renderTypeUnion } from '../generic';
import { RenderContext } from '../type';

export const renderPropLink = (
field: Pick<Field, 'validations' | 'linkType'>,
field: Pick<ContentTypeField, 'validations' | 'linkType'>,
context: RenderContext,
): string => {
const linkContentType = (field: Pick<Field, 'validations'>, context: RenderContext): string => {
const linkContentType = (
field: Pick<ContentTypeField, 'validations'>,
context: RenderContext,
): string => {
const validations = linkContentTypeValidations(field);
return validations?.length > 0
? renderTypeUnion(validations.map((validation) => context.moduleFieldsName(validation)))
Expand Down
19 changes: 19 additions & 0 deletions src/renderer/field/render-prop-resource-link.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { ContentTypeField } from 'contentful';
import { renderTypeGeneric } from '../generic';
import { RenderContext } from '../type';

export const renderPropResourceLink = (field: ContentTypeField, context: RenderContext): string => {
for (const resource of field.allowedResources!) {
if (resource.type !== 'Contentful:Entry') {
throw new Error(`Unknown type "${resource.type}"`);
}
}

context.imports.add({
moduleSpecifier: 'contentful',
namedImports: ['Entry'],
isTypeOnly: true,
});

return renderTypeGeneric('Entry', 'Record<string, any>');
};
4 changes: 2 additions & 2 deletions src/renderer/field/render-prop-richtext.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Field } from 'contentful';
import { ContentTypeField } from 'contentful';
import { RenderContext } from '../type';

export const renderRichText = (field: Field, context: RenderContext): string => {
export const renderRichText = (field: ContentTypeField, context: RenderContext): string => {
context.imports.add({
moduleSpecifier: 'contentful',
namedImports: ['EntryFields'],
Expand Down
9 changes: 6 additions & 3 deletions src/renderer/field/render-types.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import { Field, FieldType } from 'contentful';
import { ContentTypeField, ContentTypeFieldType } from 'contentful';
import { RenderContext } from '../type';

export type FieldRenderer<FType extends FieldType> = (
field: FType extends 'Link' ? Pick<Field, 'validations' | 'linkType'> : Field,
export type FieldRenderer<FType extends ContentTypeFieldType> = (
field: FType extends 'Link'
? Pick<ContentTypeField, 'validations' | 'linkType'>
: ContentTypeField,
context: RenderContext,
) => string;

export type FieldRenderers = {
RichText: FieldRenderer<'RichText'>;
Link: FieldRenderer<'Link'>;
ResourceLink: FieldRenderer<'ResourceLink'>;
Array: FieldRenderer<'Array'>;
Text: FieldRenderer<'Text'>;
Symbol: FieldRenderer<'Symbol'>;
Expand Down
6 changes: 3 additions & 3 deletions src/renderer/type/create-default-context.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { FieldType } from 'contentful';
import { ContentTypeFieldType } from 'contentful';
import { ImportDeclarationStructure, OptionalKind } from 'ts-morph';
import { moduleFieldsName, moduleName } from '../../module-name';
import { defaultRenderers, FieldRenderer } from '../field';

export type RenderContext = {
getFieldRenderer: <FType extends FieldType>(fieldType: FType) => FieldRenderer<FType>;
getFieldRenderer: <FType extends ContentTypeFieldType>(fieldType: FType) => FieldRenderer<FType>;
moduleName: (id: string) => string;
moduleFieldsName: (id: string) => string;
imports: Set<OptionalKind<ImportDeclarationStructure>>;
Expand All @@ -14,7 +14,7 @@ export const createDefaultContext = (): RenderContext => {
return {
moduleName,
moduleFieldsName,
getFieldRenderer: <FType extends FieldType>(fieldType: FType) =>
getFieldRenderer: <FType extends ContentTypeFieldType>(fieldType: FType) =>
defaultRenderers[fieldType] as FieldRenderer<FType>,
imports: new Set(),
};
Expand Down
6 changes: 3 additions & 3 deletions src/renderer/type/default-content-type-renderer.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Field } from 'contentful';
import { ContentTypeField } from 'contentful';
import {
OptionalKind,
PropertySignatureStructure,
Expand Down Expand Up @@ -48,7 +48,7 @@ export class DefaultContentTypeRenderer extends BaseContentTypeRenderer {
protected addDefaultImports(context: RenderContext): void {}

protected renderField(
field: Field,
field: ContentTypeField,
context: RenderContext,
): OptionalKind<PropertySignatureStructure> {
return {
Expand All @@ -58,7 +58,7 @@ export class DefaultContentTypeRenderer extends BaseContentTypeRenderer {
};
}

protected renderFieldType(field: Field, context: RenderContext): string {
protected renderFieldType(field: ContentTypeField, context: RenderContext): string {
return context.getFieldRenderer(field.type)(field, context);
}

Expand Down
6 changes: 3 additions & 3 deletions src/renderer/type/js-doc-renderer.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Field } from 'contentful';
import { ContentTypeField } from 'contentful';
import { ContentTypeProps } from 'contentful-management';
import { JSDocStructure, JSDocTagStructure, OptionalKind, SourceFile } from 'ts-morph';
import { CFContentType } from '../../types';
Expand All @@ -14,11 +14,11 @@ type FieldsDocsOptionsProps = {
/* Name of generated Fields type */
readonly name: string;
readonly entryName: string;
readonly fields: Field[];
readonly fields: ContentTypeField[];
};

type FieldDocsOptionsProps = {
readonly field: Field;
readonly field: ContentTypeField;
};

export type JSDocRenderOptions = {
Expand Down
4 changes: 2 additions & 2 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Field } from 'contentful';
import { ContentTypeField } from 'contentful';

export type WriteCallback = (filePath: string, content: string) => Promise<void>;

Expand All @@ -8,5 +8,5 @@ export type CFContentType = {
id: string;
type: string;
};
fields: Field[];
fields: ContentTypeField[];
};
33 changes: 33 additions & 0 deletions test/renderer/field/render-prop-array.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,4 +112,37 @@ describe('A renderPropArray function', () => {
'Entry<TypeComponentCtaFields | TypeComponentFaqFields | TypeWrapperImageFields | TypeWrapperVideoFields>[]',
);
});

it('can evaluate a "Array" of "ResourceLink"', () => {
const field = JSON.parse(`
{
"id": "components",
"name": "Components",
"type": "Array",
"localized": false,
"required": true,
"validations": [],
"disabled": false,
"omitted": false,
"items": {
"type": "ResourceLink",
"validations": []
},
"allowedResources": [
{
"type": "Contentful:Entry",
"source": "crn:contentful:::content:spaces/spaceId",
"contentTypes": [
"componentCta",
"componentFaq",
"wrapperImage",
"wrapperVideo"
]
}
]
}
`);

expect(renderPropArray(field, createDefaultContext())).toEqual('Entry<Record<string, any>>[]');
});
});
59 changes: 59 additions & 0 deletions test/renderer/field/render-prop-resource-link.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { createDefaultContext, renderPropResourceLink } from '../../../src';

describe('A renderPropResourceLink function', () => {
it('can evaluate a "ResourceLink" type', () => {
const field = JSON.parse(`
{
"id": "category",
"name": "Category",
"type": "ResourceLink",
"localized": false,
"required": true,
"validations": [],
"disabled": false,
"omitted": false,
"allowedResources": [
{
"type": "Contentful:Entry",
"source": "crn:contentful:::content:spaces/spaceId",
"contentTypes": [
"topicCategory"
]
}
]
}
`);

expect(renderPropResourceLink(field, createDefaultContext())).toEqual(
'Entry<Record<string, any>>',
);
});

it('rejects a "ResourceLink" with an unknown resource type', () => {
const field = JSON.parse(`
{
"id": "category",
"name": "Category",
"type": "ResourceLink",
"localized": false,
"required": true,
"validations": [],
"disabled": false,
"omitted": false,
"allowedResources": [
{
"type": "Contentful:UnknownEntity",
"source": "crn:contentful:::content:spaces/spaceId",
"contentTypes": [
"topicCategory"
]
}
]
}
`);

expect(() => renderPropResourceLink(field, createDefaultContext())).toThrow(
'Unknown type "Contentful:UnknownEntity"',
);
});
});
6 changes: 3 additions & 3 deletions test/renderer/type/content-type-renfderer.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Field, FieldType } from 'contentful';
import { ContentTypeField, ContentTypeFieldType } from 'contentful';

import { Project, ScriptTarget, SourceFile } from 'ts-morph';
import {
Expand Down Expand Up @@ -38,7 +38,7 @@ describe('A derived content type renderer class', () => {
return {
moduleName,
moduleFieldsName,
getFieldRenderer: <FType extends FieldType>(fieldType: FType) => {
getFieldRenderer: <FType extends ContentTypeFieldType>(fieldType: FType) => {
if (fieldType === 'Symbol') {
return symbolTypeRenderer as FieldRenderer<FType>;
}
Expand Down Expand Up @@ -88,7 +88,7 @@ describe('A derived content type renderer class', () => {

it('can return a custom field renderer with docs support', () => {
class DerivedContentTypeRenderer extends DefaultContentTypeRenderer {
protected renderField(field: Field, context: RenderContext) {
protected renderField(field: ContentTypeField, context: RenderContext) {
return {
docs: [{ description: `Field of type "${field.type}"` }],
name: field.id,
Expand Down
Loading

0 comments on commit 6b4b721

Please sign in to comment.