Skip to content

Commit 9a5bd21

Browse files
arcatarogerstefanoverna
authored andcommitted
Add isValidId(): check if a given string is a valid Dato URL-safe Base64-encoded ID
1 parent f2ef81a commit 9a5bd21

File tree

4 files changed

+102
-34
lines changed

4 files changed

+102
-34
lines changed
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { generateId, isValidId } from '../src/idUtils';
2+
3+
describe('generateId', () => {
4+
it('expects an ID from generateId() to validate as a DatoCMS ID', () => {
5+
expect(isValidId(generateId())).toBe(true);
6+
});
7+
});
8+
9+
describe('isValidId', () => {
10+
it('checks if passed string is a valid DatoCMS ID', () => {
11+
expect(isValidId('')).toBe(false);
12+
expect(isValidId('foobar')).toBe(false);
13+
expect(isValidId('WTyssHtyTzu9_EbszSVhPw')).toBe(true);
14+
});
15+
});

packages/cma-client/src/generateId.ts

Lines changed: 0 additions & 29 deletions
This file was deleted.

packages/cma-client/src/idUtils.ts

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import { v4 } from "uuid";
2+
3+
const decoder = new TextDecoder("utf8");
4+
5+
function fromUint8ArrayToUrlSafeBase64(bytes: Uint8Array) {
6+
const base64 =
7+
typeof Buffer === "undefined"
8+
? btoa(decoder.decode(bytes))
9+
: Buffer.from(bytes).toString("base64");
10+
11+
// Convert to URL-safe format (see RFC 4648, sec. 5)
12+
return (
13+
base64
14+
// Replace + with -
15+
.replace(/\+/g, "-")
16+
// Replace / with _
17+
.replace(/\//g, "_")
18+
// Drop '==' padding
19+
.substring(0, 22)
20+
);
21+
}
22+
23+
function fromUrlSafeBase64toUint8Array(urlSafeBase64: string): Uint8Array {
24+
// Convert from URL-safe format (see RFC 4648, sec. 5)
25+
const base64 = urlSafeBase64
26+
// Replace - with +
27+
.replace(/-/g, "+")
28+
// Replace _ with /
29+
.replace(/_/g, "/");
30+
31+
return typeof Buffer === "undefined"
32+
? Uint8Array.from(atob(base64), (c) => c.charCodeAt(0))
33+
: new Uint8Array(Buffer.from(base64, "base64"));
34+
}
35+
36+
export function isValidId(id: string) {
37+
const bytes = fromUrlSafeBase64toUint8Array(id);
38+
39+
if (bytes.length !== 16) {
40+
return false;
41+
}
42+
43+
// The variant field determines the layout of the UUID (see RFC 4122,
44+
// sec. 4.1.1)
45+
46+
const variant = bytes.at(8)!;
47+
48+
// Variant must be the one described in RFC 4122
49+
if ((variant & 0b11000000) !== 0b10000000) {
50+
return false;
51+
}
52+
53+
// The version number is in the most significant 4 bits of the time
54+
// stamp (see RFC 4122, sec. 4.1.3)
55+
56+
const version = bytes.at(6)! >> 4;
57+
58+
// Version number must be 4 (randomly or pseudo-randomly generated)
59+
if (version !== 0x4) {
60+
return false;
61+
}
62+
63+
return true;
64+
}
65+
66+
export function generateId() {
67+
const bytes = v4(null, new Uint8Array(16));
68+
69+
// Here we unset the first bit to ensure [A-Za-f] as the first char.
70+
//
71+
// If we didn't do this, we would generate IDs that, once encoded
72+
// in base64, could start with a '+' or a '/'. This makes them less
73+
// easy to copy/paste, with bad DX.
74+
75+
// This choice is purely aesthetic: definitely non-mandatory!
76+
77+
bytes[0] = bytes[0]! & 0x7f;
78+
79+
const base64 = fromUint8ArrayToUrlSafeBase64(bytes);
80+
81+
return base64;
82+
}

packages/cma-client/src/index.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
export { ApiError, TimeoutError, LogLevel } from '@datocms/rest-client-utils';
1+
export { ApiError, LogLevel, TimeoutError } from '@datocms/rest-client-utils';
2+
export * from './buildBlockRecord';
3+
export * from './buildClient';
24
export { Client } from './generated/Client';
3-
export * as Resources from './generated/resources';
45
export type { ClientConfigOptions } from './generated/Client';
5-
export * from './buildClient';
6-
export * from './buildBlockRecord';
7-
export * from './generateId';
6+
export * as Resources from './generated/resources';
87
export * as SchemaTypes from './generated/SchemaTypes';
98
export * as SimpleSchemaTypes from './generated/SimpleSchemaTypes';
9+
export * from './idUtils';

0 commit comments

Comments
 (0)