Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
51 changes: 51 additions & 0 deletions examples/56-add-items-validation.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
module.exports = function (migration) {
migration.createContentType('contentModelA', {
name: 'Content Model A',
description: 'A content model for addItemsValidation'
})

migration.createContentType('contentModelB', {
name: 'Content Model B',
description: 'B content model for addItemsValidation'
})

migration.createContentType('contentModelC', {
name: 'Content Model C',
description: 'C content model for addItemsValidation'
})

const testModel = migration.createContentType('testModel', {
name: 'Test Model',
description: 'A test model for addItemsValidation'
})

testModel.createField('name').name('Name').type('Symbol').required(true)

testModel
.createField('tags')
.name('Tags')
.type('Array')
.items({
type: 'Symbol'
})
.addItemsValidation([{ unique: true }, { size: { min: 1, max: 5 } }])

testModel
.createField('skills')
.name('Skills')
.type('Array')
.items({
type: 'Symbol'
})
.addItemsValidation([{ size: { min: 1 } }])

testModel
.createField('relatedEntries')
.name('Related Entries')
.type('Array')
.items({
type: 'Link',
linkType: 'Entry'
})
.addItemsValidation([{ linkContentType: ['contentModelA', 'contentModelB'] }])
}
6 changes: 6 additions & 0 deletions examples/57-add-items-validation-update.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
module.exports = function (migration) {
const testModel = migration.editContentType('testModel')

// Update the relatedEntries's items validations using addItemsValidation to add contentModelC
testModel.editField('relatedEntries').addItemsValidation([{ linkContentType: ['contentModelC'] }])
}
2 changes: 1 addition & 1 deletion src/bin/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,6 @@ const createRun = ({ shouldThrow }) =>
console.warn(chalk`⚠️ {bold.yellow Migration aborted}`)
}
}

export const runMigration = createRun({ shouldThrow: true })
export default createRun({ shouldThrow: false })

60 changes: 60 additions & 0 deletions src/lib/action/field-add-items-validation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { FieldAction } from './field-action'
import ContentType from '../entities/content-type'

class FieldAddItemsValidationAction extends FieldAction {
protected validations: any[]

constructor(contentTypeId: string, fieldId: string, validations: any[]) {
super(contentTypeId, fieldId)
this.validations = validations
}

async applyTo(ct: ContentType) {
const fields = ct.fields
const field = fields.getField(this.getFieldId())

if (!field.items) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

field would only have a items property if it is of type reference or media. So we might want to add one more check here to make sure we don't unnecessarily add the items field to a field where it wouldn't belong. Something like:

if (!field.items && ['Array', 'Link'].includes(field.type)) {

field.items = { type: 'Symbol' } // Default type for array items
}

if (!field.items.validations) {
field.items.validations = []
}

// Merge new validations with existing ones
const existingValidations = field.items.validations || []
const newValidations = this.validations || []

// Handle linkContentType validations specially
const existingLinkContentType = existingValidations.find((v) => v.linkContentType)
const newLinkContentType = newValidations.find((v) => v.linkContentType)

if (existingLinkContentType && newLinkContentType) {
// Merge linkContentType arrays
const mergedContentTypes = [
...new Set([
...(existingLinkContentType.linkContentType || []),
...(newLinkContentType.linkContentType || [])
])
]

// Remove both old validations
const filteredExisting = existingValidations.filter((v) => !v.linkContentType)
const filteredNew = newValidations.filter((v) => !v.linkContentType)

// Add merged validation
field.items.validations = [
...filteredExisting,
...filteredNew,
{ linkContentType: mergedContentTypes }
]
} else {
// If no linkContentType to merge, just append
field.items.validations = [...existingValidations, ...newValidations]
}

fields.setField(this.getFieldId(), field)
}
}

export { FieldAddItemsValidationAction }
4 changes: 4 additions & 0 deletions src/lib/intent/base-intent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,10 @@ export default abstract class Intent implements IntentInterface {
return false
}

isFieldAddItemsValidation() {
return false
}

isContentTransform() {
return false
}
Expand Down
56 changes: 56 additions & 0 deletions src/lib/intent/field-add-items-validation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import Intent from './base-intent'
import { FieldAddItemsValidationAction } from '../action/field-add-items-validation'
import { PlanMessage } from '../interfaces/plan-message'
import chalk from 'chalk'
import { RawStep } from '../interfaces/raw-step'

export default class FieldAddItemsValidationIntent extends Intent {
constructor(rawStep: RawStep) {
super(rawStep)
}

isFieldAddItemsValidation() {
return true
}

groupsWith(other: Intent): boolean {
const sameContentType = other.getContentTypeId() === this.getContentTypeId()
return (
(other.isContentTypeUpdate() ||
other.isContentTypeCreate() ||
other.isContentTypeAnnotate() ||
other.isFieldCreate() ||
other.isFieldUpdate() ||
other.isFieldMove() ||
other.isFieldAddItemsValidation()) &&
sameContentType
)
}

endsGroup(): boolean {
return false
}

toActions() {
return [
new FieldAddItemsValidationAction(
this.getContentTypeId(),
this.getFieldId(),
this.payload.props.validations
)
]
}

toPlanMessage(): PlanMessage {
return {
heading: chalk`Update Content Type {bold.yellow ${this.getContentTypeId()}}`,
sections: [
{
heading: chalk`Add items validations to array field {yellow ${this.getFieldId()}}`,
details: [`Validations: ${JSON.stringify(this.payload.props.validations)}`]
}
],
details: []
}
}
}
4 changes: 3 additions & 1 deletion src/lib/intent/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import EntrySetTagsIntent from './entry-set-tags'
import EditorLayoutChangeFieldGroupIdIntent from './editor-layout/editor-layout-change-field-group-id'
import EditorLayoutDeleteIntent from './editor-layout/editor-layout-delete'
import FieldAnnotateIntent from './field-annotate'
import FieldAddItemsValidationIntent from './field-add-items-validation'

export {
Intent as default,
Expand Down Expand Up @@ -74,5 +75,6 @@ export {
EntrySetTagsIntent as EntrySetTags,
EditorLayoutMoveFieldIntent as EditorLayoutMoveField,
EditorLayoutChangeFieldGroupIdIntent as EditorLayoutChangeFieldGroupId,
EditorLayoutDeleteIntent as EditorLayoutDelete
EditorLayoutDeleteIntent as EditorLayoutDelete,
FieldAddItemsValidationIntent as FieldAddItemsValidation
}
54 changes: 54 additions & 0 deletions src/lib/migration-steps/action-creators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -693,6 +693,60 @@ const actionCreators = {
annotations: annotationIds,
fieldAnnotationPayload
}
}),
addAnnotation: (
contentTypeId,
contentTypeInstanceId,
fieldId,
fieldInstanceId,
callsite,
annotationIds,
fieldAnnotationPayload
): Intents.FieldAnnotate =>
new Intents.FieldAnnotate({
type: 'field/annotate',
meta: {
contentTypeInstanceId: `contentType/${contentTypeId}/${contentTypeInstanceId}`,
fieldInstanceId: `fields/${fieldId}/${fieldInstanceId}`,
callsite: {
file: callsite?.getFileName(),
line: callsite?.getLineNumber()
}
},
payload: {
contentTypeId,
fieldId,
props: {
annotations: annotationIds,
fieldAnnotationPayload
}
}
}),
addItemsValidation: (
contentTypeId,
contentTypeInstanceId,
fieldId,
fieldInstanceId,
callsite,
validations
): Intents.FieldAddItemsValidation =>
new Intents.FieldAddItemsValidation({
type: 'field/addItemsValidation',
meta: {
contentTypeInstanceId: `contentType/${contentTypeId}/${contentTypeInstanceId}`,
fieldInstanceId: `fields/${fieldId}/${fieldInstanceId}`,
callsite: {
file: callsite?.getFileName(),
line: callsite?.getLineNumber()
}
},
payload: {
contentTypeId,
fieldId,
props: {
validations
}
}
})
},
tag: {
Expand Down
35 changes: 35 additions & 0 deletions src/lib/migration-steps/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,41 @@ class Field extends DispatchProxy {
)
return this
}

addAnnotation(annotationIds: string[], fieldAnnotationPayload?: Record<string, any>) {
const callsite = getFirstExternalCaller()
const fieldInstanceId = this.contentType.fieldInstanceIds.getNew(this.id)
this.contentType.dispatch(
actionCreators.field.addAnnotation(
this.contentType.id,
this.contentType.instanceId,
this.id,
fieldInstanceId,
callsite,
annotationIds,
fieldAnnotationPayload
)
)

return this
}

addItemsValidation(validations: any[]) {
const callsite = getFirstExternalCaller()
const fieldInstanceId = this.contentType.fieldInstanceIds.getNew(this.id)
this.contentType.dispatch(
actionCreators.field.addItemsValidation(
this.contentType.id,
this.contentType.instanceId,
this.id,
fieldInstanceId,
callsite,
validations
)
)

return this
}
}

class EditorLayout extends DispatchProxy {
Expand Down
65 changes: 65 additions & 0 deletions test/integration/migration.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ const deleteFieldOnContentTypeWithEditorLayout = require('../../examples/52-dele
const renameFieldOnContentTypeWithEditorLayout = require('../../examples/53-rename-field-in-content-type-with-editor-layout')
const createRichTextFieldWithValidation = require('../../examples/22-create-rich-text-field-with-validation')
const createExperienceType = require('../../examples/54-create-experience-type')
const addItemsValidation = require('../../examples/56-add-items-validation')
const addItemsValidationUpdate = require('../../examples/57-add-items-validation-update')

const { createMigrationParser } = require('../../built/lib/migration-parser')
const { DEFAULT_SIDEBAR_LIST } = require('../../built/lib/action/sidebarwidget')
Expand Down Expand Up @@ -1402,4 +1404,67 @@ describe('the migration', function () {
}
})
})

it('creates content type with items validation', async function () {
await migrator(addItemsValidation)
const ct = await request({
method: 'GET',
url: '/content_types/testModel'
})

// Verify the content type was created with all fields
expect(ct.name).to.eql('Test Model')
expect(ct.description).to.eql('A test model for addItemsValidation')

// Verify the name field
const nameField = ct.fields.find((f) => f.id === 'name')
// eslint-disable-next-line no-unused-expressions
expect(nameField).to.exist
expect(nameField.type).to.eql('Symbol')
expect(nameField.required).to.eql(true)

// Verify the tags field with its validations
const tagsField = ct.fields.find((f) => f.id === 'tags')
// eslint-disable-next-line no-unused-expressions
expect(tagsField).to.exist
expect(tagsField.type).to.eql('Array')
expect(tagsField.items.type).to.eql('Symbol')
expect(tagsField.items.validations).to.deep.include({ unique: true })
expect(tagsField.items.validations).to.deep.include({ size: { min: 1, max: 5 } })

// Verify the skills field with its validations
const skillsField = ct.fields.find((f) => f.id === 'skills')
// eslint-disable-next-line no-unused-expressions
expect(skillsField).to.exist
expect(skillsField.type).to.eql('Array')
expect(skillsField.items.type).to.eql('Symbol')
expect(skillsField.items.validations).to.deep.include({ size: { min: 1 } })

// Verify the relatedEntries field with its validations
const relatedEntriesField = ct.fields.find((f) => f.id === 'relatedEntries')
// eslint-disable-next-line no-unused-expressions
expect(relatedEntriesField).to.exist
expect(relatedEntriesField.type).to.eql('Array')
expect(relatedEntriesField.items.type).to.eql('Link')
expect(relatedEntriesField.items.linkType).to.eql('Entry')
expect(relatedEntriesField.items.validations).to.deep.include({
linkContentType: ['contentModelA', 'contentModelB']
})
})

it('updates items validation for existing field', async function () {
await migrator(addItemsValidationUpdate)
const ct = await request({
method: 'GET',
url: '/content_types/testModel'
})

// Verify the relatedEntries field now includes contentModelC in its validations
const relatedEntriesField = ct.fields.find((f) => f.id === 'relatedEntries')
// eslint-disable-next-line no-unused-expressions
expect(relatedEntriesField).to.exist
expect(relatedEntriesField.items.validations).to.deep.include({
linkContentType: ['contentModelC']
})
})
})
Loading