b2b-platform-utils 0.1.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 +15 -0
- package/cryptoWrapper.js +46 -0
- package/dates.js +47 -0
- package/errorsMap.js +25 -0
- package/fileSystem.js +28 -0
- package/localCache.js +87 -0
- package/logger.js +33 -0
- package/numbers.js +10 -0
- package/optimizeMedia.js +48 -0
- package/package.json +25 -0
- package/paginator.js +11 -0
- package/sanitizeHtml.js +67 -0
- package/slugger.js +59 -0
- package/sorting.js +31 -0
package/README.md
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# platform-utils
|
|
2
|
+
|
|
3
|
+
Shared utilities for Node.js microservices (CommonJS).
|
|
4
|
+
This package centralizes small, repeated helpers used across services so they can be maintained in one place.
|
|
5
|
+
|
|
6
|
+
> Node.js ≥ 18 is required. License: **KingSizer**.
|
|
7
|
+
> Intended for **private** use (published to npmjs.org as a restricted package).
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
# make sure your .npmrc has a valid auth token for npmjs.org
|
|
15
|
+
npm i platform-utils
|
package/cryptoWrapper.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Purpose: PBKDF2-based password hashing/validation with a salt fetched from cache or config file.
|
|
4
|
+
// Note: Public API preserved exactly.
|
|
5
|
+
|
|
6
|
+
const crypto = require('crypto');
|
|
7
|
+
const { getValue } = require('./localCache');
|
|
8
|
+
const { readFile } = require('./fileSystem');
|
|
9
|
+
|
|
10
|
+
let iterationsCount = 200;
|
|
11
|
+
let keyLen = 64;
|
|
12
|
+
|
|
13
|
+
module.exports = {
|
|
14
|
+
/**
|
|
15
|
+
* Get salt value from in-memory cache or fallback to config file.
|
|
16
|
+
*/
|
|
17
|
+
getSalt: () => {
|
|
18
|
+
let salt = getValue('salt');
|
|
19
|
+
// Migrations/seeders flow when keys are not existing in runtime
|
|
20
|
+
if (!salt) {
|
|
21
|
+
let config = readFile('./config/config.json');
|
|
22
|
+
config = JSON.parse(config);
|
|
23
|
+
salt = config?.salt;
|
|
24
|
+
}
|
|
25
|
+
return salt;
|
|
26
|
+
},
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Derive a password hash using PBKDF2-SHA512.
|
|
30
|
+
*/
|
|
31
|
+
generatePassword: (password) => {
|
|
32
|
+
return crypto
|
|
33
|
+
.pbkdf2Sync(password, module.exports.getSalt(), iterationsCount, keyLen, 'sha512')
|
|
34
|
+
.toString('hex');
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Constant-time style comparison for provided password vs stored hash.
|
|
39
|
+
*/
|
|
40
|
+
validatePassword: (password, hash) => {
|
|
41
|
+
const derived = crypto
|
|
42
|
+
.pbkdf2Sync(password, module.exports.getSalt(), iterationsCount, keyLen, 'sha512')
|
|
43
|
+
.toString('hex');
|
|
44
|
+
return hash === derived;
|
|
45
|
+
}
|
|
46
|
+
};
|
package/dates.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Purpose: Date formatting and normalization utilities using moment.
|
|
4
|
+
// Note: Combines both your variants and preserves existing exports/signatures.
|
|
5
|
+
|
|
6
|
+
const moment = require('moment');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Returns a date/time format string by logical format key.
|
|
10
|
+
* Preserves all aliases and additional cases you listed.
|
|
11
|
+
*/
|
|
12
|
+
function getDateFormat(format) {
|
|
13
|
+
switch (format) {
|
|
14
|
+
case 'birthday':
|
|
15
|
+
case 'short':
|
|
16
|
+
return 'DD.MM.YYYY';
|
|
17
|
+
case 'regular':
|
|
18
|
+
case 'bonus':
|
|
19
|
+
case 'registration':
|
|
20
|
+
case 'tournament':
|
|
21
|
+
case 'promo':
|
|
22
|
+
return 'DD.MM.YYYY HH:mm';
|
|
23
|
+
default:
|
|
24
|
+
return 'DD.MM.YYYY HH:mm:ss';
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
module.exports = {
|
|
29
|
+
getDateFormat,
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Parse a birthday value with "birthday" date format and return a UTC moment.
|
|
33
|
+
* Original behavior preserved.
|
|
34
|
+
*/
|
|
35
|
+
getBirthdayDate: (value) => {
|
|
36
|
+
return moment(value, getDateFormat('birthday')).utc(true);
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Convert any Date-like input to a UTC moment; returns null if invalid.
|
|
41
|
+
* Original behavior preserved.
|
|
42
|
+
*/
|
|
43
|
+
getDateInUtc: (date) => {
|
|
44
|
+
const converted = new Date(date);
|
|
45
|
+
return (moment.isDate(converted)) ? moment.utc(converted) : null;
|
|
46
|
+
}
|
|
47
|
+
};
|
package/errorsMap.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Purpose: Provide a consistent way to look up predefined error objects from in-memory cache.
|
|
4
|
+
// Note: Keeps the original public API and behavior (returns JSON.stringify(currentError)).
|
|
5
|
+
|
|
6
|
+
const { getValue } = require('./localCache');
|
|
7
|
+
|
|
8
|
+
module.exports = {
|
|
9
|
+
duplicatedUniqueField: () => module.exports.getSingleError('duplicatedUniqueField'),
|
|
10
|
+
invalidConstraintField: () => module.exports.getSingleError('invalidConstraintField'),
|
|
11
|
+
mediaResourceNotFound: () => module.exports.getSingleError('mediaResourceNotFound'),
|
|
12
|
+
notificationNotFound: () => module.exports.getSingleError('notificationNotFound'),
|
|
13
|
+
mailTemplateNotFound: () => module.exports.getSingleError('mailTemplateNotFound'),
|
|
14
|
+
undefinedRequest: () => module.exports.getSingleError('undefinedRequest'),
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Returns a stringified error object by key from the cached "errorsMap" array.
|
|
18
|
+
* Keeps original behavior: JSON.stringify(undefined) => undefined.
|
|
19
|
+
*/
|
|
20
|
+
getSingleError: (errorKey) => {
|
|
21
|
+
const errors = getValue('errorsMap');
|
|
22
|
+
const currentError = errors?.find((item) => item.errorKey === errorKey);
|
|
23
|
+
return JSON.stringify(currentError);
|
|
24
|
+
}
|
|
25
|
+
};
|
package/fileSystem.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Purpose: Simple sync file helpers for small config/state blobs.
|
|
4
|
+
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
|
|
7
|
+
module.exports = {
|
|
8
|
+
/**
|
|
9
|
+
* Save data as JSON into a file (overwrites if exists).
|
|
10
|
+
*/
|
|
11
|
+
saveFile: (fileName, data) => {
|
|
12
|
+
fs.writeFileSync(fileName, JSON.stringify(data));
|
|
13
|
+
},
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Read text content from a file using UTF-8.
|
|
17
|
+
*/
|
|
18
|
+
readFile: (fileName) => {
|
|
19
|
+
return fs.readFileSync(fileName, 'utf-8');
|
|
20
|
+
},
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Remove a file from the filesystem.
|
|
24
|
+
*/
|
|
25
|
+
removeFile: (fileName) => {
|
|
26
|
+
fs.unlinkSync(fileName);
|
|
27
|
+
}
|
|
28
|
+
};
|
package/localCache.js
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Purpose: Lightweight in-memory key-value store scoped to the running process.
|
|
4
|
+
// Note: Added setService(key) to allow passing microservice key without breaking existing API.
|
|
5
|
+
|
|
6
|
+
let cacheData = {};
|
|
7
|
+
let serviceKey = 'notification';
|
|
8
|
+
let bufferChunkSize = 512000;
|
|
9
|
+
const GEO_DEFAULT = 'WW';
|
|
10
|
+
|
|
11
|
+
function setValue(key, value) {
|
|
12
|
+
cacheData[key] = value;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function getValue(key) {
|
|
16
|
+
return cacheData[key] || null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function getCommonValue() {
|
|
20
|
+
return cacheData['common'] || {};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Return current microservice key.
|
|
25
|
+
*/
|
|
26
|
+
function getService() {
|
|
27
|
+
return serviceKey;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Allow overriding the microservice key at runtime (non-breaking addition).
|
|
32
|
+
*/
|
|
33
|
+
function setService(key) {
|
|
34
|
+
if (typeof key === 'string' && key.length) {
|
|
35
|
+
serviceKey = key;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function getBufferChunkSize() {
|
|
40
|
+
return bufferChunkSize;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Initialize environment/customer from process params (kept as-is).
|
|
45
|
+
*/
|
|
46
|
+
function parseInitConfig(params) {
|
|
47
|
+
const environment = params[2];
|
|
48
|
+
const customer = params[3];
|
|
49
|
+
setValue('environment', environment);
|
|
50
|
+
setValue('customer', customer);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Shallow-apply a config object into the cache.
|
|
55
|
+
*/
|
|
56
|
+
function applyConfig(data) {
|
|
57
|
+
for (const i in data) {
|
|
58
|
+
setValue(i, data[i]);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Return the whole cache snapshot (by reference).
|
|
64
|
+
*/
|
|
65
|
+
function getData() {
|
|
66
|
+
return cacheData;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Read GEODefault from "common" bag or return fallback.
|
|
71
|
+
*/
|
|
72
|
+
function getDefaultGEO() {
|
|
73
|
+
return getValue('common')?.GEODefault || GEO_DEFAULT;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
module.exports = {
|
|
77
|
+
setValue,
|
|
78
|
+
getValue,
|
|
79
|
+
parseInitConfig,
|
|
80
|
+
applyConfig,
|
|
81
|
+
getData,
|
|
82
|
+
getService,
|
|
83
|
+
setService,
|
|
84
|
+
getBufferChunkSize,
|
|
85
|
+
getDefaultGEO,
|
|
86
|
+
getCommonValue
|
|
87
|
+
};
|
package/logger.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Purpose: Colorized console logging helpers for quick CLI visibility.
|
|
4
|
+
|
|
5
|
+
module.exports = {
|
|
6
|
+
/**
|
|
7
|
+
* Print a debug payload as magenta JSON.
|
|
8
|
+
*/
|
|
9
|
+
debug: (data) => {
|
|
10
|
+
console.log(`\x1b[35m❗ ${JSON.stringify(data)} [DEBUG] \x1b[0m`);
|
|
11
|
+
},
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Print a green success note.
|
|
15
|
+
*/
|
|
16
|
+
successNote: (message) => {
|
|
17
|
+
console.info(`\x1b[32m✅ ${message?.toString()} \x1b[0m`);
|
|
18
|
+
},
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Print a red critical note.
|
|
22
|
+
*/
|
|
23
|
+
criticalNote: (message) => {
|
|
24
|
+
console.info(`\x1b[31m⛔ ${message?.toString()} ⛔\x1b[0m`);
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Print a yellow warning note.
|
|
29
|
+
*/
|
|
30
|
+
warningNote: (message) => {
|
|
31
|
+
console.warn(`\x1b[33m❗ ${message?.toString()} ❗\x1b[0m`);
|
|
32
|
+
}
|
|
33
|
+
};
|
package/numbers.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Purpose: Numeric validators and helpers.
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Validate that a provided value is a finite, non-negative number.
|
|
7
|
+
*/
|
|
8
|
+
const isValidPositiveNumber = (v) => v != null && !isNaN(v) && v >= 0;
|
|
9
|
+
|
|
10
|
+
module.exports = { isValidPositiveNumber };
|
package/optimizeMedia.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Purpose: Image optimization helpers powered by Sharp.
|
|
4
|
+
|
|
5
|
+
const sharp = require('sharp');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Convert a buffer to resized WebP with adjustable quality and alpha quality.
|
|
9
|
+
* Preserves original function name and signature.
|
|
10
|
+
*/
|
|
11
|
+
async function optimizeImageBufferWEBP(fileDataBuffer, quality = 80, resize = 1, alphaQuality = 80) {
|
|
12
|
+
const metadata = await sharp(fileDataBuffer).metadata();
|
|
13
|
+
const newWidth = Math.round(metadata.width * resize);
|
|
14
|
+
const newHeight = Math.round(metadata.height * resize);
|
|
15
|
+
|
|
16
|
+
return sharp(fileDataBuffer)
|
|
17
|
+
.resize(newWidth, newHeight)
|
|
18
|
+
.webp({
|
|
19
|
+
quality,
|
|
20
|
+
lossless: false, // true — without losses (larger size), false — with losses
|
|
21
|
+
smartSubsample: true, // better quality on small details
|
|
22
|
+
effort: 6, // 0 (fast) — 6 (slower, better compression)
|
|
23
|
+
alphaQuality
|
|
24
|
+
})
|
|
25
|
+
.toBuffer();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Resize a buffer to a max width and convert to WebP for game images.
|
|
30
|
+
* Preserves original function name and signature.
|
|
31
|
+
*/
|
|
32
|
+
async function saveGameImageBufferWEBP(fileDataBuffer, quality = 80, maxWidth = 300) {
|
|
33
|
+
return sharp(fileDataBuffer)
|
|
34
|
+
.rotate()
|
|
35
|
+
.resize({
|
|
36
|
+
width: maxWidth,
|
|
37
|
+
fit: 'inside',
|
|
38
|
+
withoutEnlargement: true
|
|
39
|
+
})
|
|
40
|
+
.webp({
|
|
41
|
+
quality,
|
|
42
|
+
effort: 5,
|
|
43
|
+
smartSubsample: true
|
|
44
|
+
})
|
|
45
|
+
.toBuffer();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
module.exports = { optimizeImageBufferWEBP, saveGameImageBufferWEBP };
|
package/package.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "b2b-platform-utils",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Shared utilities for Node.js microservices: errors map, local cache, logger, numbers, dates, filesystem, media optimization, paginator, slugger, crypto wrapper, sanitize HTML, sorting.",
|
|
5
|
+
"type": "commonjs",
|
|
6
|
+
"license": "KingSizer",
|
|
7
|
+
"private": false,
|
|
8
|
+
"files": [
|
|
9
|
+
"*.js",
|
|
10
|
+
"README.md"
|
|
11
|
+
],
|
|
12
|
+
"engines": {
|
|
13
|
+
"node": ">=18"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"nodejs",
|
|
17
|
+
"utils",
|
|
18
|
+
"microservices",
|
|
19
|
+
"commonjs"
|
|
20
|
+
],
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"moment": "^2.30.1",
|
|
23
|
+
"sharp": "^0.33.0"
|
|
24
|
+
}
|
|
25
|
+
}
|
package/paginator.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Purpose: Compute total pages given a total count and page size.
|
|
4
|
+
// Note: Keeps the exact public API and semantics.
|
|
5
|
+
|
|
6
|
+
module.exports = {
|
|
7
|
+
getTotalPages: (totalItems, limit = 0) => {
|
|
8
|
+
const totalPages = (totalItems && (+limit > 0)) ? +(totalItems / limit) : 0;
|
|
9
|
+
return (totalPages <= 1) ? 1 : +Math.ceil(totalPages);
|
|
10
|
+
}
|
|
11
|
+
};
|
package/sanitizeHtml.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Purpose: Remove inline data:image payloads and related attributes from HTML safely.
|
|
4
|
+
// Note: Converted to CommonJS while keeping the same function name and behavior.
|
|
5
|
+
|
|
6
|
+
function stripInlineImages(html) {
|
|
7
|
+
// Removing <img ... src="data:image/...">
|
|
8
|
+
html = html.replace(
|
|
9
|
+
/<img\b[^>]*\bsrc\s*=\s*(['"])\s*data\s*:\s*image\/[^"']+\1[^>]*>/gi,
|
|
10
|
+
''
|
|
11
|
+
);
|
|
12
|
+
html = html.replace(
|
|
13
|
+
/<img\b[^>]*\bsrc\s*=\s*data\s*:\s*image\/[^\s>]+[^>]*>/gi,
|
|
14
|
+
''
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
// Removing srcset with data:image
|
|
18
|
+
html = html.replace(/\bsrcset\s*=\s*(['"])[\s\S]*?\1/gi, (m) => {
|
|
19
|
+
const q = m.match(/^(srcset\s*=\s*)(['"])([\s\S]*)(\2)$/i);
|
|
20
|
+
if (!q) return m;
|
|
21
|
+
const list = q[3]
|
|
22
|
+
.split(',')
|
|
23
|
+
.map((s) => s.trim())
|
|
24
|
+
.filter((item) => !/^\s*data\s*:\s*image\//i.test(item));
|
|
25
|
+
return list.length ? `srcset="${list.join(', ')}"` : '';
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// Removing <source> with data:image in src/srcset
|
|
29
|
+
html = html.replace(
|
|
30
|
+
/<source\b[^>]*\b(?:src|srcset)\s*=\s*(['"])\s*data\s*:\s*image\/[^"']+\1[^>]*>/gi,
|
|
31
|
+
''
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
// Posters/background with data:image
|
|
35
|
+
html = html.replace(/\bposter\s*=\s*(['"])\s*data\s*:\s*image\/[^"']+\1/gi, 'poster=""');
|
|
36
|
+
html = html.replace(/\bbackground\s*=\s*(['"])\s*data\s*:\s*image\/[^"']+\1/gi, '');
|
|
37
|
+
|
|
38
|
+
// <link rel="icon" href="data:image/...">
|
|
39
|
+
html = html.replace(
|
|
40
|
+
/<link\b[^>]*\brel\s*=\s*(['"])\s*icon\1[^>]*\bhref\s*=\s*(['"])\s*data\s*:\s*image\/[^"']+\2[^>]*>/gi,
|
|
41
|
+
''
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
// Clear inline CSS url(data:...)
|
|
45
|
+
html = html.replace(/url\(\s*(['"])?\s*data\s*:\s*image\/[^"')]+(\1)?\s*\)/gi, 'none');
|
|
46
|
+
|
|
47
|
+
// Inside <style>...</style>
|
|
48
|
+
html = html.replace(
|
|
49
|
+
/(<style\b[^>]*>)([\s\S]*?)(<\/style>)/gi,
|
|
50
|
+
(_, open, css, close) =>
|
|
51
|
+
open + css.replace(/url\(\s*(['"])?\s*data\s*:\s*image\/[^"')]+(\1)?\s*\)/gi, 'none') + close
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
// Fix accidental Cyrillic <п> tags to <p>
|
|
55
|
+
html = html.replace(/<\/\s*п\s*>/g, '</p>').replace(/<\s*п(\s|>)/g, '<p$1>');
|
|
56
|
+
|
|
57
|
+
// Remove empty <h2><br/></h2> and <p><br/></p>
|
|
58
|
+
html = html.replace(/<h2[^>]*>\s*<br\s*\/?>\s*<\/h2>/gi, '<h2></h2>');
|
|
59
|
+
html = html.replace(/<p[^>]*>\s*<br\s*\/?>\s*<\/p>/gi, '<p></p>');
|
|
60
|
+
|
|
61
|
+
// Normalize redundant spaces before tags
|
|
62
|
+
html = html.replace(/\s+(?=<)/g, ' ');
|
|
63
|
+
|
|
64
|
+
return html;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
module.exports = { stripInlineImages };
|
package/slugger.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Purpose: Normalize a string into a URL-friendly slug.
|
|
4
|
+
|
|
5
|
+
module.exports = {
|
|
6
|
+
/**
|
|
7
|
+
* Convert input string to a normalized slug using replacements you provided.
|
|
8
|
+
* Preserves exact behavior and replacements.
|
|
9
|
+
*/
|
|
10
|
+
setSlug: (inputSlug = null) => {
|
|
11
|
+
if (!inputSlug) { return ''; }
|
|
12
|
+
return inputSlug.toString().toLowerCase()
|
|
13
|
+
.replaceAll(' ', '-')
|
|
14
|
+
.replaceAll('’', '')
|
|
15
|
+
.replaceAll('‘', '')
|
|
16
|
+
.replaceAll("'", '')
|
|
17
|
+
.replaceAll(':', '')
|
|
18
|
+
.replaceAll(';', '')
|
|
19
|
+
.replaceAll('_', '-')
|
|
20
|
+
.replaceAll(',', '')
|
|
21
|
+
.replaceAll('.', '')
|
|
22
|
+
.replaceAll('"', '')
|
|
23
|
+
// Specials
|
|
24
|
+
.replaceAll('!', '')
|
|
25
|
+
.replaceAll('@', '')
|
|
26
|
+
.replaceAll('#', '')
|
|
27
|
+
.replaceAll('$', '')
|
|
28
|
+
.replaceAll('%', '')
|
|
29
|
+
.replaceAll('^', '')
|
|
30
|
+
.replaceAll('*', '')
|
|
31
|
+
.replaceAll('(', '')
|
|
32
|
+
.replaceAll(')', '')
|
|
33
|
+
.replaceAll('=', '')
|
|
34
|
+
.replaceAll('|', '')
|
|
35
|
+
.replaceAll('&', 'and')
|
|
36
|
+
// Umlautes
|
|
37
|
+
.replaceAll('ö', 'o')
|
|
38
|
+
.replaceAll('ô', 'o')
|
|
39
|
+
.replaceAll('ü', 'u')
|
|
40
|
+
.replaceAll('û', 'u')
|
|
41
|
+
.replaceAll('ù', 'u')
|
|
42
|
+
.replaceAll('ü', 'u')
|
|
43
|
+
.replaceAll('ä', 'a')
|
|
44
|
+
.replaceAll('æ', 'a')
|
|
45
|
+
.replaceAll('ø', 'a')
|
|
46
|
+
.replaceAll('å', 'a')
|
|
47
|
+
.replaceAll('à', 'a')
|
|
48
|
+
.replaceAll('â', 'a')
|
|
49
|
+
.replaceAll('ß', 's')
|
|
50
|
+
.replaceAll('é', 'e')
|
|
51
|
+
.replaceAll('è', 'e')
|
|
52
|
+
.replaceAll('ë', 'e')
|
|
53
|
+
.replaceAll('î', 'i')
|
|
54
|
+
.replaceAll('ï', 'i')
|
|
55
|
+
.replaceAll('Ç', 'c')
|
|
56
|
+
// Brands
|
|
57
|
+
.replaceAll('™', 'TM');
|
|
58
|
+
}
|
|
59
|
+
};
|
package/sorting.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Purpose: Helpers to build stable sorting arrays and validate directions.
|
|
4
|
+
|
|
5
|
+
const validDirections = ['ASC', 'DESC'];
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Convert an object like { field: 'ASC', other: 'DESC' } into [['field','ASC'], ['other','DESC']]
|
|
9
|
+
* Invalid directions are filtered out.
|
|
10
|
+
*/
|
|
11
|
+
function getSortedArray(sortingObject) {
|
|
12
|
+
return Object.entries(sortingObject)
|
|
13
|
+
.map(([key, value]) => {
|
|
14
|
+
const field = key;
|
|
15
|
+
const direction = String(value).toUpperCase();
|
|
16
|
+
if (validDirections.includes(direction)) {
|
|
17
|
+
return [field, direction];
|
|
18
|
+
}
|
|
19
|
+
return null;
|
|
20
|
+
})
|
|
21
|
+
.filter(Boolean);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Check if a direction value is valid (ASC|DESC), case-insensitive.
|
|
26
|
+
*/
|
|
27
|
+
function isValidDirection(directionFromField) {
|
|
28
|
+
return validDirections.includes(directionFromField?.toUpperCase());
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
module.exports = { getSortedArray, isValidDirection };
|