Skip to content

Commit

Permalink
Improve GID regex and add parseGidObject
Browse files Browse the repository at this point in the history
  • Loading branch information
MrGVSV committed May 8, 2023
1 parent 42b7dd3 commit efdfcb3
Show file tree
Hide file tree
Showing 3 changed files with 131 additions and 31 deletions.
13 changes: 13 additions & 0 deletions packages/admin-graphql-api-utilities/README.md
Expand Up @@ -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.
Expand Down
72 changes: 45 additions & 27 deletions 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) {
Expand Down
77 changes: 73 additions & 4 deletions packages/admin-graphql-api-utilities/src/tests/index.test.ts
Expand Up @@ -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(
Expand Down Expand Up @@ -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}`;
Expand Down

0 comments on commit efdfcb3

Please sign in to comment.