Skip to content
This repository has been archived by the owner on Sep 25, 2024. It is now read-only.

Commit

Permalink
Improve GID regex and add parseGidObject
Browse files Browse the repository at this point in the history
  • Loading branch information
MrGVSV committed Jan 8, 2024
1 parent 0c8a2ba commit 5c1a825
Show file tree
Hide file tree
Showing 3 changed files with 139 additions and 43 deletions.
13 changes: 13 additions & 0 deletions packages/admin-graphql-api-utilities/README.md
Original file line number Diff line number Diff line change
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
91 changes: 52 additions & 39 deletions packages/admin-graphql-api-utilities/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
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-]*)(\?.*)?$/;

export type Gid<
Namespace extends string,
Expand All @@ -13,41 +22,50 @@ interface ParsedGid {
params: {[key: string]: string};
}

export function parseGidType(gid: string): string {
const matches = GID_TYPE_REGEXP.exec(gid);
export 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 {
// prepends 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<N extends string>(namespace: N) {
Expand All @@ -73,23 +91,18 @@ export const composeGid = composeGidFactory('shopify');
export function isGidFactory<N extends string>(namespace: N) {
return function isGid<T extends string = string>(
gid: string,
key?: T,
type?: T,
): gid is Gid<N, T> {
if (!gid.startsWith(`gid://${namespace}/`)) {
return false;
}

try {
if (key !== undefined && parseGidType(gid) !== key) {
return false;
}
const obj = parseGidObject(gid);
return (
obj.namespace === namespace &&
(type === undefined || obj.type === type) &&
obj.id.length > 0
);
} catch {
return false;
}

// prepends forward slash to help identify invalid id
const id = `/${gid}`;
return GID_REGEXP.test(id);
};
}

Expand Down
78 changes: 74 additions & 4 deletions packages/admin-graphql-api-utilities/src/tests/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,82 @@ import {
keyFromEdges,
isGid,
isGidFactory,
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 @@ -42,10 +115,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 Expand Up @@ -175,6 +244,7 @@ describe('admin-graphql-api-utilities', () => {
expect(isGid('gid:/shopify/Section/123')).toBe(false);
expect(isGid('//shopify/Section/123')).toBe(false);
expect(isGid('gid://shopify/Section/123 456')).toBe(false);
expect(isGid('123')).toBe(false);
});
});

Expand Down

0 comments on commit 5c1a825

Please sign in to comment.