Skip to content

Commit

Permalink
Feat: amplience delivery key directive (#87)
Browse files Browse the repository at this point in the history
* feat: amplienceDeliveryKey directive

* feat: amplienceDeliveryKey validation

* test: tests for amplienceDeliveryKey

* refactor: pr comments
  • Loading branch information
jonnoallcock authored Jun 11, 2024
1 parent d3a578d commit 7b21e9c
Show file tree
Hide file tree
Showing 14 changed files with 306 additions and 62 deletions.
6 changes: 6 additions & 0 deletions example/example.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ type Base @amplienceContentType {
union: Union
linkedA: A @amplienceLink
referencedB: B @amplienceReference
deliveryKey: String
@amplienceDeliveryKey(
title: "example title"
description: "example description"
pattern: "some-pattern/requirement"
)
}

enum Enum {
Expand Down
1 change: 1 addition & 0 deletions packages/common/src/directives.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export const schemaPrepend = gql`
directive @amplienceIgnore on FIELD_DEFINITION | SCALAR | OBJECT
directive @amplienceSortable on FIELD_DEFINITION
directive @amplienceFilterable on FIELD_DEFINITION
directive @amplienceDeliveryKey(title: String, description: String, pattern: String) on FIELD_DEFINITION
enum ValidationLevel {
SLOT
Expand Down
24 changes: 24 additions & 0 deletions packages/plugin-json/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,3 +151,27 @@ type MyContentType @amplienceContentType {
constArray: [String!]! @amplienceConst(items: ["this", "is", "const"])
}
```

## @amplienceDeliveryKey

Adds a delivery key field to the Content Type form. Only works on `String` types.

[See documentation](https://amplience.com/developers/docs/dev-tools/guides-tutorials/delivery-keys/#including-the-deliverykey-property-in-a-content-type-schema)

```graphql
type MyContentType @amplienceContentType {
deliveryKey: String
@amplienceDeliveryKey(
# Optional field title
# Default value: 'Delivery Key'
title: "example title"
# Optional field description
# Default value: 'Set a delivery key for this content item'
description: "Format: delivery-key/format/requirement"
# Optional field validation pattern
pattern: "delivery-key/format/requirement"
)
}
```
14 changes: 13 additions & 1 deletion packages/plugin-json/examples/output/schemas/test.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,22 @@
"title": "Name",
"description": "This a description",
"type": "string"
},
"_meta": {
"type": "object",
"title": "Delivery Key",
"properties": {
"deliveryKey": {
"type": "string",
"title": "test title",
"description": "test description",
"pattern": "test pattern"
}
}
}
},
"description": "Test",
"type": "object",
"propertyOrder": ["bla", "name"],
"propertyOrder": ["_meta", "bla", "name"],
"required": ["bla"]
}
22 changes: 2 additions & 20 deletions packages/plugin-json/examples/output/schemas/test2.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,32 +11,14 @@
"type": "array",
"items": {
"type": "object",
"properties": {
"bla": { "title": "Bla", "type": "integer" },
"name": {
"title": "Name",
"description": "This a description",
"type": "string"
}
},
"propertyOrder": ["bla", "name"],
"required": ["bla"]
"allOf": [{ "$ref": "https://schema-examples.com/test" }]
}
},
"name": { "title": "Name", "type": "string" },
"test": {
"title": "Test",
"type": "object",
"properties": {
"bla": { "title": "Bla", "type": "integer" },
"name": {
"title": "Name",
"description": "This a description",
"type": "string"
}
},
"propertyOrder": ["bla", "name"],
"required": ["bla"]
"allOf": [{ "$ref": "https://schema-examples.com/test" }]
}
},
"description": "Test2",
Expand Down
7 changes: 7 additions & 0 deletions packages/plugin-json/examples/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ type Test @amplienceContentType {
name: String

bla: Int!

deliveryKey: String
@amplienceDeliveryKey(
title: "test title"
description: "test description"
pattern: "test pattern"
)
}

type Test2 @amplienceContentType {
Expand Down
16 changes: 16 additions & 0 deletions packages/plugin-json/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@ import { paramCase } from "change-case";
import { ObjectTypeDefinitionNode } from "graphql";
import { contentTypeSchemaBody } from "./lib/amplience-schema-transformers";
import {
getDeliveryKeyNotNullableStringReport,
getObjectTypeDefinitions,
getRequiredLocalizedFieldsReport,
getTooManyDeliveryKeysReport,
getTooManyFiltersReport,
} from "./lib/validation";

Expand Down Expand Up @@ -69,6 +71,20 @@ export const validate: PluginValidateFn<PluginConfig> = (
`Types can have no more than 5 fields with '@amplienceFiltered'.\n\n${tooManyFiltersReport}`
);
}

const tooManyDeliveryKeysReport = getTooManyDeliveryKeysReport(types);
if (tooManyDeliveryKeysReport) {
throw new Error(
`Types can only have 1 field with '@amplienceDeliveryKey'.\n\n${tooManyDeliveryKeysReport}`
)
}

const deliveryKeyNotNullableStringReport = getDeliveryKeyNotNullableStringReport(types);
if (deliveryKeyNotNullableStringReport) {
throw new Error(
`Fields with '@amplienceDeliveryKey' must be of Nullable type String.\n\n${deliveryKeyNotNullableStringReport}`
)
}
};

export const preset: Types.OutputPreset<PresetConfig> = {
Expand Down
109 changes: 72 additions & 37 deletions packages/plugin-json/src/lib/amplience-schema-transformers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
} from 'amplience-graphql-codegen-common'
import { capitalCase, paramCase } from 'change-case'
import {
ConstDirectiveNode,
EnumValueNode,
FieldDefinitionNode,
GraphQLSchema,
Expand All @@ -26,7 +27,7 @@ import {
isObjectType,
isUnionType,
} from 'graphql'
import { AmplienceContentTypeSchemaBody, AmpliencePropertyType } from './types'
import { AmplienceContentTypeSchemaBody, AmplienceDeliveryKeyType, AmpliencePropertyType } from './types'

/**
* Returns a Amplience ContentType from an interface type.
Expand All @@ -35,42 +36,52 @@ export const contentTypeSchemaBody = (
type: ObjectTypeDefinitionNode,
schema: GraphQLSchema,
schemaHost: string
): AmplienceContentTypeSchemaBody => ({
$id: typeUri(type, schemaHost),
$schema: "http://json-schema.org/draft-07/schema#",
...refType(AMPLIENCE_TYPE.CORE.Content),
title: capitalCase(type.name.value),
properties: {
...objectProperties(type, schema, schemaHost),
},
description: type.description?.value ?? capitalCase(type.name.value),
"trait:sortable": sortableTrait(type),
"trait:hierarchy": isHierarchy(type)
? hierarchyTrait(type, schema, schemaHost)
: undefined,
"trait:filterable": filterableTrait(type),
type: "object",
propertyOrder:
): AmplienceContentTypeSchemaBody => {
const properties = objectProperties(type, schema, schemaHost)
const propertyOrder =
type.fields
?.filter((field) => isAmplienceProperty(type, field))
.map((field) => field.name.value) ?? [],
required: type.fields
?.filter((field) => isAmplienceProperty(type, field))
.filter(
(field) =>
field.type.kind === "NonNullType" ||
hasDirective(field, "amplienceLocalized")
)
.map((n) => n.name.value),
...(isHierarchy(type)
? {
allOf: [
refType(AMPLIENCE_TYPE.CORE.Content).allOf[0],
refType(AMPLIENCE_TYPE.CORE.HierarchyNode).allOf[0],
],
}
: {}),
});
?.filter((field) => isAmplienceProperty(type, field) && !hasDeliveryKeyDirective(field))
.map((field) => field.name.value) ?? []

const deliveryKeyDirective = maybeDeliveryKeyDirective(type)
if (deliveryKeyDirective) {
properties._meta = deliveryKeyMetaProperty(deliveryKeyDirective)
// always place delivery keys at the top of the form, if present
propertyOrder.unshift('_meta')
}

return {
$id: typeUri(type, schemaHost),
$schema: "http://json-schema.org/draft-07/schema#",
...refType(AMPLIENCE_TYPE.CORE.Content),
title: capitalCase(type.name.value),
properties,
description: type.description?.value ?? capitalCase(type.name.value),
"trait:sortable": sortableTrait(type),
"trait:hierarchy": isHierarchy(type)
? hierarchyTrait(type, schema, schemaHost)
: undefined,
"trait:filterable": filterableTrait(type),
type: "object",
propertyOrder,
required: type.fields
?.filter((field) => isAmplienceProperty(type, field) && !hasDeliveryKeyDirective(field))
.filter(
(field) =>
field.type.kind === "NonNullType" ||
hasDirective(field, "amplienceLocalized")
)
.map((n) => n.name.value),
...(isHierarchy(type)
? {
allOf: [
refType(AMPLIENCE_TYPE.CORE.Content).allOf[0],
refType(AMPLIENCE_TYPE.CORE.HierarchyNode).allOf[0],
],
}
: {}),
}
}

/**
* Returns the properties that go inside Amplience `{type: 'object', properties: ...}`
Expand All @@ -82,7 +93,7 @@ export const objectProperties = (
): { [name: string]: AmpliencePropertyType } =>
Object.fromEntries(
type.fields
?.filter((field) => isAmplienceProperty(type, field))
?.filter((field) => isAmplienceProperty(type, field) && !hasDeliveryKeyDirective(field))
.map((prop) => [
prop.name.value,
{
Expand Down Expand Up @@ -115,6 +126,30 @@ export const objectProperties = (
]) ?? []
);

const maybeDeliveryKeyDirective = (type: ObjectTypeDefinitionNode): ConstDirectiveNode | undefined =>
type.fields?.reduce<ConstDirectiveNode | undefined>((directive, field) => directive ?? maybeDirective(field, 'amplienceDeliveryKey'), undefined)

const hasDeliveryKeyDirective = (field: FieldDefinitionNode) => Boolean(maybeDirective(field, 'amplienceDeliveryKey'))

const deliveryKeyMetaProperty = (deliveryKeyDirective: ConstDirectiveNode): AmplienceDeliveryKeyType => {
const title = maybeDirectiveValue<StringValueNode>(deliveryKeyDirective, 'title')?.value ?? 'Delivery Key'
const description = maybeDirectiveValue<StringValueNode>(deliveryKeyDirective, 'description')?.value ?? 'Set a delivery key for this content item'
const pattern = maybeDirectiveValue<StringValueNode>(deliveryKeyDirective, 'pattern')?.value

return {
type: 'object',
title: 'Delivery Key',
properties: {
deliveryKey: {
type: 'string',
title,
description,
pattern,
}
}
}
}

const isHierarchy = (type: ObjectTypeDefinitionNode) =>
maybeDirectiveValue<EnumValueNode>(
maybeDirective(type, "amplienceContentType")!,
Expand Down
15 changes: 14 additions & 1 deletion packages/plugin-json/src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,19 @@ export interface AmpliencePropertyType {
examples?: string[];
}

export type AmplienceDeliveryKeyType = {
type: 'object',
title: 'Delivery Key'
properties: {
deliveryKey: {
type: 'string'
title: string
description: string
pattern?: string
}
}
}

export interface AmplienceContentTypeSchemaBody {
$id: string;
$schema: string;
Expand All @@ -29,7 +42,7 @@ export interface AmplienceContentTypeSchemaBody {
"trait:hierarchy"?: {};
"trait:sortable"?: {};
type: "object";
properties?: { [name: string]: AmpliencePropertyType };
properties?: {[name: string | '_meta']: AmpliencePropertyType | AmplienceDeliveryKeyType | undefined};
definitions?: { [name: string]: AmpliencePropertyType };
propertyOrder?: string[];
required?: string[];
Expand Down
31 changes: 28 additions & 3 deletions packages/plugin-json/src/lib/validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,39 @@ const isNonNullLocalizedField = (field: FieldDefinitionNode) =>
export const getTooManyFiltersReport = (types: ObjectTypeDefinitionNode[]) =>
getFieldsReport(
types.filter(
(type) => (type.fields?.filter(filterableField).length ?? 0) > 5
(type) => (type.fields?.filter(isFilterableField).length ?? 0) > 5
),
filterableField
isFilterableField
);

const filterableField = (field: FieldDefinitionNode) =>
const isFilterableField = (field: FieldDefinitionNode) =>
hasDirective(field, "amplienceFilterable");

export const getTooManyDeliveryKeysReport = (types: ObjectTypeDefinitionNode[]) =>
getFieldsReport(
types.filter(
(type) => (type.fields?.filter(isDeliveryKeyField).length ?? 0) > 1
),
isDeliveryKeyField
)

export const getDeliveryKeyNotNullableStringReport = (types: ObjectTypeDefinitionNode[]) =>
getFieldsReport(
types.filter(
(type) => type.fields?.some((field) => isDeliveryKeyField(field) && !isNullableStringField(field))
),
(field) => isDeliveryKeyField(field) && !isNullableStringField(field)
)

const isDeliveryKeyField = (field: FieldDefinitionNode) =>
hasDirective(field, 'amplienceDeliveryKey')

const isNullableStringField = (field: FieldDefinitionNode) =>
(
field.type.kind === 'NamedType' &&
field.type.name.value === 'String'
)

/**
* Converts a type with filtered fields in a simple string report.
*
Expand Down
11 changes: 11 additions & 0 deletions packages/plugin-json/test/testdata/base.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ type Base @amplienceContentType {
union: Union
linkedA: A @amplienceLink
referencedB: B @amplienceReference
deliveryKeyWithDefaults: String @amplienceDeliveryKey
}

enum Enum {
Expand All @@ -43,6 +44,16 @@ type B @amplienceContentType {
b: String!
}

type DeliveryKeyExplicit @amplienceContentType {
a: String!
deliveryKeyExplicit: String
@amplienceDeliveryKey(
title: "explicit title"
description: "explicit description"
pattern: "explicit pattern"
)
}

type Inlined {
inlined: String!
}
Expand Down
Loading

0 comments on commit 7b21e9c

Please sign in to comment.