@typed/id 0.15.0 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/Cuid.d.ts +27 -0
- package/dist/Cuid.js +86 -0
- package/dist/Cuid.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/readme.md +30 -4
- package/src/Cuid.ts +135 -0
- package/src/id.test.ts +34 -29
- package/src/index.ts +1 -0
package/dist/Cuid.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Effect, Layer, Schema } from 'effect';
|
|
2
|
+
import { DateTimes } from './DateTimes.js';
|
|
3
|
+
import { GetRandomValues } from './GetRandomValues.js';
|
|
4
|
+
export declare const Cuid: Schema.brand<Schema.filter<Schema.Schema<string, string, never>>, "@typed/id/CUID">;
|
|
5
|
+
export type Cuid = Schema.Schema.Type<typeof Cuid>;
|
|
6
|
+
export declare const isCuid: (value: string) => value is Cuid;
|
|
7
|
+
export type CuidSeed = {
|
|
8
|
+
readonly timestamp: number;
|
|
9
|
+
readonly counter: number;
|
|
10
|
+
readonly random: Uint8Array;
|
|
11
|
+
readonly fingerprint: string;
|
|
12
|
+
};
|
|
13
|
+
declare const CuidState_base: import("effect/Context").TagClass<CuidState, "CuidState", {
|
|
14
|
+
readonly next: Effect.Effect<CuidSeed>;
|
|
15
|
+
}> & Effect.Tag.Proxy<CuidState, {
|
|
16
|
+
readonly next: Effect.Effect<CuidSeed>;
|
|
17
|
+
}> & {
|
|
18
|
+
use: <X>(body: (_: {
|
|
19
|
+
readonly next: Effect.Effect<CuidSeed>;
|
|
20
|
+
}) => X) => [X] extends [Effect.Effect<infer A, infer E, infer R>] ? Effect.Effect<A, E, R | CuidState> : [X] extends [PromiseLike<infer A_1>] ? Effect.Effect<A_1, import("effect/Cause").UnknownException, CuidState> : Effect.Effect<X, never, CuidState>;
|
|
21
|
+
};
|
|
22
|
+
export declare class CuidState extends CuidState_base {
|
|
23
|
+
static readonly layer: (envData: string) => Layer.Layer<CuidState, never, DateTimes | GetRandomValues>;
|
|
24
|
+
static readonly Default: Layer.Layer<CuidState, never, DateTimes | GetRandomValues>;
|
|
25
|
+
}
|
|
26
|
+
export declare const makeCuid: Effect.Effect<Cuid, never, DateTimes | GetRandomValues | CuidState>;
|
|
27
|
+
export {};
|
package/dist/Cuid.js
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { Effect, Layer, Schema } from 'effect';
|
|
2
|
+
import { DateTimes } from './DateTimes.js';
|
|
3
|
+
import { GetRandomValues } from './GetRandomValues.js';
|
|
4
|
+
// Constants
|
|
5
|
+
const DEFAULT_LENGTH = 24;
|
|
6
|
+
const BIG_LENGTH = 32;
|
|
7
|
+
const INITIAL_COUNT_MAX = 476782367;
|
|
8
|
+
// Schema
|
|
9
|
+
export const Cuid = Schema.String.pipe(Schema.pattern(/^[a-z][0-9a-z]+$/), Schema.brand('@typed/id/CUID'));
|
|
10
|
+
export const isCuid = Schema.is(Cuid);
|
|
11
|
+
// Utilities
|
|
12
|
+
const ALPHABET = Array.from({ length: 26 }, (_, i) => String.fromCharCode(i + 97));
|
|
13
|
+
const encoder = new TextEncoder();
|
|
14
|
+
function createEntropy(length, random) {
|
|
15
|
+
let entropy = '';
|
|
16
|
+
let offset = 0;
|
|
17
|
+
while (entropy.length < length) {
|
|
18
|
+
const value = random[offset];
|
|
19
|
+
entropy += Math.floor(value % 36).toString(36);
|
|
20
|
+
offset = (offset + 1) % random.length;
|
|
21
|
+
}
|
|
22
|
+
return entropy;
|
|
23
|
+
}
|
|
24
|
+
function hash(input) {
|
|
25
|
+
// Convert string to bytes
|
|
26
|
+
const data = encoder.encode(input);
|
|
27
|
+
// Create a hash using the Web Crypto API
|
|
28
|
+
return crypto.subtle.digest('SHA-512', data).then((buffer) => {
|
|
29
|
+
const view = new Uint8Array(buffer);
|
|
30
|
+
let value = 0n;
|
|
31
|
+
for (const byte of view) {
|
|
32
|
+
value = (value << 8n) + BigInt(byte);
|
|
33
|
+
}
|
|
34
|
+
// Drop the first character because it will bias the histogram to the left
|
|
35
|
+
return value.toString(36).slice(1);
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
// State Management
|
|
39
|
+
export class CuidState extends Effect.Tag('CuidState')() {
|
|
40
|
+
static layer = (envData) => Layer.effect(this, Effect.gen(function* () {
|
|
41
|
+
const { now } = yield* DateTimes;
|
|
42
|
+
const getRandomValues = yield* GetRandomValues;
|
|
43
|
+
const initialBytes = yield* getRandomValues(4);
|
|
44
|
+
const initialValue = Math.abs((initialBytes[0] << 24) |
|
|
45
|
+
(initialBytes[1] << 16) |
|
|
46
|
+
(initialBytes[2] << 8) |
|
|
47
|
+
initialBytes[3]) % INITIAL_COUNT_MAX;
|
|
48
|
+
// Create fingerprint from environment data
|
|
49
|
+
const fingerprint = yield* Effect.promise(() => hash(envData).then((h) => h.substring(0, BIG_LENGTH)));
|
|
50
|
+
let counter = initialValue;
|
|
51
|
+
return {
|
|
52
|
+
next: Effect.gen(function* () {
|
|
53
|
+
const timestamp = yield* now;
|
|
54
|
+
const random = yield* getRandomValues(32);
|
|
55
|
+
return {
|
|
56
|
+
timestamp,
|
|
57
|
+
counter: counter++,
|
|
58
|
+
random,
|
|
59
|
+
fingerprint,
|
|
60
|
+
};
|
|
61
|
+
}),
|
|
62
|
+
};
|
|
63
|
+
}));
|
|
64
|
+
static Default = this.layer('node');
|
|
65
|
+
}
|
|
66
|
+
// Core Functions
|
|
67
|
+
function cuidFromSeed({ timestamp, counter, random, fingerprint }) {
|
|
68
|
+
return Effect.gen(function* () {
|
|
69
|
+
// First letter is always a random lowercase letter from the seed
|
|
70
|
+
const firstLetter = ALPHABET[random[0] % ALPHABET.length];
|
|
71
|
+
// Convert components to base36
|
|
72
|
+
const time = timestamp.toString(36);
|
|
73
|
+
const count = counter.toString(36);
|
|
74
|
+
// Create entropy from remaining random bytes
|
|
75
|
+
const salt = createEntropy(4, random.slice(1));
|
|
76
|
+
// Hash all components together
|
|
77
|
+
const hashInput = `${time}${salt}${count}${fingerprint}`;
|
|
78
|
+
const hashed = yield* Effect.promise(() => hash(hashInput));
|
|
79
|
+
// Construct the final CUID
|
|
80
|
+
const id = `${firstLetter}${hashed.substring(0, DEFAULT_LENGTH - 1)}`;
|
|
81
|
+
return Cuid.make(id);
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
// Public API
|
|
85
|
+
export const makeCuid = Effect.flatMap(CuidState.next, cuidFromSeed);
|
|
86
|
+
//# sourceMappingURL=Cuid.js.map
|
package/dist/Cuid.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Cuid.js","sourceRoot":"","sources":["../src/Cuid.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAA;AAC9B,cAAc,sBAAsB,CAAA;AACpC,cAAc,aAAa,CAAA;AAC3B,cAAc,WAAW,CAAA;AACzB,cAAc,YAAY,CAAA;AAC1B,cAAc,YAAY,CAAA;AAC1B,cAAc,YAAY,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,WAAW,CAAA;AACzB,cAAc,gBAAgB,CAAA;AAC9B,cAAc,sBAAsB,CAAA;AACpC,cAAc,aAAa,CAAA;AAC3B,cAAc,WAAW,CAAA;AACzB,cAAc,YAAY,CAAA;AAC1B,cAAc,YAAY,CAAA;AAC1B,cAAc,YAAY,CAAA"}
|
package/package.json
CHANGED
package/readme.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @typed/id
|
|
2
2
|
|
|
3
|
-
A TypeScript library providing common ID format generation using [Effect](https://effect.website/). This package includes implementations for UUID, NanoID, and
|
|
3
|
+
A TypeScript library providing common ID format generation using [Effect](https://effect.website/). This package includes implementations for UUID, NanoID, ULID, and CUID generation with a focus on type safety and functional programming principles.
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
@@ -17,9 +17,10 @@ yarn add @typed/id effect
|
|
|
17
17
|
- 🎯 Type-safe ID generation
|
|
18
18
|
- 🔧 Built on top of Effect
|
|
19
19
|
- 🎨 Multiple ID format support:
|
|
20
|
-
- UUID (v4, v5,
|
|
20
|
+
- UUID (v4, v5, v7)
|
|
21
21
|
- NanoID
|
|
22
22
|
- ULID
|
|
23
|
+
- CUID2
|
|
23
24
|
- ⚡ Efficient and secure random value generation
|
|
24
25
|
- 📦 Zero dependencies (except Effect)
|
|
25
26
|
|
|
@@ -37,7 +38,9 @@ import {
|
|
|
37
38
|
Uuid5Namespace,
|
|
38
39
|
Sha1,
|
|
39
40
|
makeNanoId,
|
|
40
|
-
makeUlid
|
|
41
|
+
makeUlid,
|
|
42
|
+
makeCuid,
|
|
43
|
+
CuidState,
|
|
41
44
|
} from '@typed/id'
|
|
42
45
|
|
|
43
46
|
// Generate a UUID v4 (random)
|
|
@@ -80,6 +83,15 @@ await makeUlid.pipe(
|
|
|
80
83
|
Effect.runPromise
|
|
81
84
|
)
|
|
82
85
|
// Output: "01ARZ3NDEKTSV4RRFFQ69G5FAV"
|
|
86
|
+
|
|
87
|
+
// Generate a CUID
|
|
88
|
+
await makeCuid.pipe(
|
|
89
|
+
Effect.provide(CuidState.layer('my-environment')), // Provide environment fingerprint
|
|
90
|
+
Effect.provide([GetRandomValues.CryptoRandom, DateTimes.Default]),
|
|
91
|
+
Effect.flatMap(Effect.log),
|
|
92
|
+
Effect.runPromise
|
|
93
|
+
)
|
|
94
|
+
// Output: "clh3aqnd900003b64zpka3df"
|
|
83
95
|
```
|
|
84
96
|
|
|
85
97
|
## API
|
|
@@ -88,7 +100,6 @@ await makeUlid.pipe(
|
|
|
88
100
|
|
|
89
101
|
- `makeUuid4`: Generates a v4 UUID (random)
|
|
90
102
|
- `makeUuid5`: Generates a v5 UUID (SHA-1 hash of namespace + name)
|
|
91
|
-
- `makeUuid6`: Generates a v6 UUID (reordered time-based for better sorting)
|
|
92
103
|
- `makeUuid7`: Generates a v7 UUID (time-sortable)
|
|
93
104
|
|
|
94
105
|
### UUID v5 Namespaces
|
|
@@ -107,6 +118,21 @@ Pre-defined namespaces for UUID v5 generation:
|
|
|
107
118
|
|
|
108
119
|
- `makeUlid`: Generates a ULID (Universally Unique Lexicographically Sortable Identifier)
|
|
109
120
|
|
|
121
|
+
### CUID
|
|
122
|
+
|
|
123
|
+
- `makeCuid`: Generates a CUID2 (Collision-resistant Unique IDentifier)
|
|
124
|
+
- `CuidState.layer(envData)`: Creates a CUID state layer with environment fingerprint
|
|
125
|
+
- `envData`: A string identifying the environment (e.g., 'browser', 'node', 'mobile-ios')
|
|
126
|
+
- Used to help prevent collisions in distributed systems
|
|
127
|
+
- Cached and reused for efficiency
|
|
128
|
+
- Format: 24 characters, starting with a lowercase letter, followed by numbers and lowercase letters
|
|
129
|
+
- Properties:
|
|
130
|
+
- Sequential for database performance
|
|
131
|
+
- Secure from enumeration
|
|
132
|
+
- URL-safe
|
|
133
|
+
- Horizontally scalable
|
|
134
|
+
- Includes timestamp for time-based sorting
|
|
135
|
+
|
|
110
136
|
## License
|
|
111
137
|
|
|
112
138
|
MIT
|
package/src/Cuid.ts
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { Effect, Layer, Schema } from 'effect'
|
|
2
|
+
import { DateTimes } from './DateTimes.js'
|
|
3
|
+
import { GetRandomValues } from './GetRandomValues.js'
|
|
4
|
+
|
|
5
|
+
// Constants
|
|
6
|
+
const DEFAULT_LENGTH = 24
|
|
7
|
+
const BIG_LENGTH = 32
|
|
8
|
+
const INITIAL_COUNT_MAX = 476782367
|
|
9
|
+
|
|
10
|
+
// Schema
|
|
11
|
+
export const Cuid = Schema.String.pipe(
|
|
12
|
+
Schema.pattern(/^[a-z][0-9a-z]+$/),
|
|
13
|
+
Schema.brand('@typed/id/CUID'),
|
|
14
|
+
)
|
|
15
|
+
export type Cuid = Schema.Schema.Type<typeof Cuid>
|
|
16
|
+
|
|
17
|
+
export const isCuid: (value: string) => value is Cuid = Schema.is(Cuid)
|
|
18
|
+
|
|
19
|
+
// Types
|
|
20
|
+
export type CuidSeed = {
|
|
21
|
+
readonly timestamp: number
|
|
22
|
+
readonly counter: number
|
|
23
|
+
readonly random: Uint8Array
|
|
24
|
+
readonly fingerprint: string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Utilities
|
|
28
|
+
const ALPHABET = Array.from({ length: 26 }, (_, i) => String.fromCharCode(i + 97))
|
|
29
|
+
const encoder = new TextEncoder()
|
|
30
|
+
|
|
31
|
+
function createEntropy(length: number, random: Uint8Array): string {
|
|
32
|
+
let entropy = ''
|
|
33
|
+
let offset = 0
|
|
34
|
+
|
|
35
|
+
while (entropy.length < length) {
|
|
36
|
+
const value = random[offset]
|
|
37
|
+
entropy += Math.floor(value % 36).toString(36)
|
|
38
|
+
offset = (offset + 1) % random.length
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return entropy
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function hash(input: string): Promise<string> {
|
|
45
|
+
// Convert string to bytes
|
|
46
|
+
const data = encoder.encode(input)
|
|
47
|
+
|
|
48
|
+
// Create a hash using the Web Crypto API
|
|
49
|
+
return crypto.subtle.digest('SHA-512', data).then((buffer) => {
|
|
50
|
+
const view = new Uint8Array(buffer)
|
|
51
|
+
let value = 0n
|
|
52
|
+
for (const byte of view) {
|
|
53
|
+
value = (value << 8n) + BigInt(byte)
|
|
54
|
+
}
|
|
55
|
+
// Drop the first character because it will bias the histogram to the left
|
|
56
|
+
return value.toString(36).slice(1)
|
|
57
|
+
})
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// State Management
|
|
61
|
+
export class CuidState extends Effect.Tag('CuidState')<
|
|
62
|
+
CuidState,
|
|
63
|
+
{
|
|
64
|
+
readonly next: Effect.Effect<CuidSeed>
|
|
65
|
+
}
|
|
66
|
+
>() {
|
|
67
|
+
static readonly layer = (
|
|
68
|
+
envData: string,
|
|
69
|
+
): Layer.Layer<CuidState, never, DateTimes | GetRandomValues> =>
|
|
70
|
+
Layer.effect(
|
|
71
|
+
this,
|
|
72
|
+
Effect.gen(function* () {
|
|
73
|
+
const { now } = yield* DateTimes
|
|
74
|
+
const getRandomValues = yield* GetRandomValues
|
|
75
|
+
const initialBytes = yield* getRandomValues(4)
|
|
76
|
+
const initialValue =
|
|
77
|
+
Math.abs(
|
|
78
|
+
(initialBytes[0] << 24) |
|
|
79
|
+
(initialBytes[1] << 16) |
|
|
80
|
+
(initialBytes[2] << 8) |
|
|
81
|
+
initialBytes[3],
|
|
82
|
+
) % INITIAL_COUNT_MAX
|
|
83
|
+
|
|
84
|
+
// Create fingerprint from environment data
|
|
85
|
+
const fingerprint = yield* Effect.promise(() =>
|
|
86
|
+
hash(envData).then((h) => h.substring(0, BIG_LENGTH)),
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
let counter = initialValue
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
next: Effect.gen(function* () {
|
|
93
|
+
const timestamp = yield* now
|
|
94
|
+
const random = yield* getRandomValues(32)
|
|
95
|
+
return {
|
|
96
|
+
timestamp,
|
|
97
|
+
counter: counter++,
|
|
98
|
+
random,
|
|
99
|
+
fingerprint,
|
|
100
|
+
}
|
|
101
|
+
}),
|
|
102
|
+
}
|
|
103
|
+
}),
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
static readonly Default = this.layer('node')
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Core Functions
|
|
110
|
+
function cuidFromSeed({ timestamp, counter, random, fingerprint }: CuidSeed): Effect.Effect<Cuid> {
|
|
111
|
+
return Effect.gen(function* () {
|
|
112
|
+
// First letter is always a random lowercase letter from the seed
|
|
113
|
+
const firstLetter = ALPHABET[random[0] % ALPHABET.length]
|
|
114
|
+
|
|
115
|
+
// Convert components to base36
|
|
116
|
+
const time = timestamp.toString(36)
|
|
117
|
+
const count = counter.toString(36)
|
|
118
|
+
|
|
119
|
+
// Create entropy from remaining random bytes
|
|
120
|
+
const salt = createEntropy(4, random.slice(1))
|
|
121
|
+
|
|
122
|
+
// Hash all components together
|
|
123
|
+
const hashInput = `${time}${salt}${count}${fingerprint}`
|
|
124
|
+
const hashed = yield* Effect.promise(() => hash(hashInput))
|
|
125
|
+
|
|
126
|
+
// Construct the final CUID
|
|
127
|
+
const id = `${firstLetter}${hashed.substring(0, DEFAULT_LENGTH - 1)}`
|
|
128
|
+
|
|
129
|
+
return Cuid.make(id)
|
|
130
|
+
})
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Public API
|
|
134
|
+
export const makeCuid: Effect.Effect<Cuid, never, DateTimes | GetRandomValues | CuidState> =
|
|
135
|
+
Effect.flatMap(CuidState.next, cuidFromSeed)
|
package/src/id.test.ts
CHANGED
|
@@ -5,18 +5,18 @@ import {
|
|
|
5
5
|
GetRandomValues,
|
|
6
6
|
isUuid4,
|
|
7
7
|
isUuid5,
|
|
8
|
-
isUuid6,
|
|
9
8
|
isUuid7,
|
|
10
9
|
makeNanoId,
|
|
11
10
|
makeUlid,
|
|
12
11
|
makeUuid4,
|
|
13
12
|
makeUuid5,
|
|
14
|
-
makeUuid6,
|
|
15
13
|
makeUuid7,
|
|
16
14
|
Sha1,
|
|
17
15
|
Uuid5Namespace,
|
|
18
|
-
Uuid6State,
|
|
19
16
|
Uuid7State,
|
|
17
|
+
makeCuid,
|
|
18
|
+
isCuid,
|
|
19
|
+
CuidState,
|
|
20
20
|
} from './index.js'
|
|
21
21
|
|
|
22
22
|
const makeTestValues = (length: number) => {
|
|
@@ -28,19 +28,18 @@ const makeTestValues = (length: number) => {
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
const provideTestValues = flow(
|
|
31
|
-
Effect.provide(
|
|
31
|
+
Effect.provide(CuidState.layer('test')),
|
|
32
|
+
Effect.provide(Uuid7State.Default),
|
|
32
33
|
Effect.provide(Sha1.Default),
|
|
33
|
-
Effect.provide(
|
|
34
|
-
|
|
35
|
-
DateTimes.Fixed(new Date(0)),
|
|
36
|
-
]),
|
|
34
|
+
Effect.provide(GetRandomValues.layer((length) => Effect.succeed(makeTestValues(length)))),
|
|
35
|
+
Effect.provide(DateTimes.Fixed(new Date(0))),
|
|
37
36
|
)
|
|
38
37
|
|
|
39
38
|
describe(__filename, () => {
|
|
40
39
|
describe('Uuid4', () => {
|
|
41
40
|
it.effect('generates a UUID v4', () =>
|
|
42
|
-
Effect.gen(function* (
|
|
43
|
-
const id = yield*
|
|
41
|
+
Effect.gen(function* () {
|
|
42
|
+
const id = yield* makeUuid4
|
|
44
43
|
expect(id).toMatchInlineSnapshot(`"00010203-0405-4607-8809-0a0b0c0d0e0f"`)
|
|
45
44
|
expect(id.length).toEqual(36)
|
|
46
45
|
expect(isUuid4(id)).toEqual(true)
|
|
@@ -50,8 +49,8 @@ describe(__filename, () => {
|
|
|
50
49
|
|
|
51
50
|
describe('Uuid5', () => {
|
|
52
51
|
it.effect('generates a UUID v5', () =>
|
|
53
|
-
Effect.gen(function* (
|
|
54
|
-
const id = yield*
|
|
52
|
+
Effect.gen(function* () {
|
|
53
|
+
const id = yield* makeUuid5(Uuid5Namespace.DNS, 'example.com')
|
|
55
54
|
expect(id).toMatchInlineSnapshot(`"cfbff0d1-9375-5685-968c-48ce8b15ae17"`)
|
|
56
55
|
expect(id.length).toEqual(36)
|
|
57
56
|
expect(isUuid5(id)).toEqual(true)
|
|
@@ -59,21 +58,10 @@ describe(__filename, () => {
|
|
|
59
58
|
)
|
|
60
59
|
})
|
|
61
60
|
|
|
62
|
-
describe('Uuid6', () => {
|
|
63
|
-
it.effect('generates a UUID v6', () =>
|
|
64
|
-
Effect.gen(function* (_) {
|
|
65
|
-
const id = yield* _(makeUuid6)
|
|
66
|
-
expect(id).toMatchInlineSnapshot(`"1b21dd21-3814-6000-8809-0b0b0c0d0e0f"`)
|
|
67
|
-
expect(id.length).toEqual(36)
|
|
68
|
-
expect(isUuid6(id)).toEqual(true)
|
|
69
|
-
}).pipe(provideTestValues),
|
|
70
|
-
)
|
|
71
|
-
})
|
|
72
|
-
|
|
73
61
|
describe('Uuid7', () => {
|
|
74
62
|
it.effect('generates a UUID v7', () =>
|
|
75
|
-
Effect.gen(function* (
|
|
76
|
-
const id = yield*
|
|
63
|
+
Effect.gen(function* () {
|
|
64
|
+
const id = yield* makeUuid7
|
|
77
65
|
expect(id).toMatchInlineSnapshot(`"00000000-0000-7030-9c20-260b0c0d0e0f"`)
|
|
78
66
|
expect(id.length).toEqual(36)
|
|
79
67
|
expect(isUuid7(id)).toEqual(true)
|
|
@@ -83,8 +71,8 @@ describe(__filename, () => {
|
|
|
83
71
|
|
|
84
72
|
describe('NanoId', () => {
|
|
85
73
|
it.effect('generates a NanoId', () =>
|
|
86
|
-
Effect.gen(function* (
|
|
87
|
-
const id = yield*
|
|
74
|
+
Effect.gen(function* () {
|
|
75
|
+
const id = yield* makeNanoId
|
|
88
76
|
expect(id).toMatchInlineSnapshot(`"0123456789abcdefghijk"`)
|
|
89
77
|
expect(id.length).toEqual(21)
|
|
90
78
|
}).pipe(provideTestValues),
|
|
@@ -93,11 +81,28 @@ describe(__filename, () => {
|
|
|
93
81
|
|
|
94
82
|
describe('Ulid', () => {
|
|
95
83
|
it.effect('generates a Ulid', () =>
|
|
96
|
-
Effect.gen(function* (
|
|
97
|
-
const id = yield*
|
|
84
|
+
Effect.gen(function* () {
|
|
85
|
+
const id = yield* makeUlid
|
|
98
86
|
expect(id).toMatchInlineSnapshot(`"00000000000123456789ABCDEF"`)
|
|
99
87
|
expect(id.length).toEqual(26)
|
|
100
88
|
}).pipe(provideTestValues),
|
|
101
89
|
)
|
|
102
90
|
})
|
|
91
|
+
|
|
92
|
+
describe('Cuid', () => {
|
|
93
|
+
it.effect('generates a CUID', () =>
|
|
94
|
+
Effect.gen(function* () {
|
|
95
|
+
const id = yield* makeCuid
|
|
96
|
+
expect(id.length).toEqual(24)
|
|
97
|
+
expect(isCuid(id)).toEqual(true)
|
|
98
|
+
expect(id).toMatchInlineSnapshot(`"ai17q5mkkp8w5f2cey3lyzu5"`)
|
|
99
|
+
|
|
100
|
+
// Generate another to ensure uniqueness
|
|
101
|
+
const id2 = yield* makeCuid
|
|
102
|
+
expect(id2).not.toEqual(id)
|
|
103
|
+
expect(isCuid(id2)).toEqual(true)
|
|
104
|
+
expect(id2).toMatchInlineSnapshot(`"abeo5wmlmnjxjrnjiidlfvzp"`)
|
|
105
|
+
}).pipe(provideTestValues),
|
|
106
|
+
)
|
|
107
|
+
})
|
|
103
108
|
})
|
package/src/index.ts
CHANGED