@typed/id 1.0.0-beta.3 → 1.0.0-beta.5
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/README.md +171 -65
- package/dist/Cuid.d.ts +128 -13
- package/dist/Cuid.d.ts.map +1 -1
- package/dist/Cuid.js +135 -41
- package/dist/DateTimes.d.ts +66 -3
- package/dist/DateTimes.d.ts.map +1 -1
- package/dist/DateTimes.js +72 -5
- package/dist/Ids.d.ts +173 -30
- package/dist/Ids.d.ts.map +1 -1
- package/dist/Ids.js +162 -19
- package/dist/Ksuid.d.ts +51 -1
- package/dist/Ksuid.d.ts.map +1 -1
- package/dist/Ksuid.js +59 -7
- package/dist/NanoId.d.ts +47 -0
- package/dist/NanoId.d.ts.map +1 -1
- package/dist/NanoId.js +48 -1
- package/dist/RandomValues.d.ts +63 -4
- package/dist/RandomValues.d.ts.map +1 -1
- package/dist/RandomValues.js +74 -10
- package/dist/Ulid.d.ts +49 -1
- package/dist/Ulid.d.ts.map +1 -1
- package/dist/Ulid.js +54 -4
- package/dist/Uuid4.d.ts +47 -0
- package/dist/Uuid4.d.ts.map +1 -1
- package/dist/Uuid4.js +48 -1
- package/dist/Uuid5.d.ts +178 -6
- package/dist/Uuid5.d.ts.map +1 -1
- package/dist/Uuid5.js +152 -15
- package/dist/Uuid7.d.ts +121 -15
- package/dist/Uuid7.d.ts.map +1 -1
- package/dist/Uuid7.js +124 -16
- package/dist/__tests__/helpers.d.ts +7 -0
- package/dist/__tests__/helpers.d.ts.map +1 -0
- package/dist/__tests__/helpers.js +19 -0
- package/dist/__tests__/public-contract.type-test.d.ts +2 -0
- package/dist/__tests__/public-contract.type-test.d.ts.map +1 -0
- package/dist/__tests__/public-contract.type-test.js +3 -0
- package/dist/_sha.d.ts +32 -0
- package/dist/_sha.d.ts.map +1 -1
- package/dist/_sha.js +32 -0
- package/dist/_uuid-stringify.d.ts +15 -0
- package/dist/_uuid-stringify.d.ts.map +1 -1
- package/dist/_uuid-stringify.js +15 -0
- package/package.json +65 -11
- package/src/Cuid.ts +0 -124
- package/src/DateTimes.ts +0 -36
- package/src/Id.test.ts +0 -230
- package/src/Ids.ts +0 -128
- package/src/Ksuid.ts +0 -74
- package/src/NanoId.ts +0 -33
- package/src/RandomValues.ts +0 -33
- package/src/Ulid.ts +0 -51
- package/src/Uuid4.ts +0 -24
- package/src/Uuid5.ts +0 -71
- package/src/Uuid7.ts +0 -104
- package/src/_sha.ts +0 -11
- package/src/_uuid-stringify.ts +0 -30
- package/src/index.ts +0 -10
|
@@ -1,2 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Formats the first 16 bytes of a seed as a lowercase hyphenated UUID string.
|
|
3
|
+
* @remarks
|
|
4
|
+
* ## Why
|
|
5
|
+
* This published low-level helper centralizes UUID byte layout formatting but deliberately does not validate length, version, or variant bits.
|
|
6
|
+
* ## Ownership and lifetime
|
|
7
|
+
* This pure function acquires no resources, does not mutate or retain the seed, and returns a new string.
|
|
8
|
+
* @example
|
|
9
|
+
* ```ts
|
|
10
|
+
* import { uuidStringify } from "@typed/id/_uuid-stringify"
|
|
11
|
+
* const text = uuidStringify(new Uint8Array(16))
|
|
12
|
+
* ```
|
|
13
|
+
* @category Formatting
|
|
14
|
+
* @since 1.0.0
|
|
15
|
+
*/
|
|
1
16
|
export declare function uuidStringify(seed: Uint8Array): string;
|
|
2
17
|
//# sourceMappingURL=_uuid-stringify.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"_uuid-stringify.d.ts","sourceRoot":"","sources":["../src/_uuid-stringify.ts"],"names":[],"mappings":"AAMA,wBAAgB,aAAa,CAAC,IAAI,EAAE,UAAU,GAAG,MAAM,CAuBtD"}
|
|
1
|
+
{"version":3,"file":"_uuid-stringify.d.ts","sourceRoot":"","sources":["../src/_uuid-stringify.ts"],"names":[],"mappings":"AAMA;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,UAAU,GAAG,MAAM,CAuBtD"}
|
package/dist/_uuid-stringify.js
CHANGED
|
@@ -2,6 +2,21 @@ const byteToHex = [];
|
|
|
2
2
|
for (let i = 0; i < 256; ++i) {
|
|
3
3
|
byteToHex.push((i + 0x100).toString(16).slice(1));
|
|
4
4
|
}
|
|
5
|
+
/**
|
|
6
|
+
* Formats the first 16 bytes of a seed as a lowercase hyphenated UUID string.
|
|
7
|
+
* @remarks
|
|
8
|
+
* ## Why
|
|
9
|
+
* This published low-level helper centralizes UUID byte layout formatting but deliberately does not validate length, version, or variant bits.
|
|
10
|
+
* ## Ownership and lifetime
|
|
11
|
+
* This pure function acquires no resources, does not mutate or retain the seed, and returns a new string.
|
|
12
|
+
* @example
|
|
13
|
+
* ```ts
|
|
14
|
+
* import { uuidStringify } from "@typed/id/_uuid-stringify"
|
|
15
|
+
* const text = uuidStringify(new Uint8Array(16))
|
|
16
|
+
* ```
|
|
17
|
+
* @category Formatting
|
|
18
|
+
* @since 1.0.0
|
|
19
|
+
*/
|
|
5
20
|
export function uuidStringify(seed) {
|
|
6
21
|
return [
|
|
7
22
|
byteToHex[seed[0]],
|
package/package.json
CHANGED
|
@@ -1,33 +1,87 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@typed/id",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.5",
|
|
4
|
+
"description": "Branded ID generators and facades for Typed applications.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"homepage": "https://github.com/TylorS/typed-smol/tree/main/packages/id#readme",
|
|
7
|
+
"bugs": "https://github.com/TylorS/typed-smol/issues",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/TylorS/typed-smol.git",
|
|
11
|
+
"directory": "packages/id"
|
|
12
|
+
},
|
|
4
13
|
"files": [
|
|
5
|
-
"dist"
|
|
6
|
-
"src"
|
|
14
|
+
"dist"
|
|
7
15
|
],
|
|
8
16
|
"type": "module",
|
|
17
|
+
"sideEffects": false,
|
|
9
18
|
"exports": {
|
|
10
19
|
".": {
|
|
11
20
|
"types": "./dist/index.d.ts",
|
|
12
21
|
"import": "./dist/index.js"
|
|
13
22
|
},
|
|
14
|
-
"
|
|
15
|
-
"types": "./dist
|
|
16
|
-
"import": "./dist
|
|
23
|
+
"./Cuid": {
|
|
24
|
+
"types": "./dist/Cuid.d.ts",
|
|
25
|
+
"import": "./dist/Cuid.js"
|
|
26
|
+
},
|
|
27
|
+
"./DateTimes": {
|
|
28
|
+
"types": "./dist/DateTimes.d.ts",
|
|
29
|
+
"import": "./dist/DateTimes.js"
|
|
30
|
+
},
|
|
31
|
+
"./Ids": {
|
|
32
|
+
"types": "./dist/Ids.d.ts",
|
|
33
|
+
"import": "./dist/Ids.js"
|
|
34
|
+
},
|
|
35
|
+
"./Ksuid": {
|
|
36
|
+
"types": "./dist/Ksuid.d.ts",
|
|
37
|
+
"import": "./dist/Ksuid.js"
|
|
38
|
+
},
|
|
39
|
+
"./NanoId": {
|
|
40
|
+
"types": "./dist/NanoId.d.ts",
|
|
41
|
+
"import": "./dist/NanoId.js"
|
|
42
|
+
},
|
|
43
|
+
"./RandomValues": {
|
|
44
|
+
"types": "./dist/RandomValues.d.ts",
|
|
45
|
+
"import": "./dist/RandomValues.js"
|
|
46
|
+
},
|
|
47
|
+
"./Ulid": {
|
|
48
|
+
"types": "./dist/Ulid.d.ts",
|
|
49
|
+
"import": "./dist/Ulid.js"
|
|
50
|
+
},
|
|
51
|
+
"./Uuid4": {
|
|
52
|
+
"types": "./dist/Uuid4.d.ts",
|
|
53
|
+
"import": "./dist/Uuid4.js"
|
|
54
|
+
},
|
|
55
|
+
"./Uuid5": {
|
|
56
|
+
"types": "./dist/Uuid5.d.ts",
|
|
57
|
+
"import": "./dist/Uuid5.js"
|
|
58
|
+
},
|
|
59
|
+
"./Uuid7": {
|
|
60
|
+
"types": "./dist/Uuid7.d.ts",
|
|
61
|
+
"import": "./dist/Uuid7.js"
|
|
62
|
+
},
|
|
63
|
+
"./_sha": {
|
|
64
|
+
"types": "./dist/_sha.d.ts",
|
|
65
|
+
"import": "./dist/_sha.js"
|
|
66
|
+
},
|
|
67
|
+
"./_uuid-stringify": {
|
|
68
|
+
"types": "./dist/_uuid-stringify.d.ts",
|
|
69
|
+
"import": "./dist/_uuid-stringify.js"
|
|
17
70
|
}
|
|
18
71
|
},
|
|
19
72
|
"publishConfig": {
|
|
20
73
|
"access": "public"
|
|
21
74
|
},
|
|
22
75
|
"dependencies": {
|
|
23
|
-
"effect": "4.0.0-
|
|
76
|
+
"effect": "4.0.0-rc.112"
|
|
24
77
|
},
|
|
25
78
|
"devDependencies": {
|
|
26
|
-
"typescript": "
|
|
27
|
-
"vitest": "
|
|
79
|
+
"typescript": "7.0.2",
|
|
80
|
+
"vitest": "5.0.0"
|
|
28
81
|
},
|
|
29
82
|
"scripts": {
|
|
30
|
-
"build": "
|
|
31
|
-
"test": "vitest run
|
|
83
|
+
"build": "node -e \"const fs=require('node:fs'); fs.rmSync('dist',{recursive:true,force:true}); fs.rmSync('tsconfig.tsbuildinfo',{force:true})\" && tsc",
|
|
84
|
+
"test": "vitest run",
|
|
85
|
+
"test:types": "tsc -p tsconfig.type-tests.json && tsc -p tsconfig.tests.json"
|
|
32
86
|
}
|
|
33
87
|
}
|
package/src/Cuid.ts
DELETED
|
@@ -1,124 +0,0 @@
|
|
|
1
|
-
import * as Effect from "effect/Effect";
|
|
2
|
-
import * as Layer from "effect/Layer";
|
|
3
|
-
import * as Schema from "effect/Schema";
|
|
4
|
-
import * as ServiceMap from "effect/ServiceMap";
|
|
5
|
-
import { sha512 } from "./_sha.js";
|
|
6
|
-
import { DateTimes } from "./DateTimes.js";
|
|
7
|
-
import { RandomValues } from "./RandomValues.js";
|
|
8
|
-
|
|
9
|
-
// Constants
|
|
10
|
-
const DEFAULT_LENGTH = 24;
|
|
11
|
-
const BIG_LENGTH = 32;
|
|
12
|
-
const INITIAL_COUNT_MAX = 476782367;
|
|
13
|
-
|
|
14
|
-
// Schema
|
|
15
|
-
export const Cuid = Schema.String.pipe(
|
|
16
|
-
Schema.check(Schema.isPattern(/^[a-z][0-9a-z]+$/)),
|
|
17
|
-
Schema.brand("@typed/id/CUID"),
|
|
18
|
-
);
|
|
19
|
-
export type Cuid = Schema.Schema.Type<typeof Cuid>;
|
|
20
|
-
|
|
21
|
-
export const isCuid: (value: string) => value is Cuid = Schema.is(Cuid);
|
|
22
|
-
|
|
23
|
-
// Types
|
|
24
|
-
export type CuidSeed = {
|
|
25
|
-
readonly timestamp: number;
|
|
26
|
-
readonly counter: number;
|
|
27
|
-
readonly random: Uint8Array;
|
|
28
|
-
readonly fingerprint: string;
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
export class CuidState extends ServiceMap.Service<CuidState>()("@typed/id/CuidState", {
|
|
32
|
-
make: (envData: string) =>
|
|
33
|
-
Effect.gen(function* () {
|
|
34
|
-
const { now } = yield* DateTimes;
|
|
35
|
-
const getRandomValues = yield* RandomValues;
|
|
36
|
-
const initialBytes = yield* getRandomValues(4);
|
|
37
|
-
const initialValue =
|
|
38
|
-
Math.abs(
|
|
39
|
-
(initialBytes[0] << 24) |
|
|
40
|
-
(initialBytes[1] << 16) |
|
|
41
|
-
(initialBytes[2] << 8) |
|
|
42
|
-
initialBytes[3],
|
|
43
|
-
) % INITIAL_COUNT_MAX;
|
|
44
|
-
|
|
45
|
-
// Create fingerprint from environment data
|
|
46
|
-
const fingerprint = (yield* hash(envData)).substring(0, BIG_LENGTH);
|
|
47
|
-
|
|
48
|
-
let counter = initialValue;
|
|
49
|
-
|
|
50
|
-
return Effect.gen(function* () {
|
|
51
|
-
const timestamp = yield* now;
|
|
52
|
-
const random = yield* getRandomValues(32);
|
|
53
|
-
return {
|
|
54
|
-
timestamp,
|
|
55
|
-
counter: counter++,
|
|
56
|
-
random,
|
|
57
|
-
fingerprint,
|
|
58
|
-
} satisfies CuidSeed;
|
|
59
|
-
});
|
|
60
|
-
}),
|
|
61
|
-
}) {
|
|
62
|
-
static readonly next = Effect.flatten(CuidState.asEffect());
|
|
63
|
-
|
|
64
|
-
static readonly Default = Layer.effect(CuidState, CuidState.make("node")).pipe(
|
|
65
|
-
Layer.provideMerge([DateTimes.Default, RandomValues.Default]),
|
|
66
|
-
);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
export const cuid: Effect.Effect<Cuid, never, CuidState> = Effect.flatMap(
|
|
70
|
-
CuidState.next,
|
|
71
|
-
cuidFromSeed,
|
|
72
|
-
);
|
|
73
|
-
|
|
74
|
-
// Utilities
|
|
75
|
-
const ALPHABET = Array.from({ length: 26 }, (_, i) => String.fromCharCode(i + 97));
|
|
76
|
-
const encoder = new TextEncoder();
|
|
77
|
-
|
|
78
|
-
function createEntropy(length: number, random: Uint8Array): string {
|
|
79
|
-
let entropy = "";
|
|
80
|
-
let offset = 0;
|
|
81
|
-
|
|
82
|
-
while (entropy.length < length) {
|
|
83
|
-
const value = random[offset];
|
|
84
|
-
entropy += Math.floor(value % 36).toString(36);
|
|
85
|
-
offset = (offset + 1) % random.length;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
return entropy;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function hash(input: string): Effect.Effect<string> {
|
|
92
|
-
return Effect.map(sha512(encoder.encode(input)), (buffer) => {
|
|
93
|
-
const view = new Uint8Array(buffer);
|
|
94
|
-
let value = 0n;
|
|
95
|
-
for (const byte of view) {
|
|
96
|
-
value = (value << 8n) + BigInt(byte);
|
|
97
|
-
}
|
|
98
|
-
// Drop the first character because it will bias the histogram to the left
|
|
99
|
-
return value.toString(36).slice(1);
|
|
100
|
-
});
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
function cuidFromSeed({ counter, fingerprint, random, timestamp }: CuidSeed): Effect.Effect<Cuid> {
|
|
104
|
-
return Effect.gen(function* () {
|
|
105
|
-
// First letter is always a random lowercase letter from the seed
|
|
106
|
-
const firstLetter = ALPHABET[random[0] % ALPHABET.length];
|
|
107
|
-
|
|
108
|
-
// Convert components to base36
|
|
109
|
-
const time = timestamp.toString(36);
|
|
110
|
-
const count = counter.toString(36);
|
|
111
|
-
|
|
112
|
-
// Create entropy from remaining random bytes
|
|
113
|
-
const salt = createEntropy(4, random.slice(1));
|
|
114
|
-
|
|
115
|
-
// Hash all components together
|
|
116
|
-
const hashInput = `${time}${salt}${count}${fingerprint}`;
|
|
117
|
-
const hashed = yield* hash(hashInput);
|
|
118
|
-
|
|
119
|
-
// Construct the final CUID
|
|
120
|
-
const id = `${firstLetter}${hashed.substring(0, DEFAULT_LENGTH - 1)}`;
|
|
121
|
-
|
|
122
|
-
return Cuid.makeUnsafe(id);
|
|
123
|
-
});
|
|
124
|
-
}
|
package/src/DateTimes.ts
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import * as Clock from "effect/Clock";
|
|
2
|
-
import * as Effect from "effect/Effect";
|
|
3
|
-
import * as Layer from "effect/Layer";
|
|
4
|
-
import * as ServiceMap from "effect/ServiceMap";
|
|
5
|
-
|
|
6
|
-
export class DateTimes extends ServiceMap.Service<DateTimes>()("@typed/id/DateTimes", {
|
|
7
|
-
make: Effect.succeed({
|
|
8
|
-
now: Effect.sync(() => Date.now()),
|
|
9
|
-
date: Effect.sync(() => new Date()),
|
|
10
|
-
}),
|
|
11
|
-
}) {
|
|
12
|
-
static readonly now = Effect.flatMap(DateTimes.asEffect(), ({ now }) => now);
|
|
13
|
-
static readonly date = Effect.flatMap(DateTimes.asEffect(), ({ date }) => date);
|
|
14
|
-
|
|
15
|
-
static readonly Default = Layer.effect(DateTimes, DateTimes.make);
|
|
16
|
-
|
|
17
|
-
static readonly Fixed = (baseDate: number | string | Date) =>
|
|
18
|
-
Layer.effect(
|
|
19
|
-
DateTimes,
|
|
20
|
-
Effect.gen(function* () {
|
|
21
|
-
const clock = yield* Clock.Clock;
|
|
22
|
-
const base = new Date(baseDate);
|
|
23
|
-
const baseN = BigInt(base.getTime());
|
|
24
|
-
const startMillis = yield* clock.currentTimeMillis;
|
|
25
|
-
const now = clock.currentTimeMillis.pipe(
|
|
26
|
-
Effect.map((millis) =>
|
|
27
|
-
// Use BigInt to avoid floating point precision issues which can break deterministic testing
|
|
28
|
-
Number(baseN + BigInt(millis) - BigInt(startMillis)),
|
|
29
|
-
),
|
|
30
|
-
);
|
|
31
|
-
const date = now.pipe(Effect.map((millis) => new Date(millis)));
|
|
32
|
-
|
|
33
|
-
return DateTimes.of({ now, date });
|
|
34
|
-
}),
|
|
35
|
-
);
|
|
36
|
-
}
|
package/src/Id.test.ts
DELETED
|
@@ -1,230 +0,0 @@
|
|
|
1
|
-
import * as Effect from "effect/Effect";
|
|
2
|
-
import * as FastCheck from "effect/testing/FastCheck";
|
|
3
|
-
import { describe, expect, it } from "vitest";
|
|
4
|
-
import * as Cuid from "./Cuid.js";
|
|
5
|
-
import { Ids } from "./Ids.js";
|
|
6
|
-
import * as Ksuid from "./Ksuid.js";
|
|
7
|
-
import * as NanoId from "./NanoId.js";
|
|
8
|
-
import * as Ulid from "./Ulid.js";
|
|
9
|
-
import * as Uuid4 from "./Uuid4.js";
|
|
10
|
-
import * as Uuid5 from "./Uuid5.js";
|
|
11
|
-
import * as Uuid7 from "./Uuid7.js";
|
|
12
|
-
import { Random } from "effect";
|
|
13
|
-
|
|
14
|
-
const run = <A, E>(effect: Effect.Effect<A, E, Ids>) =>
|
|
15
|
-
Effect.runPromise(Effect.provide(effect, Ids.Test()));
|
|
16
|
-
|
|
17
|
-
describe("@typed/id", () => {
|
|
18
|
-
describe("type guards (property-based)", () => {
|
|
19
|
-
describe("isUuid4", () => {
|
|
20
|
-
it("accepts any UUID v4", () => {
|
|
21
|
-
FastCheck.assert(
|
|
22
|
-
FastCheck.property(FastCheck.uuid({ version: 4 }), (s) => Uuid4.isUuid4(s)),
|
|
23
|
-
);
|
|
24
|
-
});
|
|
25
|
-
it("rejects non-UUID-v4 strings", () => {
|
|
26
|
-
const invalid = FastCheck.oneof(
|
|
27
|
-
FastCheck.constant(""),
|
|
28
|
-
FastCheck.string({ maxLength: 35 }).filter((s) => s.length < 36),
|
|
29
|
-
FastCheck.string().filter(
|
|
30
|
-
(s) =>
|
|
31
|
-
!/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(s),
|
|
32
|
-
),
|
|
33
|
-
);
|
|
34
|
-
FastCheck.assert(FastCheck.property(invalid, (s) => !Uuid4.isUuid4(s)));
|
|
35
|
-
});
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
describe("isUuid5", () => {
|
|
39
|
-
it("accepts any UUID v5", () => {
|
|
40
|
-
FastCheck.assert(
|
|
41
|
-
FastCheck.property(FastCheck.uuid({ version: 5 }), (s) => Uuid5.isUuid5(s)),
|
|
42
|
-
);
|
|
43
|
-
});
|
|
44
|
-
it("rejects non-UUID-v5 strings", () => {
|
|
45
|
-
const invalid = FastCheck.oneof(
|
|
46
|
-
FastCheck.constant(""),
|
|
47
|
-
FastCheck.string().filter(
|
|
48
|
-
(s) =>
|
|
49
|
-
!/^[0-9a-f]{8}-[0-9a-f]{4}-5[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(s),
|
|
50
|
-
),
|
|
51
|
-
);
|
|
52
|
-
FastCheck.assert(FastCheck.property(invalid, (s) => !Uuid5.isUuid5(s)));
|
|
53
|
-
});
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
describe("isUuid7", () => {
|
|
57
|
-
it("accepts any UUID v7", () => {
|
|
58
|
-
FastCheck.assert(
|
|
59
|
-
FastCheck.property(FastCheck.uuid({ version: 7 }), (s) => Uuid7.isUuid7(s)),
|
|
60
|
-
);
|
|
61
|
-
});
|
|
62
|
-
it("rejects non-UUID-v7 strings", () => {
|
|
63
|
-
const invalid = FastCheck.oneof(
|
|
64
|
-
FastCheck.constant(""),
|
|
65
|
-
FastCheck.string().filter(
|
|
66
|
-
(s) =>
|
|
67
|
-
!/^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(s),
|
|
68
|
-
),
|
|
69
|
-
);
|
|
70
|
-
FastCheck.assert(FastCheck.property(invalid, (s) => !Uuid7.isUuid7(s)));
|
|
71
|
-
});
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
describe("isCuid", () => {
|
|
75
|
-
it("accepts any CUID-like string (lowercase letter + base36)", () => {
|
|
76
|
-
const cuidArb = FastCheck.stringMatching(/^[a-z][0-9a-z]+$/);
|
|
77
|
-
FastCheck.assert(FastCheck.property(cuidArb, (s) => Cuid.isCuid(s)));
|
|
78
|
-
});
|
|
79
|
-
it("rejects strings that do not match CUID pattern", () => {
|
|
80
|
-
const invalid = FastCheck.oneof(
|
|
81
|
-
FastCheck.constant(""),
|
|
82
|
-
FastCheck.string().filter((s) => !/^[a-z][0-9a-z]+$/.test(s)),
|
|
83
|
-
);
|
|
84
|
-
FastCheck.assert(FastCheck.property(invalid, (s) => !Cuid.isCuid(s)));
|
|
85
|
-
});
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
describe("isKsuid", () => {
|
|
89
|
-
it("accepts any 27-char base62 string", () => {
|
|
90
|
-
const ksuidArb = FastCheck.stringMatching(/^[0-9A-Za-z]{27}$/);
|
|
91
|
-
FastCheck.assert(FastCheck.property(ksuidArb, (s) => Ksuid.isKsuid(s)));
|
|
92
|
-
});
|
|
93
|
-
it("rejects non-KSUID strings", () => {
|
|
94
|
-
const invalid = FastCheck.oneof(
|
|
95
|
-
FastCheck.constant(""),
|
|
96
|
-
FastCheck.string().filter((s) => s.length !== 27 || !/^[0-9A-Za-z]{27}$/.test(s)),
|
|
97
|
-
);
|
|
98
|
-
FastCheck.assert(FastCheck.property(invalid, (s) => !Ksuid.isKsuid(s)));
|
|
99
|
-
});
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
describe("isNanoId", () => {
|
|
103
|
-
it("accepts any string with only 0-9a-zA-Z_-", () => {
|
|
104
|
-
const nanoIdArb = FastCheck.stringMatching(/^[0-9a-zA-Z_-]+$/);
|
|
105
|
-
FastCheck.assert(FastCheck.property(nanoIdArb, (s) => NanoId.isNanoId(s)));
|
|
106
|
-
});
|
|
107
|
-
it("rejects strings with invalid characters", () => {
|
|
108
|
-
const invalid = FastCheck.oneof(
|
|
109
|
-
FastCheck.constant(""),
|
|
110
|
-
FastCheck.string().filter((s) => !/^[0-9a-zA-Z_-]+$/.test(s)),
|
|
111
|
-
);
|
|
112
|
-
FastCheck.assert(FastCheck.property(invalid, (s) => !NanoId.isNanoId(s)));
|
|
113
|
-
});
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
describe("isUlid", () => {
|
|
117
|
-
it("accepts any valid ULID", () => {
|
|
118
|
-
FastCheck.assert(FastCheck.property(FastCheck.ulid(), (s) => Ulid.isUlid(s)));
|
|
119
|
-
});
|
|
120
|
-
it("rejects non-ULID strings", () => {
|
|
121
|
-
const invalid = FastCheck.oneof(
|
|
122
|
-
FastCheck.constant(""),
|
|
123
|
-
FastCheck.string().filter(
|
|
124
|
-
(s) => s.length !== 26 || !/^[0-9A-HJKMNP-TV-Za-hjkmnp-tv-z]{26}$/.test(s),
|
|
125
|
-
),
|
|
126
|
-
);
|
|
127
|
-
FastCheck.assert(FastCheck.property(invalid, (s) => !Ulid.isUlid(s)));
|
|
128
|
-
});
|
|
129
|
-
});
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
describe("Ids service", () => {
|
|
133
|
-
it("uuid4 produces valid UUID v4", async () => {
|
|
134
|
-
const id = await run(Ids.uuid4);
|
|
135
|
-
expect(Uuid4.isUuid4(id)).toBe(true);
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
it("uuid5 produces valid UUID v5", async () => {
|
|
139
|
-
const id = await run(Ids.uuid5("hello", Uuid5.Uuid5Namespace.DNS));
|
|
140
|
-
expect(Uuid5.isUuid5(id)).toBe(true);
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
it("uuid5 is deterministic for same name and namespace", async () => {
|
|
144
|
-
const a = await run(Ids.uuid5("test", Uuid5.Uuid5Namespace.URL));
|
|
145
|
-
const b = await run(Ids.uuid5("test", Uuid5.Uuid5Namespace.URL));
|
|
146
|
-
expect(a).toBe(b);
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
it("uuid5 differs for different names", async () => {
|
|
150
|
-
const a = await run(Ids.uuid5("a", Uuid5.Uuid5Namespace.DNS));
|
|
151
|
-
const b = await run(Ids.uuid5("b", Uuid5.Uuid5Namespace.DNS));
|
|
152
|
-
expect(a).not.toBe(b);
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
it("uuid5 predefined namespaces work", async () => {
|
|
156
|
-
const dns = await run(Ids.uuid5.dns("example.com"));
|
|
157
|
-
const url = await run(Ids.uuid5.url("https://example.com"));
|
|
158
|
-
const oid = await run(Ids.uuid5.oid("1.2.3"));
|
|
159
|
-
const x500 = await run(Ids.uuid5.x500("cn=test"));
|
|
160
|
-
expect(Uuid5.isUuid5(dns)).toBe(true);
|
|
161
|
-
expect(Uuid5.isUuid5(url)).toBe(true);
|
|
162
|
-
expect(Uuid5.isUuid5(oid)).toBe(true);
|
|
163
|
-
expect(Uuid5.isUuid5(x500)).toBe(true);
|
|
164
|
-
expect(new Set([dns, url, oid, x500]).size).toBe(4);
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
it("uuid7 produces valid UUID v7", async () => {
|
|
168
|
-
const id = await run(Ids.uuid7);
|
|
169
|
-
expect(Uuid7.isUuid7(id)).toBe(true);
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
it("cuid produces valid CUID", async () => {
|
|
173
|
-
const id = await run(Ids.cuid);
|
|
174
|
-
expect(Cuid.isCuid(id)).toBe(true);
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
it("ksuid produces valid KSUID", async () => {
|
|
178
|
-
const id = await run(Ids.ksuid);
|
|
179
|
-
expect(Ksuid.isKsuid(id)).toBe(true);
|
|
180
|
-
expect(id).toHaveLength(27);
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
it("nanoId produces valid NanoId", async () => {
|
|
184
|
-
const id = await run(Ids.nanoId);
|
|
185
|
-
expect(NanoId.isNanoId(id)).toBe(true);
|
|
186
|
-
expect(id).toHaveLength(21);
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
it("ulid produces valid ULID", async () => {
|
|
190
|
-
const id = await run(Ids.ulid);
|
|
191
|
-
expect(Ulid.isUlid(id)).toBe(true);
|
|
192
|
-
expect(id).toHaveLength(26);
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
it("Ids.Test with fixed time yields deterministic time-based prefixes", async () => {
|
|
196
|
-
const runFixed = <A, E>(effect: Effect.Effect<A, E, Ids>) =>
|
|
197
|
-
effect.pipe(Effect.provide(Ids.Test({})), Random.withSeed(0), Effect.runPromise);
|
|
198
|
-
|
|
199
|
-
const [ulid1, ulid2, ksuid1, ksuid2] = await runFixed(
|
|
200
|
-
Effect.all([Ids.ulid, Ids.ulid, Ids.ksuid, Ids.ksuid], { concurrency: "unbounded" }),
|
|
201
|
-
);
|
|
202
|
-
expect(ulid1.slice(0, 10)).toBe(ulid2.slice(0, 10));
|
|
203
|
-
expect(ksuid1.slice(0, 5)).toBe(ksuid2.slice(0, 5));
|
|
204
|
-
});
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
describe("Ids.Default", () => {
|
|
208
|
-
it("runs all generators with default layer", async () => {
|
|
209
|
-
const program = Effect.gen(function* () {
|
|
210
|
-
const ids = yield* Ids;
|
|
211
|
-
const u4 = yield* ids.uuid4;
|
|
212
|
-
const u5 = yield* ids.uuid5("default-test", Uuid5.Uuid5Namespace.DNS);
|
|
213
|
-
const u7 = yield* ids.uuid7;
|
|
214
|
-
const c = yield* ids.cuid;
|
|
215
|
-
const k = yield* ids.ksuid;
|
|
216
|
-
const n = yield* ids.nanoId;
|
|
217
|
-
const u = yield* ids.ulid;
|
|
218
|
-
return { u4, u5, u7, c, k, n, u };
|
|
219
|
-
});
|
|
220
|
-
const result = await Effect.runPromise(Effect.provide(program, Ids.Default));
|
|
221
|
-
expect(Uuid4.isUuid4(result.u4)).toBe(true);
|
|
222
|
-
expect(Uuid5.isUuid5(result.u5)).toBe(true);
|
|
223
|
-
expect(Uuid7.isUuid7(result.u7)).toBe(true);
|
|
224
|
-
expect(Cuid.isCuid(result.c)).toBe(true);
|
|
225
|
-
expect(Ksuid.isKsuid(result.k)).toBe(true);
|
|
226
|
-
expect(NanoId.isNanoId(result.n)).toBe(true);
|
|
227
|
-
expect(Ulid.isUlid(result.u)).toBe(true);
|
|
228
|
-
});
|
|
229
|
-
});
|
|
230
|
-
});
|
package/src/Ids.ts
DELETED
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
import * as Effect from "effect/Effect";
|
|
2
|
-
import { dual } from "effect/Function";
|
|
3
|
-
import * as Layer from "effect/Layer";
|
|
4
|
-
import * as ServiceMap from "effect/ServiceMap";
|
|
5
|
-
import type { Cuid } from "./Cuid.js";
|
|
6
|
-
import { cuid, CuidState } from "./Cuid.js";
|
|
7
|
-
import { DateTimes } from "./DateTimes.js";
|
|
8
|
-
import type { Ksuid } from "./Ksuid.js";
|
|
9
|
-
import { ksuid } from "./Ksuid.js";
|
|
10
|
-
import type { NanoId } from "./NanoId.js";
|
|
11
|
-
import { nanoId } from "./NanoId.js";
|
|
12
|
-
import { RandomValues } from "./RandomValues.js";
|
|
13
|
-
import type { Ulid } from "./Ulid.js";
|
|
14
|
-
import { ulid } from "./Ulid.js";
|
|
15
|
-
import type { Uuid4 } from "./Uuid4.js";
|
|
16
|
-
import { uuid4 } from "./Uuid4.js";
|
|
17
|
-
import { type Uuid5, uuid5, Uuid5Namespace } from "./Uuid5.js";
|
|
18
|
-
import type { Uuid7 } from "./Uuid7.js";
|
|
19
|
-
import { uuid7, Uuid7State } from "./Uuid7.js";
|
|
20
|
-
import { TestClock } from "effect/testing";
|
|
21
|
-
|
|
22
|
-
export class Ids extends ServiceMap.Service<Ids>()("@typed/id/Ids", {
|
|
23
|
-
make: Effect.gen(function* () {
|
|
24
|
-
const services = yield* Effect.services<DateTimes | RandomValues | CuidState | Uuid7State>();
|
|
25
|
-
|
|
26
|
-
const uuid5_: {
|
|
27
|
-
(namespace: Uuid5Namespace): (name: string) => Effect.Effect<Uuid5>;
|
|
28
|
-
(name: string, namespace: Uuid5Namespace): Effect.Effect<Uuid5>;
|
|
29
|
-
readonly dns: (name: string) => Effect.Effect<Uuid5>;
|
|
30
|
-
readonly url: (name: string) => Effect.Effect<Uuid5>;
|
|
31
|
-
readonly oid: (name: string) => Effect.Effect<Uuid5>;
|
|
32
|
-
readonly x500: (name: string) => Effect.Effect<Uuid5>;
|
|
33
|
-
} = Object.assign(
|
|
34
|
-
dual(2, (name: string, namespace: Uuid5Namespace) =>
|
|
35
|
-
Effect.provide(uuid5(name, namespace), services),
|
|
36
|
-
),
|
|
37
|
-
{
|
|
38
|
-
dns: uuid5(Uuid5Namespace.DNS),
|
|
39
|
-
url: uuid5(Uuid5Namespace.URL),
|
|
40
|
-
oid: uuid5(Uuid5Namespace.OID),
|
|
41
|
-
x500: uuid5(Uuid5Namespace.X500),
|
|
42
|
-
},
|
|
43
|
-
);
|
|
44
|
-
|
|
45
|
-
return {
|
|
46
|
-
cuid: Effect.provide(cuid, services),
|
|
47
|
-
ksuid: Effect.provide(ksuid, services),
|
|
48
|
-
nanoId: Effect.provide(nanoId, services),
|
|
49
|
-
ulid: Effect.provide(ulid, services),
|
|
50
|
-
uuid4: Effect.provide(uuid4, services),
|
|
51
|
-
uuid5: uuid5_,
|
|
52
|
-
uuid7: Effect.provide(uuid7, services),
|
|
53
|
-
};
|
|
54
|
-
}),
|
|
55
|
-
}) {
|
|
56
|
-
static readonly cuid: Effect.Effect<Cuid, never, Ids> = Effect.flatMap(
|
|
57
|
-
Ids.asEffect(),
|
|
58
|
-
({ cuid }) => cuid,
|
|
59
|
-
);
|
|
60
|
-
|
|
61
|
-
static readonly ksuid: Effect.Effect<Ksuid, never, Ids> = Effect.flatMap(
|
|
62
|
-
Ids.asEffect(),
|
|
63
|
-
({ ksuid }) => ksuid,
|
|
64
|
-
);
|
|
65
|
-
|
|
66
|
-
static readonly nanoId: Effect.Effect<NanoId, never, Ids> = Effect.flatMap(
|
|
67
|
-
Ids.asEffect(),
|
|
68
|
-
({ nanoId }) => nanoId,
|
|
69
|
-
);
|
|
70
|
-
|
|
71
|
-
static readonly ulid: Effect.Effect<Ulid, never, Ids> = Effect.flatMap(
|
|
72
|
-
Ids.asEffect(),
|
|
73
|
-
({ ulid }) => ulid,
|
|
74
|
-
);
|
|
75
|
-
|
|
76
|
-
static readonly uuid4: Effect.Effect<Uuid4, never, Ids> = Effect.flatMap(
|
|
77
|
-
Ids.asEffect(),
|
|
78
|
-
({ uuid4 }) => uuid4,
|
|
79
|
-
);
|
|
80
|
-
|
|
81
|
-
static readonly uuid5: {
|
|
82
|
-
(namespace: Uuid5Namespace): (name: string) => Effect.Effect<Uuid5, never, Ids>;
|
|
83
|
-
(name: string, namespace: Uuid5Namespace): Effect.Effect<Uuid5, never, Ids>;
|
|
84
|
-
readonly dns: (name: string) => Effect.Effect<Uuid5, never, Ids>;
|
|
85
|
-
readonly url: (name: string) => Effect.Effect<Uuid5, never, Ids>;
|
|
86
|
-
readonly oid: (name: string) => Effect.Effect<Uuid5, never, Ids>;
|
|
87
|
-
readonly x500: (name: string) => Effect.Effect<Uuid5, never, Ids>;
|
|
88
|
-
} = Object.assign(
|
|
89
|
-
dual(2, (name: string, namespace: Uuid5Namespace) =>
|
|
90
|
-
Effect.flatMap(Ids.asEffect(), ({ uuid5 }) => uuid5(name, namespace)),
|
|
91
|
-
),
|
|
92
|
-
{
|
|
93
|
-
dns: (name: string) => Effect.flatMap(Ids.asEffect(), ({ uuid5 }) => uuid5.dns(name)),
|
|
94
|
-
url: (name: string) => Effect.flatMap(Ids.asEffect(), ({ uuid5 }) => uuid5.url(name)),
|
|
95
|
-
oid: (name: string) => Effect.flatMap(Ids.asEffect(), ({ uuid5 }) => uuid5.oid(name)),
|
|
96
|
-
x500: (name: string) => Effect.flatMap(Ids.asEffect(), ({ uuid5 }) => uuid5.x500(name)),
|
|
97
|
-
},
|
|
98
|
-
);
|
|
99
|
-
static readonly uuid7: Effect.Effect<Uuid7, never, Ids> = Effect.flatMap(
|
|
100
|
-
Ids.asEffect(),
|
|
101
|
-
({ uuid7 }) => uuid7,
|
|
102
|
-
);
|
|
103
|
-
|
|
104
|
-
static readonly Default: Layer.Layer<Ids | DateTimes | RandomValues, never, never> = Layer.effect(
|
|
105
|
-
Ids,
|
|
106
|
-
Ids.make,
|
|
107
|
-
).pipe(
|
|
108
|
-
Layer.provide([CuidState.Default, Uuid7State.Default]),
|
|
109
|
-
Layer.provideMerge([DateTimes.Default, RandomValues.Default]),
|
|
110
|
-
);
|
|
111
|
-
|
|
112
|
-
static readonly Test = (
|
|
113
|
-
options?: TestOptions,
|
|
114
|
-
): Layer.Layer<Ids | DateTimes | RandomValues | TestClock.TestClock> =>
|
|
115
|
-
Layer.effect(Ids, Ids.make).pipe(
|
|
116
|
-
Layer.provide([
|
|
117
|
-
Layer.effect(CuidState, CuidState.make(options?.envData ?? "node")),
|
|
118
|
-
Uuid7State.Default,
|
|
119
|
-
]),
|
|
120
|
-
Layer.provideMerge([DateTimes.Fixed(options?.currentTime ?? 0), RandomValues.Random]),
|
|
121
|
-
Layer.provideMerge(TestClock.layer({})),
|
|
122
|
-
);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
export type TestOptions = {
|
|
126
|
-
readonly currentTime?: number | string | Date;
|
|
127
|
-
readonly envData?: string;
|
|
128
|
-
};
|