Skip to content

Commit 3baeb83

Browse files
committed
feat: add taxonomy validations to content type
1 parent 6686899 commit 3baeb83

33 files changed

+1253
-37
lines changed

CONTRIBUTING.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,13 @@ Don't worry if this is your first time with Typescript. The language reads like
1616

1717
In order to run integration tests, you need:
1818

19-
* a source space the tests are run against (`CONTENTFUL_SPACE_ID`) and is called `contentful-migration`
20-
* a management token for the defined space (`CONTENTFUL_INTEGRATION_TEST_CMA_TOKEN`)
19+
- a source space the tests are run against (`CONTENTFUL_SPACE_ID`) and is called `contentful-migration`
20+
- a source organization the tests are run against (`CONTENTFUL_ORGANIZATION_ID`)
21+
- a management token for the defined space (`CONTENTFUL_INTEGRATION_TEST_CMA_TOKEN`)
2122

2223
```sh
2324
CONTENTFUL_SPACE_ID=<spaceId> \
2425
CONTENTFUL_INTEGRATION_TEST_CMA_TOKEN=CFPAT-xxx \
26+
CONTENTFUL_ORGANIZATION_ID=<organizationId> \
2527
npm test
2628
```
27-

README.md

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -764,6 +764,39 @@ Configure the annotations assigned to this content type. See [annotations docume
764764

765765
Remove all assigned annotations from this content type
766766

767+
#### `addTaxonomyValidation(id, linkType[, opts])`
768+
769+
Adds a taxonomy validation to this content type. This method is additive - you can call it multiple times to add validations one by one.
770+
771+
**`id : string`** – The ID of the taxonomy concept or concept scheme.
772+
773+
**`linkType : string`** – The type of taxonomy link. Must be either `TaxonomyConcept` or `TaxonomyConceptScheme`.
774+
775+
**`opts : Object`** – Validation options, with the following properties:
776+
777+
- **`required : boolean`** – Whether this taxonomy validation is required. Defaults to `false`.
778+
779+
#### `clearTaxonomyValidations()`
780+
781+
Remove all taxonomy validations from this content type.
782+
783+
#### `setTaxonomyValidations(validations)`
784+
785+
Replace all existing taxonomy validations with a new set of validations.
786+
787+
**`validations : Array`** – Array of taxonomy validation objects. Each validation object should have the following structure:
788+
789+
```javascript
790+
{
791+
sys: {
792+
type: 'Link',
793+
id: 'taxonomy-id',
794+
linkType: 'TaxonomyConcept' // or 'TaxonomyConceptScheme'
795+
},
796+
required: true // or false
797+
}
798+
```
799+
767800
### Field
768801

769802
The field object has the same methods as the properties listed in the [`ContentType.createField`](#createfieldid--string-opts--object--field) method.
@@ -881,7 +914,7 @@ You can use Typescript to write your migration files using `ts-node`! First `npm
881914
then run your migration with ts-node:
882915

883916
```bash
884-
node_modules/.bin/ts-node node_modules/.bin/contentful-migration -s $CONTENTFUL_SPACE_ID -a $CONTENTFUL_MANAGEMENT_TOKEN my_migration.ts
917+
node_modules/.bin/ts-node node_modules/.bin/contentful-migration -s $CONTENTFUL_SPACE_ID -a $CONTENTFUL_MANAGEMENT_TOKEN -o $CONTENTFUL_ORGANIZATION_ID my_migration.ts
885918
```
886919

887920
An example Typescript migration:
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// Note: Taxonomy concepts and concept schemes cannot yet be created via migrations.
2+
// You must first create them via the Contentful web app or CMA API.
3+
// This example shows how to add existing taxonomy concepts/schemes as validations to a content type.
4+
5+
module.exports = function (migration) {
6+
const contentType = migration.createContentType('taxonomy-example').name('Taxonomy Example')
7+
8+
// Add taxonomy validations one by one
9+
contentType
10+
.addTaxonomyValidation('existing-concept', 'TaxonomyConcept', { required: true })
11+
.addTaxonomyValidation('existing-concept-scheme', 'TaxonomyConceptScheme')
12+
13+
// Clear all taxonomy validations
14+
contentType.clearTaxonomyValidations()
15+
16+
// Replace all taxonomy validations with a new set
17+
contentType.setTaxonomyValidations([
18+
{
19+
sys: {
20+
type: 'Link',
21+
id: 'existing-concept',
22+
linkType: 'TaxonomyConcept'
23+
},
24+
required: true
25+
},
26+
{
27+
sys: {
28+
type: 'Link',
29+
id: 'existing-concept-scheme',
30+
linkType: 'TaxonomyConceptScheme'
31+
},
32+
required: false
33+
}
34+
])
35+
}

index.d.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ export type RunMigrationConfig = {
44
accessToken?: string
55
spaceId?: string
66
environmentId?: string
7+
organizationId?: string
78
proxy?: string
89
rawProxy?: boolean
910
yes?: boolean
@@ -239,6 +240,24 @@ export interface IEntryEditor {
239240
settings?: IEditorInterfaceOptions
240241
}
241242

243+
export interface ITaxonomyConceptValidationLink {
244+
sys: {
245+
type: 'Link'
246+
linkType: 'TaxonomyConcept'
247+
id: string
248+
}
249+
required?: boolean
250+
}
251+
252+
export interface ITaxonomyConceptSchemeValidationLink {
253+
sys: {
254+
type: 'Link'
255+
linkType: 'TaxonomyConceptScheme'
256+
id: string
257+
}
258+
required?: boolean
259+
}
260+
242261
export interface ContentType {
243262
id: string
244263
instanceId: string
@@ -256,6 +275,23 @@ export interface ContentType {
256275
/** Removes all annotaions associated with the field */
257276
clearAnnotations(): ContentType
258277

278+
/** Set taxonomy validations associated with the content type */
279+
setTaxonomyValidations(
280+
taxonomyValidations: Array<
281+
ITaxonomyConceptValidationLink | ITaxonomyConceptSchemeValidationLink
282+
>
283+
): ContentType
284+
285+
/** Add a single taxonomy validation to the content type */
286+
addTaxonomyValidation(
287+
id: string,
288+
linkType: 'TaxonomyConcept' | 'TaxonomyConceptScheme',
289+
options?: { required?: boolean }
290+
): ContentType
291+
292+
/** Clear all taxonomy validations from the content type */
293+
clearTaxonomyValidations(): ContentType
294+
259295
/** Creates a field with provided id. */
260296
createField(id: string, init?: IFieldOptions): Field
261297

@@ -702,6 +738,7 @@ export interface ClientConfig {
702738
accessToken?: string
703739
spaceId?: string
704740
environmentId?: string
741+
organizationId?: string
705742
proxy?: string
706743
rawProxy?: boolean
707744
requestBatchSize?: number

package-lock.json

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,4 +166,4 @@
166166
"prettier --write"
167167
]
168168
}
169-
}
169+
}

src/bin/cli.ts

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -54,17 +54,27 @@ const makeTerminatingFunction =
5454
}
5555
}
5656

57+
export enum UrlBaseType {
58+
Space = 'space',
59+
Org = 'org'
60+
}
61+
5762
export const createMakeRequest = (
5863
client: PlainClientAPI,
59-
{ spaceId, environmentId, limit = 10, host = 'api.contentful.com' }
64+
{ spaceId, environmentId, organizationId, limit = 10, host = 'api.contentful.com' }
6065
) => {
6166
const throttle = pThrottle({
6267
limit,
6368
interval: 1000,
6469
strict: false
6570
})
6671

67-
const makeBaseUrl = (url: string) => {
72+
const makeOrgBaseUrl = (url: string) => {
73+
const parts = [`https://${host}`, 'organizations', organizationId, trim(url, '/')]
74+
return parts.filter((x) => x !== '').join('/')
75+
}
76+
77+
const makeSpaceBaseUrl = (url: string) => {
6878
const parts = [
6979
`https://${host}`,
7080
'spaces',
@@ -73,13 +83,12 @@ export const createMakeRequest = (
7383
environmentId,
7484
trim(url, '/')
7585
]
76-
7786
return parts.filter((x) => x !== '').join('/')
7887
}
7988

80-
return function makeRequest(requestConfig: RawAxiosRequestConfig) {
81-
const { url, ...config } = requestConfig
82-
const fullUrl = makeBaseUrl(url)
89+
return function makeRequest(requestConfig: RawAxiosRequestConfig & { baseType?: UrlBaseType }) {
90+
const { url, baseType, ...config } = requestConfig
91+
const fullUrl = baseType === UrlBaseType.Org ? makeOrgBaseUrl(url) : makeSpaceBaseUrl(url)
8392

8493
return throttle(() => client.raw.http(fullUrl, config))()
8594
}
@@ -121,6 +130,7 @@ const createRun = ({ shouldThrow }) =>
121130
const makeRequest = createMakeRequest(client, {
122131
spaceId: clientConfig.spaceId,
123132
environmentId: clientConfig.environmentId,
133+
organizationId: clientConfig.organizationId,
124134
limit: argv.requestLimit,
125135
host: clientConfig.host
126136
})

src/bin/lib/config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ function getEnvConfig(): ClientConfig {
3636
function getArgvConfig({
3737
spaceId,
3838
environmentId = 'master',
39+
organizationId,
3940
accessToken,
4041
proxy,
4142
rawProxy,
@@ -47,6 +48,7 @@ function getArgvConfig({
4748
const config = {
4849
spaceId,
4950
environmentId,
51+
organizationId,
5052
accessToken,
5153
proxy,
5254
rawProxy,

src/bin/usage-params.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ export default yargs
2727
describe: 'ID of the environment within the space to run the migration script on',
2828
default: 'master'
2929
})
30+
.option('organization-id', {
31+
alias: 'o',
32+
describe: 'ID of the organization (required for organization-scoped operations)'
33+
})
3034
.option('access-token', {
3135
alias: 'a',
3236
describe:
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import { EntityAction, EntityType } from './action'
2+
import {
3+
ContentType,
4+
TaxonomyConceptValidationLink,
5+
TaxonomyConceptSchemeValidationLink
6+
} from '../entities/content-type'
7+
8+
export class ContentTypeSetTaxonomyValidationsAction extends EntityAction {
9+
private contentTypeId: string
10+
private taxonomyValidations: Array<
11+
TaxonomyConceptValidationLink | TaxonomyConceptSchemeValidationLink
12+
>
13+
14+
constructor(
15+
contentTypeId: string,
16+
taxonomyValidations?: Array<TaxonomyConceptValidationLink | TaxonomyConceptSchemeValidationLink>
17+
) {
18+
super()
19+
this.contentTypeId = contentTypeId
20+
this.taxonomyValidations = taxonomyValidations
21+
}
22+
23+
getEntityId(): string {
24+
return this.contentTypeId
25+
}
26+
27+
getEntityType(): EntityType {
28+
return EntityType.ContentType
29+
}
30+
31+
async applyTo(ct: ContentType): Promise<void> {
32+
ct.setTaxonomyValidations(this.taxonomyValidations)
33+
}
34+
}
35+
36+
export class ContentTypeAddTaxonomyValidationAction extends EntityAction {
37+
private contentTypeId: string
38+
private taxonomyId: string
39+
private linkType: 'TaxonomyConcept' | 'TaxonomyConceptScheme'
40+
private options: { required?: boolean }
41+
42+
constructor(
43+
contentTypeId: string,
44+
taxonomyId: string,
45+
linkType: 'TaxonomyConcept' | 'TaxonomyConceptScheme',
46+
options: { required?: boolean } = {}
47+
) {
48+
super()
49+
this.contentTypeId = contentTypeId
50+
this.taxonomyId = taxonomyId
51+
this.linkType = linkType
52+
this.options = options
53+
}
54+
55+
getEntityId(): string {
56+
return this.contentTypeId
57+
}
58+
59+
getEntityType(): EntityType {
60+
return EntityType.ContentType
61+
}
62+
63+
async applyTo(ct: ContentType): Promise<void> {
64+
ct.addTaxonomyValidation(this.taxonomyId, this.linkType, this.options)
65+
}
66+
}
67+
68+
export class ContentTypeClearTaxonomyValidationsAction extends EntityAction {
69+
private contentTypeId: string
70+
71+
constructor(contentTypeId: string) {
72+
super()
73+
this.contentTypeId = contentTypeId
74+
}
75+
76+
getEntityId(): string {
77+
return this.contentTypeId
78+
}
79+
80+
getEntityType(): EntityType {
81+
return EntityType.ContentType
82+
}
83+
84+
async applyTo(ct: ContentType): Promise<void> {
85+
ct.clearTaxonomyValidations()
86+
}
87+
}

0 commit comments

Comments
 (0)