From efdfcb39c921319a15372c92cfc2b3ef9ecdabb0 Mon Sep 17 00:00:00 2001 From: Gino Valente Date: Mon, 8 May 2023 12:06:25 -0700 Subject: [PATCH] Improve GID regex and add parseGidObject --- .../admin-graphql-api-utilities/README.md | 13 ++++ .../admin-graphql-api-utilities/src/index.ts | 72 ++++++++++------- .../src/tests/index.test.ts | 77 ++++++++++++++++++- 3 files changed, 131 insertions(+), 31 deletions(-) diff --git a/packages/admin-graphql-api-utilities/README.md b/packages/admin-graphql-api-utilities/README.md index f423f41a11..a89505d844 100644 --- a/packages/admin-graphql-api-utilities/README.md +++ b/packages/admin-graphql-api-utilities/README.md @@ -27,6 +27,19 @@ parseGidType('gid://shopify/Customer/12345'); // → 'Customer' ``` +### `parseGidObject(gid: string): GidObject` + +Given a Gid string, parse the components into an object. + +#### Example Usage + +```typescript +import {parseGidObject} from '@shopify/admin-graphql-api-utilities'; + +parseGidObject('gid://shopify/Customer/12345'); +// → {namespace: 'shopify', type: 'Customer', id: '12345'} +``` + ### `function parseGid(gid: string): string` Given a Gid string, parse out the id. diff --git a/packages/admin-graphql-api-utilities/src/index.ts b/packages/admin-graphql-api-utilities/src/index.ts index 9f2ed9debb..eb6cbe6f3d 100644 --- a/packages/admin-graphql-api-utilities/src/index.ts +++ b/packages/admin-graphql-api-utilities/src/index.ts @@ -1,46 +1,64 @@ -const GID_TYPE_REGEXP = /^gid:\/\/[\w-]+\/([\w-]+)\//; -const GID_REGEXP = /\/(\w[\w-]*)(?:\?(.*))*$/; +/** + * Matches a GID and captures the following groups: + * 1. The namespace + * 2. The type + * 3. The ID + * 4. The query string (if any) + * + * @see https://regex101.com/r/5j5AXK + */ +const GID_REGEX = + /^gid:\/\/([a-zA-Z][a-zA-Z0-9-]*)\/([a-zA-Z][\w-]*)\/(\w[\w-]*)(\?.*)?$/; interface ParsedGid { id: string; params: {[key: string]: string}; } -export function parseGidType(gid: string): string { - const matches = GID_TYPE_REGEXP.exec(gid); +interface GidObject { + namespace: string; + type: string; + id: string; + queryString?: string; +} - if (matches && matches[1] !== undefined) { - return matches[1]; +/** + * Attempts to parse a string into a GID object. + * + * @throws {Error} If the string is not a valid GID. + */ +export function parseGidObject(gid: string): GidObject { + const matches = GID_REGEX.exec(gid); + + if (matches) { + return { + namespace: matches[1], + type: matches[2], + id: matches[3], + queryString: matches[4], + }; } + throw new Error(`Invalid gid: ${gid}`); } +export function parseGidType(gid: string): string { + return parseGidObject(gid).type; +} + export function parseGid(gid: string): string { - // prepends forward slash to help identify invalid id - const id = `/${gid}`; - const matches = GID_REGEXP.exec(id); - if (matches && matches[1] !== undefined) { - return matches[1]; - } - throw new Error(`Invalid gid: ${gid}`); + return parseGidObject(gid).id; } export function parseGidWithParams(gid: string): ParsedGid { - // appends forward slash to help identify invalid id - const id = `/${gid}`; - const matches = GID_REGEXP.exec(id); - if (matches && matches[1] !== undefined) { - const params = - matches[2] === undefined + const obj = parseGidObject(gid); + return { + id: obj.id, + params: + obj.queryString === undefined ? {} - : fromEntries(new URLSearchParams(matches[2]).entries()); - - return { - id: matches[1], - params, - }; - } - throw new Error(`Invalid gid: ${gid}`); + : fromEntries(new URLSearchParams(obj.queryString).entries()), + }; } export function composeGidFactory(namescape: string) { diff --git a/packages/admin-graphql-api-utilities/src/tests/index.test.ts b/packages/admin-graphql-api-utilities/src/tests/index.test.ts index c8eb39c8f6..27d8a5981e 100644 --- a/packages/admin-graphql-api-utilities/src/tests/index.test.ts +++ b/packages/admin-graphql-api-utilities/src/tests/index.test.ts @@ -8,9 +8,82 @@ import { composeGid, nodesFromEdges, keyFromEdges, + parseGidObject, } from '..'; describe('admin-graphql-api-utilities', () => { + describe('parseGidObject()', () => { + it('parses a standard GID', () => { + expect(parseGidObject('gid://shopify/Collection/123')).toStrictEqual({ + namespace: 'shopify', + type: 'Collection', + id: '123', + queryString: undefined, + }); + }); + + it('parses a GID with a non-numeric ID', () => { + expect(parseGidObject('gid://shopify/Collection/foo')).toStrictEqual({ + namespace: 'shopify', + type: 'Collection', + id: 'foo', + queryString: undefined, + }); + }); + + it('parses a GID with single-digit components', () => { + expect(parseGidObject('gid://s/C/1')).toStrictEqual({ + namespace: 's', + type: 'C', + id: '1', + queryString: undefined, + }); + }); + + it('parses a standard GID with params', () => { + expect( + parseGidObject( + 'gid://shopify/Collection/123?title=hello%sworld&tags=large+blue', + ), + ).toStrictEqual({ + namespace: 'shopify', + type: 'Collection', + id: '123', + queryString: '?title=hello%sworld&tags=large+blue', + }); + }); + + it('throws on GID with extraneous components', () => { + const gid = 'gid://shopify/A/B/C/D/E/F/G/123'; + expect(() => parseGidObject(gid)).toThrow(`Invalid gid: ${gid}`); + }); + + it('throws on GID with missing namespace', () => { + const gid = 'gid:///Collection/123'; + expect(() => parseGidObject(gid)).toThrow(`Invalid gid: ${gid}`); + }); + + it('throws on GID with missing prefix', () => { + const gid = '//shopify/Collection/123'; + expect(() => parseGidObject(gid)).toThrow(`Invalid gid: ${gid}`); + }); + + it('throws on GID with invalid prefix', () => { + const gid = '@#$%^&^*()/Foo/123'; + expect(() => parseGidObject(gid)).toThrow(`Invalid gid: ${gid}`); + }); + + it('throws on GID with spaces', () => { + const gid = 'gid://shopify/Some Collection/123'; + expect(() => parseGidObject(gid)).toThrow(`Invalid gid: ${gid}`); + }); + + it('throws on GID with invalid identifiers', () => { + const gid = 'gid://-_-_-_-/--------/__________'; + expect(() => parseGidObject(gid)).toThrow(`Invalid gid: ${gid}`); + }); + }); + describe('parseGidType()', () => { it('returns the type from a GID without param', () => { const parsedType = parseGidType( @@ -39,10 +112,6 @@ describe('admin-graphql-api-utilities', () => { ); }); - it('returns the id portion of an unprefixed gid', () => { - ['1', '1a', v4()].forEach((id) => expect(parseGid(id)).toBe(id)); - }); - it('returns the id portion of a gid for integer ids', () => { const id = '12'; const gid = `gid://shopify/Section/${id}`;