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
7 changes: 7 additions & 0 deletions .changeset/fix-shared-block-inline-names.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@portabletext/sanity-bridge': patch
---

fix: deduplicate shared block/inline object names in schema compilation

When a type name appears in both `blockObjects` and `inlineObjects` of a `SchemaDefinition`, `compileSchemaDefinitionToPortableTextMemberSchemaTypes` would pass duplicate type names to `SanitySchema.compile()`, causing a "Duplicate type name added to schema" error. This generalizes the existing temporary-name pattern (previously only handling `image` and `url`) to dynamically detect and rename any shared names before compilation, then map them back afterward.
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,68 @@ import {portableTextMemberSchemaTypesToSchema} from './portable-text-member-sche
import {compileSchemaDefinitionToPortableTextMemberSchemaTypes} from './schema-definition-to-portable-text-member-schema-types'

describe(compileSchemaDefinitionToPortableTextMemberSchemaTypes.name, () => {
test('handles type appearing in both blockObjects and inlineObjects', () => {
const schema: Schema = {
annotations: [],
block: {name: 'block'},
blockObjects: [
{
name: 'test',
fields: [{name: 'title', type: 'string', title: 'Title'}],
title: 'Test',
},
],
decorators: [],
inlineObjects: [
{
name: 'test',
fields: [{name: 'title', type: 'string', title: 'Title'}],
title: 'Test',
},
],
span: {name: 'span'},
styles: [{name: 'normal', title: 'Normal', value: 'normal'}],
lists: [],
}

expect(
portableTextMemberSchemaTypesToSchema(
compileSchemaDefinitionToPortableTextMemberSchemaTypes(schema),
),
).toEqual(schema)
})

test('back and forth with shared blockObject and inlineObject names', () => {
const schema: Schema = {
annotations: [],
block: {name: 'block'},
blockObjects: [
{
name: 'myObject',
fields: [{name: 'value', type: 'string', title: 'Value'}],
title: 'My Object',
},
],
decorators: [{name: 'strong', title: 'Bold', value: 'strong'}],
inlineObjects: [
{
name: 'myObject',
fields: [{name: 'value', type: 'string', title: 'Value'}],
title: 'My Object',
},
],
span: {name: 'span'},
styles: [{name: 'normal', title: 'Normal', value: 'normal'}],
lists: [],
}

expect(
portableTextMemberSchemaTypesToSchema(
compileSchemaDefinitionToPortableTextMemberSchemaTypes(schema),
),
).toEqual(schema)
})

test("image object doesn't get Sanity-specific fields", () => {
expect(
compileSchemaDefinitionToPortableTextMemberSchemaTypes({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,36 +14,17 @@ import {
} from './portable-text-member-schema-types'
import {startCase} from './start-case'

const temporaryImageBlockObjectName = `tmp-${keyGenerator()}-image`
const temporaryUrlBlockObjectName = `tmp-${keyGenerator()}-url`
const temporaryImageInlineObjectName = `tmp-${keyGenerator()}-image`
const temporaryUrlInlineObjectName = `tmp-${keyGenerator()}-url`

const temporaryBlockObjectNames: Record<string, string> = {
image: temporaryImageBlockObjectName,
url: temporaryUrlBlockObjectName,
}

const temporaryInlineObjectNames: Record<string, string> = {
image: temporaryImageInlineObjectName,
url: temporaryUrlInlineObjectName,
}

const blockObjectNames: Record<string, string> = {
[temporaryImageBlockObjectName]: 'image',
[temporaryUrlBlockObjectName]: 'url',
}

const inlineObjectNames: Record<string, string> = {
[temporaryImageInlineObjectName]: 'image',
[temporaryUrlInlineObjectName]: 'url',
}

const defaultObjectTitles: Record<string, string> = {
image: 'Image',
url: 'URL',
}

/**
* Names that conflict with Sanity's built-in schema types and need temporary
* names during `SanitySchema.compile` to avoid getting default fields added.
*/
const sanityBuiltinNames = new Set(['image', 'url'])

/**
* @public
* Compile a Portable Text schema definition to Sanity-specific schema types for
Expand All @@ -52,49 +33,82 @@ const defaultObjectTitles: Record<string, string> = {
export function compileSchemaDefinitionToPortableTextMemberSchemaTypes(
definition?: SchemaDefinition,
): PortableTextMemberSchemaTypes {
const blockObjects =
definition?.blockObjects?.map((blockObject) =>
defineType({
type: 'object',
// Very naive way to work around `SanitySchema.compile` adding default
// fields to objects with certain names.
name: temporaryBlockObjectNames[blockObject.name] ?? blockObject.name,
title:
blockObject.title === undefined
? // This avoids the default title which is a title case of the object name
defaultObjectTitles[blockObject.name]
: blockObject.title,
fields:
blockObject.fields?.map((field) => ({
name: field.name,
type: field.type,
title: field.title ?? startCase(field.name),
})) ?? [],
}),
) ?? []

const inlineObjects =
definition?.inlineObjects?.map((inlineObject) =>
defineType({
type: 'object',
// Very naive way to work around `SanitySchema.compile` adding default
// fields to objects with certain names.
name:
temporaryInlineObjectNames[inlineObject.name] ?? inlineObject.name,

title:
inlineObject.title === undefined
? // This avoids the default title which is a title case of the object name
defaultObjectTitles[inlineObject.name]
: inlineObject.title,
fields:
inlineObject.fields?.map((field) => ({
name: field.name,
type: field.type,
title: field.title ?? startCase(field.name),
})) ?? [],
}),
) ?? []
const blockObjectDefs = definition?.blockObjects ?? []
const inlineObjectDefs = definition?.inlineObjects ?? []

// Collect names that appear in both blockObjects and inlineObjects, or that
// conflict with Sanity built-in types. These need temporary names so that
// `SanitySchema.compile` doesn't see duplicate type registrations.
const blockObjectNameSet = new Set(
blockObjectDefs.map((blockObject) => blockObject.name),
)
const inlineObjectNameSet = new Set(
inlineObjectDefs.map((inlineObject) => inlineObject.name),
)

const temporaryBlockObjectNames: Record<string, string> = {}
const temporaryInlineObjectNames: Record<string, string> = {}
const blockObjectNames: Record<string, string> = {}
const inlineObjectNames: Record<string, string> = {}

for (const name of blockObjectNameSet) {
if (sanityBuiltinNames.has(name) || inlineObjectNameSet.has(name)) {
const tmpName = `tmp-${keyGenerator()}-${name}`
temporaryBlockObjectNames[name] = tmpName
blockObjectNames[tmpName] = name
}
}

for (const name of inlineObjectNameSet) {
if (sanityBuiltinNames.has(name) || blockObjectNameSet.has(name)) {
const tmpName = `tmp-${keyGenerator()}-${name}`
temporaryInlineObjectNames[name] = tmpName
inlineObjectNames[tmpName] = name
}
}

const blockObjects = blockObjectDefs.map((blockObject) =>
defineType({
type: 'object',
// Use temporary names to work around `SanitySchema.compile` adding
// default fields to objects with certain names, and to avoid duplicate
// type names when a type appears in both blockObjects and inlineObjects.
name: temporaryBlockObjectNames[blockObject.name] ?? blockObject.name,
title:
blockObject.title === undefined
? // This avoids the default title which is a title case of the object name
defaultObjectTitles[blockObject.name]
: blockObject.title,
fields:
blockObject.fields?.map((field) => ({
name: field.name,
type: field.type,
title: field.title ?? startCase(field.name),
})) ?? [],
}),
)

const inlineObjects = inlineObjectDefs.map((inlineObject) =>
defineType({
type: 'object',
// Use temporary names to work around `SanitySchema.compile` adding
// default fields to objects with certain names, and to avoid duplicate
// type names when a type appears in both blockObjects and inlineObjects.
name: temporaryInlineObjectNames[inlineObject.name] ?? inlineObject.name,

title:
inlineObject.title === undefined
? // This avoids the default title which is a title case of the object name
defaultObjectTitles[inlineObject.name]
: inlineObject.title,
fields:
inlineObject.fields?.map((field) => ({
name: field.name,
type: field.type,
title: field.title ?? startCase(field.name),
})) ?? [],
}),
)

const portableTextSchema = defineField({
type: 'array',
Expand Down