Skip to content
Merged
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
8 changes: 6 additions & 2 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ type FieldType =
| 'Link'
| 'ResourceLink'

type RegExpValidation = { pattern: string | RegExp; flags?: string }

export interface IFieldOptions {
newId?: string

Expand Down Expand Up @@ -170,8 +172,10 @@ export interface IValidation {
size?: { max?: number; min?: number }
/** Takes min and/or max parameters and validates the range of a value. */
range?: { max?: number; min?: number }
/** Takes a string that reflects a JS regex and flags, validates against a string. See JS reference for the parameters. */
regexp?: { pattern: string; flags?: string }
/** Takes a string or RegExp pattern and flags, validates against a string. Explicit flags override RegExp flags. See JS reference for the parameters. */
regexp?: RegExpValidation
/** Takes a string or RegExp pattern and flags, validates that a value does not match a pattern. Explicit flags override RegExp flags. */
prohibitRegexp?: RegExpValidation
/** Validates that there are no other entries that have the same field value at the time of publication. */
unique?: true
/** Validates that a value falls within a certain range of dates. */
Expand Down
55 changes: 54 additions & 1 deletion src/lib/entities/content-type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,59 @@ function prune(obj: any) {
return isEmpty ? undefined : obj
}

const regexpValidationNames = ['regexp', 'prohibitRegexp']

type FieldWithValidations = {
validations?: any[]
items?: {
type: string
linkType?: string
validations?: any[]
}
}

function serializeRegExp(validation: any): { pattern: string; flags?: string | null } {
const pattern = validation.pattern
const flags = validation.flags === undefined || validation.flags === null ? pattern.flags : validation.flags
const serializedValidation = {
...validation,
pattern: pattern.source
}

if (flags.length > 0) {
serializedValidation.flags = flags
}

return serializedValidation
}

function serializeRegExpValidations<T extends FieldWithValidations>(field: T): T {
const serializedField = { ...field }

if (serializedField.validations) {
serializedField.validations = serializedField.validations.map((validation) => {
for (const validationName of regexpValidationNames) {
const regexpValidation = validation[validationName]

if (regexpValidation?.pattern instanceof RegExp) {
return {
...validation,
[validationName]: serializeRegExp(regexpValidation)
}
}
}

return validation
})
}

if (serializedField.items) {
serializedField.items = serializeRegExpValidations(serializedField.items)
}

return serializedField
}

export type EditorLayoutFieldMovementDirection =
| 'afterField'
| 'beforeField'
Expand Down Expand Up @@ -812,7 +865,7 @@ class ContentType {
},
name: this.name,
displayField: this.displayField,
fields: this.fields.toRaw(),
fields: this.fields.toRaw().map(serializeRegExpValidations),
description: this.description
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,21 +27,15 @@ const linkMimetypeGroup = validation('linkMimetypeGroup', Joi.array().items(Joi.
const size = validation('size', range('number'))
const rangeValidation = validation('range', range('number'))

const regexp = validation(
'regexp',
const regexpValidation = () =>
Joi.object({
pattern: Joi.string(),
pattern: Joi.alternatives().try(Joi.string(), Joi.object().instance(RegExp)),
flags: Joi.string().allow(null).optional()
})
)

const prohibitRegexp = validation(
'prohibitRegexp',
Joi.object({
pattern: Joi.string(),
flags: Joi.string().allow(null).optional()
})
)
const regexp = validation('regexp', regexpValidation())

const prohibitRegexp = validation('prohibitRegexp', regexpValidation())

const unique = validation('unique', Joi.boolean())
const dateRange = validation('dateRange', rangeForDate())
Expand Down
17 changes: 17 additions & 0 deletions src/lib/offline-api/validator/schema/schema-validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ interface SimplifiedValidationError {
dupeValue?: any
key?: any
keys?: any[]
types?: string[]
valids?: any[]
value?: any
}
Expand Down Expand Up @@ -500,6 +501,22 @@ const validateFields = function (
)
}
}

if (type === 'alternatives.types') {
const actualType = kindOf(reach(field, prop))
const expectedType = context.types.includes('string')
? 'string'
: context.types.join(' or ')

return {
type: 'InvalidPayload',
message: errorMessages.field.validations.INVALID_VALIDATION_PARAMETER(
context.key,
expectedType,
actualType
)
}
}
}
})
}
Expand Down
59 changes: 59 additions & 0 deletions test/unit/lib/offline-api/request-batches.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,65 @@ describe('Payload builder', () => {
]
])
})

it('serializes RegExp validation patterns in the content type payload', async () => {
const requests = await buildPayloads(function up(migration) {
const person = migration.createContentType('person', {
name: 'Person'
})

person.createField('slug', {
name: 'Slug',
type: 'Symbol',
validations: [
{ regexp: { pattern: /^[a-z\s]+$/i } },
{ prohibitRegexp: { pattern: /admin/g } }
]
})

person.createField('aliases', {
name: 'Aliases',
type: 'Array',
items: {
type: 'Symbol',
validations: [{ regexp: { pattern: /^[a-z]+$/ } }]
}
})

person.createField('code', {
name: 'Code',
type: 'Symbol',
validations: [{ regexp: { pattern: /^[0-9]+$/i, flags: 'g' } }]
})
}, [])

expect(requests[0][0].data.fields).toEqual([
{
id: 'slug',
name: 'Slug',
type: 'Symbol',
validations: [
{ regexp: { pattern: '^[a-z\\s]+$', flags: 'i' } },
{ prohibitRegexp: { pattern: 'admin', flags: 'g' } }
]
},
{
id: 'aliases',
name: 'Aliases',
type: 'Array',
items: {
type: 'Symbol',
validations: [{ regexp: { pattern: '^[a-z]+$' } }]
}
},
{
id: 'code',
name: 'Code',
type: 'Symbol',
validations: [{ regexp: { pattern: '^[0-9]+$', flags: 'g' } }]
}
])
})
})

describe('when deleting a field', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,44 @@ describe('payload validation', () => {
expect(errors).toEqual([[]])
})

it('allows RegExp values for regexp validation patterns', async function () {
const errors = await validateBatches(function up(migration) {
const person = migration.createContentType('person').name('Person').description('A Person')

person
.createField('fullName')
.name('Full Name')
.type('Symbol')
.validations([
{ regexp: { pattern: /^[A-Za-z\s]+$/i } },
{ prohibitRegexp: { pattern: /admin/g } }
])
}, [])

expect(errors).toEqual([[]])
})

it('errors on wrong regexp pattern parameter type', async function () {
const errors = await validateBatches(function up(migration) {
const person = migration.createContentType('person').name('Person').description('A Person')

person
.createField('fullName')
.name('Full Name')
.type('Symbol')
.validations([{ regexp: { pattern: 1 } }])
}, [])

expect(errors).toEqual([
[
{
type: 'InvalidPayload',
message: `"pattern" validation expected to be "string", but got "number"`
}
]
])
})

it('can validate all blocks and inlines for RichText', async function () {
const errors = await validateBatches(function up(migration) {
const novel = migration
Expand Down
Loading