diff --git a/.changeset/fuzzy-roses-swim.md b/.changeset/fuzzy-roses-swim.md new file mode 100644 index 0000000000..234d79c96a --- /dev/null +++ b/.changeset/fuzzy-roses-swim.md @@ -0,0 +1,7 @@ +--- +'@backstage/catalog-model': patch +--- + +Updated `parseEntityRef` to allow `:` and `/` in the entity name. For example, parsing `'component:default/foo:bar'` will result in the name `'foo:bar'`. + +Note that only parsing `'foo:bar'` itself will result in the name `'bar'` and the entity kind `'foo'`, meaning this is a particularly nasty trap for user defined entity references. For this reason it is strongly discouraged to use names that contain these characters, and the catalog model does not allow it by default. However, this change now makes is possible to use these names if the default catalog validation is replaced, and in particular a high level of automation of the catalog population can limit issues that it might otherwise cause. diff --git a/packages/catalog-model/src/entity/ref.test.ts b/packages/catalog-model/src/entity/ref.test.ts index 6cb7612439..0945eddf6e 100644 --- a/packages/catalog-model/src/entity/ref.test.ts +++ b/packages/catalog-model/src/entity/ref.test.ts @@ -34,11 +34,24 @@ describe('ref', () => { it('rejects bad inputs', () => { expect(() => parseEntityRef(null as any)).toThrow(); expect(() => parseEntityRef(7 as any)).toThrow(); - expect(() => parseEntityRef('a:b:c')).toThrow(); - expect(() => parseEntityRef('a/b/c')).toThrow(); - expect(() => parseEntityRef('a/b:c')).toThrow(); - expect(() => parseEntityRef('a:b/c/d')).toThrow(); - expect(() => parseEntityRef('a:b/c:d')).toThrow(); + }); + + it('allows names with : and /', () => { + expect( + parseEntityRef('a:b:c', { defaultKind: 'k', defaultNamespace: 'ns' }), + ).toEqual({ kind: 'a', namespace: 'ns', name: 'b:c' }); + expect( + parseEntityRef('a/b/c', { defaultKind: 'k', defaultNamespace: 'ns' }), + ).toEqual({ kind: 'k', namespace: 'a', name: 'b/c' }); + expect( + parseEntityRef('a/b:c', { defaultKind: 'k', defaultNamespace: 'ns' }), + ).toEqual({ kind: 'k', namespace: 'a', name: 'b:c' }); + expect( + parseEntityRef('a:b/c/d', { defaultKind: 'k', defaultNamespace: 'ns' }), + ).toEqual({ kind: 'a', namespace: 'b', name: 'c/d' }); + expect( + parseEntityRef('a:b/c:d', { defaultKind: 'k', defaultNamespace: 'ns' }), + ).toEqual({ kind: 'a', namespace: 'b', name: 'c:d' }); }); it('rejects empty parts in strings', () => { diff --git a/packages/catalog-model/src/entity/ref.ts b/packages/catalog-model/src/entity/ref.ts index acede5a56b..cfe979423f 100644 --- a/packages/catalog-model/src/entity/ref.ts +++ b/packages/catalog-model/src/entity/ref.ts @@ -23,18 +23,25 @@ function parseRefString(ref: string): { namespace?: string; name: string; } { - const match = /^([^:/]+:)?([^:/]+\/)?([^:/]+)$/.exec(ref.trim()); - if (!match) { + let colonI = ref.indexOf(':'); + const slashI = ref.indexOf('/'); + + // If the / is ahead of the :, treat the rest as the name + if (slashI !== -1 && slashI < colonI) { + colonI = -1; + } + + const kind = colonI === -1 ? undefined : ref.slice(0, colonI); + const namespace = slashI === -1 ? undefined : ref.slice(colonI + 1, slashI); + const name = ref.slice(Math.max(colonI + 1, slashI + 1)); + + if (kind === '' || namespace === '' || name === '') { throw new TypeError( `Entity reference "${ref}" was not on the form [:][/]`, ); } - return { - kind: match[1]?.slice(0, -1), - namespace: match[2]?.slice(0, -1), - name: match[3], - }; + return { kind, namespace, name }; } /**