@aria-framework/kit 0.6.0 → 0.7.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/asyncHandler.js +11 -1
- package/index.js +2 -0
- package/package.json +3 -2
- package/publicId.js +69 -0
package/asyncHandler.js
CHANGED
|
@@ -13,12 +13,22 @@
|
|
|
13
13
|
*
|
|
14
14
|
* Express 5 handles this natively. Kept anyway: it is a no-op there, and removing it would mean
|
|
15
15
|
* editing every route the day an app upgrades.
|
|
16
|
+
*
|
|
17
|
+
* IT RETURNS THE PROMISE, and that `return` is load-bearing even though Express ignores it.
|
|
18
|
+
* TEST HARNESSES AWAIT THE HANDLER. Both consuming apps drive real route handlers directly —
|
|
19
|
+
* `await callback(req, res, next)` — to exercise a flow without standing up a server. Drop the
|
|
20
|
+
* return and `await undefined` resolves immediately, so every assertion runs BEFORE the handler
|
|
21
|
+
* has finished and a suite that was proving something starts proving nothing.
|
|
22
|
+
*
|
|
23
|
+
* This was found the hard way: 0.6.0 shipped without it, because the original was a one-line
|
|
24
|
+
* arrow whose implicit return was invisible when it was rewritten as a named function. Eight
|
|
25
|
+
* checks in one app's Entra suite failed, all of them looking like authorisation bugs.
|
|
16
26
|
*/
|
|
17
27
|
|
|
18
28
|
'use strict';
|
|
19
29
|
|
|
20
30
|
module.exports = function asyncHandler(fn) {
|
|
21
31
|
return function (req, res, next) {
|
|
22
|
-
Promise.resolve(fn(req, res, next)).catch(next);
|
|
32
|
+
return Promise.resolve(fn(req, res, next)).catch(next);
|
|
23
33
|
};
|
|
24
34
|
};
|
package/index.js
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* fullName(first, last) / splitName(full) — person-name helpers
|
|
11
11
|
* fileSniff.sniff(path) / fileSniff.matches(path, mime)
|
|
12
12
|
* trackingId.generate() / trackingId.generateUnique(isTaken, maxAttempts)
|
|
13
|
+
* publicId.generate(prefix) / publicId.isValid(value, prefix) — prefixed permanent ids
|
|
13
14
|
* asyncHandler(fn) — Express async-rejection wrapper
|
|
14
15
|
* dbTime.parseDbTimestampAsUtc / dbTime.partsInTimezone — a stored timestamp, in a timezone
|
|
15
16
|
*/
|
|
@@ -26,6 +27,7 @@ module.exports = {
|
|
|
26
27
|
splitName,
|
|
27
28
|
fileSniff: require('./fileSniff'),
|
|
28
29
|
trackingId: require('./trackingId'),
|
|
30
|
+
publicId: require('./publicId'),
|
|
29
31
|
// Page arithmetic for list views: the clamp, the past-the-end correction and the page-link
|
|
30
32
|
// window, which are the three things every hand-rolled paginator here got wrong or omitted.
|
|
31
33
|
paginate: require('./paginate').paginate,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aria-framework/kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Aria App Framework — kit module. Small dependency-free server utilities: open-redirect guard (safeReturnTo), SQL LIKE escaping, magic-byte upload validation (fileSniff), Crockford base32 tracking IDs, and person-name compose/split helpers.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"private": false,
|
|
@@ -15,12 +15,13 @@
|
|
|
15
15
|
"isEmail.js",
|
|
16
16
|
"fileSniff.js",
|
|
17
17
|
"trackingId.js",
|
|
18
|
+
"publicId.js",
|
|
18
19
|
"personName.js",
|
|
19
20
|
"paginate.js",
|
|
20
21
|
"asyncHandler.js",
|
|
21
22
|
"dbTime.js"
|
|
22
23
|
],
|
|
23
24
|
"scripts": {
|
|
24
|
-
"test": "node test/smoke.js && node test/paginate.js && node test/dbTime.js"
|
|
25
|
+
"test": "node test/smoke.js && node test/paginate.js && node test/dbTime.js && node test/publicId.js"
|
|
25
26
|
}
|
|
26
27
|
}
|
package/publicId.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public identifiers — prefix_XXXXXXXXXXXXXXXX, sixteen Crockford base32 characters.
|
|
3
|
+
*
|
|
4
|
+
* The THIRD identifier kind, beside the integer primary key and the caller's own external ref.
|
|
5
|
+
* The integer id is the database's key (joins, foreign keys) and never leaves the building — it is
|
|
6
|
+
* enumerable and it counts your rows for anyone who sees it. The external ref is the CALLER'S key,
|
|
7
|
+
* agreed at onboarding, optional and editable. This is OURS and permanent: how an API, a URL or a
|
|
8
|
+
* log names an entity, assigned once at creation and never changed. A `cnt_…` in a log line says
|
|
9
|
+
* what it is before anyone looks it up.
|
|
10
|
+
*
|
|
11
|
+
* This is the RAW primitive — it takes a prefix string and knows nothing of any app's entities.
|
|
12
|
+
* The registry of which prefix means which entity (customer, invoice, …) is per-app policy and
|
|
13
|
+
* lives in the app, exactly as `trackingId` leaves the storage existence-check to its caller. The
|
|
14
|
+
* two things every consumer shares — the alphabet and the length — are single-sourced HERE, so no
|
|
15
|
+
* two apps can drift on what a public id physically is.
|
|
16
|
+
*
|
|
17
|
+
* Crockford base32 (no I, L, O, U — same alphabet as trackingId) so ids survive being read aloud
|
|
18
|
+
* and retyped. crypto.randomInt gives an unbiased draw. Sixteen characters is 32^16 ≈ 1.2 × 10^24
|
|
19
|
+
* of space: at that size a check-the-database-first loop buys nothing, so generation is a plain
|
|
20
|
+
* draw and a UNIQUE index on the storing column is the collision backstop — if the astronomically
|
|
21
|
+
* unlikely happens, the INSERT fails loudly instead of two rows sharing a name.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
'use strict';
|
|
25
|
+
|
|
26
|
+
const crypto = require('crypto');
|
|
27
|
+
|
|
28
|
+
const ALPHABET = '0123456789ABCDEFGHJKMNPQRSTVWXYZ'; // Crockford base32 (no I L O U)
|
|
29
|
+
const DEFAULT_LENGTH = 16;
|
|
30
|
+
|
|
31
|
+
/** A prefix is a short token that names the KIND — it must not contain the `_` separator. */
|
|
32
|
+
function assertPrefix(prefix) {
|
|
33
|
+
if (typeof prefix !== 'string' || prefix.length === 0 || prefix.includes('_')) {
|
|
34
|
+
throw new Error('publicId: prefix must be a non-empty string with no "_" separator');
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* A well-formed public id for the given prefix, e.g. generate('cnt') → 'cnt_8HW3PB6XK2M94TQZ'.
|
|
40
|
+
* @param {string} prefix - the KIND token (no underscore)
|
|
41
|
+
* @param {{length?: number}} [opts]
|
|
42
|
+
* @returns {string}
|
|
43
|
+
*/
|
|
44
|
+
function generate(prefix, { length = DEFAULT_LENGTH } = {}) {
|
|
45
|
+
assertPrefix(prefix);
|
|
46
|
+
let body = '';
|
|
47
|
+
for (let i = 0; i < length; i++) body += ALPHABET[crypto.randomInt(ALPHABET.length)];
|
|
48
|
+
return `${prefix}_${body}`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* True when `value` is a well-formed public id for exactly this prefix.
|
|
53
|
+
*
|
|
54
|
+
* The prefix is REQUIRED: this primitive validates against one known prefix, because it has no
|
|
55
|
+
* registry to tell it which prefixes are legitimate. An app that wants "any of my entities" ORs
|
|
56
|
+
* this over its own prefix set (or precompiles a single regex from the exported ALPHABET).
|
|
57
|
+
*
|
|
58
|
+
* @param {string} value
|
|
59
|
+
* @param {string} prefix
|
|
60
|
+
* @param {{length?: number}} [opts]
|
|
61
|
+
* @returns {boolean}
|
|
62
|
+
*/
|
|
63
|
+
function isValid(value, prefix, { length = DEFAULT_LENGTH } = {}) {
|
|
64
|
+
assertPrefix(prefix);
|
|
65
|
+
const re = new RegExp(`^${prefix}_[${ALPHABET}]{${length}}$`);
|
|
66
|
+
return re.test(String(value == null ? '' : value));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
module.exports = { generate, isValid, ALPHABET, DEFAULT_LENGTH };
|