namespace-guard 0.4.0 → 0.6.1
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 +44 -12
- package/dist/cli.js +263 -14
- package/dist/cli.mjs +263 -14
- package/dist/index.d.mts +7 -3
- package/dist/index.d.ts +7 -3
- package/dist/index.js +268 -20
- package/dist/index.mjs +268 -20
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -33,29 +33,51 @@ var DEFAULT_MESSAGES = {
|
|
|
33
33
|
};
|
|
34
34
|
function extractMaxLength(pattern) {
|
|
35
35
|
const testStrings = ["a", "1", "a1", "a-1"];
|
|
36
|
-
|
|
36
|
+
let lo = 1;
|
|
37
|
+
let hi = 100;
|
|
38
|
+
let best = 30;
|
|
39
|
+
while (lo <= hi) {
|
|
40
|
+
const mid = Math.floor((lo + hi) / 2);
|
|
41
|
+
let anyMatch = false;
|
|
37
42
|
for (const chars of testStrings) {
|
|
38
|
-
const s = chars.repeat(Math.ceil(
|
|
39
|
-
if (pattern.test(s))
|
|
43
|
+
const s = chars.repeat(Math.ceil(mid / chars.length)).slice(0, mid);
|
|
44
|
+
if (pattern.test(s)) {
|
|
45
|
+
anyMatch = true;
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (anyMatch) {
|
|
50
|
+
best = mid;
|
|
51
|
+
lo = mid + 1;
|
|
52
|
+
} else {
|
|
53
|
+
hi = mid - 1;
|
|
40
54
|
}
|
|
41
55
|
}
|
|
42
|
-
return
|
|
56
|
+
return best;
|
|
43
57
|
}
|
|
44
58
|
function createDefaultSuggest(pattern) {
|
|
45
59
|
const maxLen = extractMaxLength(pattern);
|
|
46
60
|
return (identifier) => {
|
|
61
|
+
const seen = /* @__PURE__ */ new Set();
|
|
47
62
|
const candidates = [];
|
|
48
63
|
for (let i = 1; i <= 9; i++) {
|
|
49
64
|
const hyphenated = `${identifier}-${i}`;
|
|
50
|
-
if (hyphenated.length <= maxLen)
|
|
65
|
+
if (hyphenated.length <= maxLen) {
|
|
66
|
+
seen.add(hyphenated);
|
|
67
|
+
candidates.push(hyphenated);
|
|
68
|
+
}
|
|
51
69
|
const compact = `${identifier}${i}`;
|
|
52
|
-
if (compact.length <= maxLen)
|
|
70
|
+
if (compact.length <= maxLen) {
|
|
71
|
+
seen.add(compact);
|
|
72
|
+
candidates.push(compact);
|
|
73
|
+
}
|
|
53
74
|
}
|
|
54
75
|
if (identifier.length >= maxLen - 1) {
|
|
55
76
|
for (let i = 1; i <= 9; i++) {
|
|
56
77
|
const suffix = String(i);
|
|
57
78
|
const truncated = identifier.slice(0, maxLen - suffix.length) + suffix;
|
|
58
|
-
if (truncated !== identifier && !
|
|
79
|
+
if (truncated !== identifier && !seen.has(truncated)) {
|
|
80
|
+
seen.add(truncated);
|
|
59
81
|
candidates.push(truncated);
|
|
60
82
|
}
|
|
61
83
|
}
|
|
@@ -63,6 +85,184 @@ function createDefaultSuggest(pattern) {
|
|
|
63
85
|
return candidates;
|
|
64
86
|
};
|
|
65
87
|
}
|
|
88
|
+
var SUFFIX_WORDS = ["dev", "io", "app", "hq", "pro", "team", "labs", "hub", "go", "one"];
|
|
89
|
+
function createRandomDigitsStrategy(pattern) {
|
|
90
|
+
const maxLen = extractMaxLength(pattern);
|
|
91
|
+
return (identifier) => {
|
|
92
|
+
const seen = /* @__PURE__ */ new Set();
|
|
93
|
+
const candidates = [];
|
|
94
|
+
for (let i = 0; i < 15; i++) {
|
|
95
|
+
const digits = String(Math.floor(100 + Math.random() * 9900));
|
|
96
|
+
const candidate = `${identifier}-${digits}`;
|
|
97
|
+
if (candidate.length <= maxLen && !seen.has(candidate)) {
|
|
98
|
+
seen.add(candidate);
|
|
99
|
+
candidates.push(candidate);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return candidates;
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
function createSuffixWordsStrategy(pattern) {
|
|
106
|
+
const maxLen = extractMaxLength(pattern);
|
|
107
|
+
return (identifier) => {
|
|
108
|
+
const candidates = [];
|
|
109
|
+
for (const word of SUFFIX_WORDS) {
|
|
110
|
+
const candidate = `${identifier}-${word}`;
|
|
111
|
+
if (candidate.length <= maxLen) {
|
|
112
|
+
candidates.push(candidate);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return candidates;
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
function createShortRandomStrategy(pattern) {
|
|
119
|
+
const maxLen = extractMaxLength(pattern);
|
|
120
|
+
const chars = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
121
|
+
return (identifier) => {
|
|
122
|
+
const seen = /* @__PURE__ */ new Set();
|
|
123
|
+
const candidates = [];
|
|
124
|
+
for (let i = 0; i < 10; i++) {
|
|
125
|
+
let suffix = "";
|
|
126
|
+
for (let j = 0; j < 3; j++) {
|
|
127
|
+
suffix += chars[Math.floor(Math.random() * chars.length)];
|
|
128
|
+
}
|
|
129
|
+
const candidate = `${identifier}-${suffix}`;
|
|
130
|
+
if (candidate.length <= maxLen && !seen.has(candidate)) {
|
|
131
|
+
seen.add(candidate);
|
|
132
|
+
candidates.push(candidate);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return candidates;
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
function createScrambleStrategy(_pattern) {
|
|
139
|
+
return (identifier) => {
|
|
140
|
+
const seen = /* @__PURE__ */ new Set();
|
|
141
|
+
const candidates = [];
|
|
142
|
+
const chars = identifier.split("");
|
|
143
|
+
for (let i = 0; i < chars.length - 1; i++) {
|
|
144
|
+
if (chars[i] !== chars[i + 1]) {
|
|
145
|
+
const swapped = [...chars];
|
|
146
|
+
[swapped[i], swapped[i + 1]] = [swapped[i + 1], swapped[i]];
|
|
147
|
+
const candidate = swapped.join("");
|
|
148
|
+
if (candidate !== identifier && !seen.has(candidate)) {
|
|
149
|
+
seen.add(candidate);
|
|
150
|
+
candidates.push(candidate);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return candidates;
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
function createSimilarStrategy(pattern) {
|
|
158
|
+
const maxLen = extractMaxLength(pattern);
|
|
159
|
+
const nearby = {
|
|
160
|
+
a: "sqwz",
|
|
161
|
+
b: "vngh",
|
|
162
|
+
c: "xdfv",
|
|
163
|
+
d: "sfce",
|
|
164
|
+
e: "wrd",
|
|
165
|
+
f: "dgcv",
|
|
166
|
+
g: "fhtb",
|
|
167
|
+
h: "gjyn",
|
|
168
|
+
i: "uko",
|
|
169
|
+
j: "hknm",
|
|
170
|
+
k: "jli",
|
|
171
|
+
l: "kop",
|
|
172
|
+
m: "njk",
|
|
173
|
+
n: "bmhj",
|
|
174
|
+
o: "ipl",
|
|
175
|
+
p: "ol",
|
|
176
|
+
q: "wa",
|
|
177
|
+
r: "eft",
|
|
178
|
+
s: "adwz",
|
|
179
|
+
t: "rgy",
|
|
180
|
+
u: "yij",
|
|
181
|
+
v: "cfgb",
|
|
182
|
+
w: "qase",
|
|
183
|
+
x: "zsdc",
|
|
184
|
+
y: "tuh",
|
|
185
|
+
z: "xas",
|
|
186
|
+
"0": "19",
|
|
187
|
+
"1": "02",
|
|
188
|
+
"2": "13",
|
|
189
|
+
"3": "24",
|
|
190
|
+
"4": "35",
|
|
191
|
+
"5": "46",
|
|
192
|
+
"6": "57",
|
|
193
|
+
"7": "68",
|
|
194
|
+
"8": "79",
|
|
195
|
+
"9": "80"
|
|
196
|
+
};
|
|
197
|
+
const prefixes = ["the", "my", "x", "i"];
|
|
198
|
+
const suffixes = ["x", "o", "i", "z"];
|
|
199
|
+
return (identifier) => {
|
|
200
|
+
const seen = /* @__PURE__ */ new Set();
|
|
201
|
+
const candidates = [];
|
|
202
|
+
function add(c) {
|
|
203
|
+
if (c.length >= 2 && c.length <= maxLen && c !== identifier && pattern.test(c) && !seen.has(c)) {
|
|
204
|
+
seen.add(c);
|
|
205
|
+
candidates.push(c);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
for (let i = 0; i < identifier.length; i++) {
|
|
209
|
+
add(identifier.slice(0, i) + identifier.slice(i + 1));
|
|
210
|
+
}
|
|
211
|
+
for (let i = 0; i < identifier.length; i++) {
|
|
212
|
+
const ch = identifier[i];
|
|
213
|
+
const neighbours = nearby[ch] ?? "";
|
|
214
|
+
for (const n of neighbours) {
|
|
215
|
+
add(identifier.slice(0, i) + n + identifier.slice(i + 1));
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
for (const p of prefixes) {
|
|
219
|
+
add(p + identifier);
|
|
220
|
+
}
|
|
221
|
+
for (const s of suffixes) {
|
|
222
|
+
add(identifier + s);
|
|
223
|
+
}
|
|
224
|
+
return candidates;
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
function createStrategy(name, pattern) {
|
|
228
|
+
switch (name) {
|
|
229
|
+
case "sequential":
|
|
230
|
+
return createDefaultSuggest(pattern);
|
|
231
|
+
case "random-digits":
|
|
232
|
+
return createRandomDigitsStrategy(pattern);
|
|
233
|
+
case "suffix-words":
|
|
234
|
+
return createSuffixWordsStrategy(pattern);
|
|
235
|
+
case "short-random":
|
|
236
|
+
return createShortRandomStrategy(pattern);
|
|
237
|
+
case "scramble":
|
|
238
|
+
return createScrambleStrategy(pattern);
|
|
239
|
+
case "similar":
|
|
240
|
+
return createSimilarStrategy(pattern);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
function resolveGenerator(suggest, pattern) {
|
|
244
|
+
if (suggest?.generate) return suggest.generate;
|
|
245
|
+
const strategyInput = suggest?.strategy ?? ["sequential", "random-digits"];
|
|
246
|
+
if (typeof strategyInput === "function") return strategyInput;
|
|
247
|
+
const names = Array.isArray(strategyInput) ? strategyInput : [strategyInput];
|
|
248
|
+
const generators = names.map((name) => createStrategy(name, pattern));
|
|
249
|
+
if (generators.length === 1) return generators[0];
|
|
250
|
+
return (identifier) => {
|
|
251
|
+
const lists = generators.map((g) => g(identifier));
|
|
252
|
+
const seen = /* @__PURE__ */ new Set();
|
|
253
|
+
const result = [];
|
|
254
|
+
const maxListLen = Math.max(...lists.map((l) => l.length));
|
|
255
|
+
for (let i = 0; i < maxListLen; i++) {
|
|
256
|
+
for (const list of lists) {
|
|
257
|
+
if (i < list.length && !seen.has(list[i])) {
|
|
258
|
+
seen.add(list[i]);
|
|
259
|
+
result.push(list[i]);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return result;
|
|
264
|
+
};
|
|
265
|
+
}
|
|
66
266
|
function normalize(raw) {
|
|
67
267
|
return raw.trim().toLowerCase().replace(/^@+/, "");
|
|
68
268
|
}
|
|
@@ -70,17 +270,16 @@ function createProfanityValidator(words, options) {
|
|
|
70
270
|
const message = options?.message ?? "That name is not allowed.";
|
|
71
271
|
const checkSubstrings = options?.checkSubstrings ?? true;
|
|
72
272
|
const wordSet = new Set(words.map((w) => w.toLowerCase()));
|
|
273
|
+
const substringRegex = checkSubstrings && wordSet.size > 0 ? new RegExp(
|
|
274
|
+
Array.from(wordSet).map((w) => w.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|")
|
|
275
|
+
) : null;
|
|
73
276
|
return async (value) => {
|
|
74
277
|
const normalized = value.toLowerCase();
|
|
75
278
|
if (wordSet.has(normalized)) {
|
|
76
279
|
return { available: false, message };
|
|
77
280
|
}
|
|
78
|
-
if (
|
|
79
|
-
|
|
80
|
-
if (normalized.includes(word)) {
|
|
81
|
-
return { available: false, message };
|
|
82
|
-
}
|
|
83
|
-
}
|
|
281
|
+
if (substringRegex && substringRegex.test(normalized)) {
|
|
282
|
+
return { available: false, message };
|
|
84
283
|
}
|
|
85
284
|
return null;
|
|
86
285
|
};
|
|
@@ -120,6 +319,8 @@ function createNamespaceGuard(config, adapter) {
|
|
|
120
319
|
const cached = cacheMap.get(key);
|
|
121
320
|
if (cached && cached.expires > now) {
|
|
122
321
|
cacheHits++;
|
|
322
|
+
cacheMap.delete(key);
|
|
323
|
+
cacheMap.set(key, cached);
|
|
123
324
|
return cached.promise;
|
|
124
325
|
}
|
|
125
326
|
cacheMisses++;
|
|
@@ -147,6 +348,24 @@ function createNamespaceGuard(config, adapter) {
|
|
|
147
348
|
}
|
|
148
349
|
return null;
|
|
149
350
|
}
|
|
351
|
+
async function checkDbOnly(value, scope) {
|
|
352
|
+
const findOptions = config.caseInsensitive ? { caseInsensitive: true } : void 0;
|
|
353
|
+
const checks = config.sources.map(async (source) => {
|
|
354
|
+
const existing = await cachedFindOne(source, value, findOptions);
|
|
355
|
+
if (!existing) return null;
|
|
356
|
+
if (source.scopeKey) {
|
|
357
|
+
const scopeValue = scope[source.scopeKey];
|
|
358
|
+
const idColumn = source.idColumn ?? "id";
|
|
359
|
+
const existingId = existing[idColumn];
|
|
360
|
+
if (scopeValue && existingId && scopeValue === String(existingId)) {
|
|
361
|
+
return null;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
return source.name;
|
|
365
|
+
});
|
|
366
|
+
const results = await Promise.all(checks);
|
|
367
|
+
return !results.some((r) => r !== null);
|
|
368
|
+
}
|
|
150
369
|
async function check(identifier, scope = {}, options) {
|
|
151
370
|
const normalized = normalize(identifier);
|
|
152
371
|
if (!pattern.test(normalized)) {
|
|
@@ -196,16 +415,45 @@ function createNamespaceGuard(config, adapter) {
|
|
|
196
415
|
source: collision
|
|
197
416
|
};
|
|
198
417
|
if (config.suggest && !options?.skipSuggestions) {
|
|
199
|
-
const generate = config.suggest
|
|
418
|
+
const generate = resolveGenerator(config.suggest, pattern);
|
|
200
419
|
const max = config.suggest.max ?? 3;
|
|
201
420
|
const candidates = generate(normalized);
|
|
202
421
|
const suggestions = [];
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
422
|
+
const passedSync = candidates.filter(
|
|
423
|
+
(c) => pattern.test(c) && !reservedMap.has(c)
|
|
424
|
+
);
|
|
425
|
+
for (let i = 0; i < passedSync.length && suggestions.length < max; i += max) {
|
|
426
|
+
const batch = passedSync.slice(i, i + max);
|
|
427
|
+
let validated = batch;
|
|
428
|
+
if (validators.length > 0) {
|
|
429
|
+
const validationResults = await Promise.all(
|
|
430
|
+
batch.map(async (c) => {
|
|
431
|
+
for (const validator of validators) {
|
|
432
|
+
try {
|
|
433
|
+
const rejection = await validator(c);
|
|
434
|
+
if (rejection) return null;
|
|
435
|
+
} catch {
|
|
436
|
+
return null;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
return c;
|
|
440
|
+
})
|
|
441
|
+
);
|
|
442
|
+
validated = validationResults.filter(
|
|
443
|
+
(c) => c !== null
|
|
444
|
+
);
|
|
445
|
+
}
|
|
446
|
+
if (validated.length > 0) {
|
|
447
|
+
const dbResults = await Promise.all(
|
|
448
|
+
validated.map(async (c) => ({
|
|
449
|
+
candidate: c,
|
|
450
|
+
available: await checkDbOnly(c, scope)
|
|
451
|
+
}))
|
|
452
|
+
);
|
|
453
|
+
for (const { candidate, available } of dbResults) {
|
|
454
|
+
if (suggestions.length >= max) break;
|
|
455
|
+
if (available) suggestions.push(candidate);
|
|
456
|
+
}
|
|
209
457
|
}
|
|
210
458
|
}
|
|
211
459
|
if (suggestions.length > 0) {
|
package/dist/index.mjs
CHANGED
|
@@ -7,29 +7,51 @@ var DEFAULT_MESSAGES = {
|
|
|
7
7
|
};
|
|
8
8
|
function extractMaxLength(pattern) {
|
|
9
9
|
const testStrings = ["a", "1", "a1", "a-1"];
|
|
10
|
-
|
|
10
|
+
let lo = 1;
|
|
11
|
+
let hi = 100;
|
|
12
|
+
let best = 30;
|
|
13
|
+
while (lo <= hi) {
|
|
14
|
+
const mid = Math.floor((lo + hi) / 2);
|
|
15
|
+
let anyMatch = false;
|
|
11
16
|
for (const chars of testStrings) {
|
|
12
|
-
const s = chars.repeat(Math.ceil(
|
|
13
|
-
if (pattern.test(s))
|
|
17
|
+
const s = chars.repeat(Math.ceil(mid / chars.length)).slice(0, mid);
|
|
18
|
+
if (pattern.test(s)) {
|
|
19
|
+
anyMatch = true;
|
|
20
|
+
break;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
if (anyMatch) {
|
|
24
|
+
best = mid;
|
|
25
|
+
lo = mid + 1;
|
|
26
|
+
} else {
|
|
27
|
+
hi = mid - 1;
|
|
14
28
|
}
|
|
15
29
|
}
|
|
16
|
-
return
|
|
30
|
+
return best;
|
|
17
31
|
}
|
|
18
32
|
function createDefaultSuggest(pattern) {
|
|
19
33
|
const maxLen = extractMaxLength(pattern);
|
|
20
34
|
return (identifier) => {
|
|
35
|
+
const seen = /* @__PURE__ */ new Set();
|
|
21
36
|
const candidates = [];
|
|
22
37
|
for (let i = 1; i <= 9; i++) {
|
|
23
38
|
const hyphenated = `${identifier}-${i}`;
|
|
24
|
-
if (hyphenated.length <= maxLen)
|
|
39
|
+
if (hyphenated.length <= maxLen) {
|
|
40
|
+
seen.add(hyphenated);
|
|
41
|
+
candidates.push(hyphenated);
|
|
42
|
+
}
|
|
25
43
|
const compact = `${identifier}${i}`;
|
|
26
|
-
if (compact.length <= maxLen)
|
|
44
|
+
if (compact.length <= maxLen) {
|
|
45
|
+
seen.add(compact);
|
|
46
|
+
candidates.push(compact);
|
|
47
|
+
}
|
|
27
48
|
}
|
|
28
49
|
if (identifier.length >= maxLen - 1) {
|
|
29
50
|
for (let i = 1; i <= 9; i++) {
|
|
30
51
|
const suffix = String(i);
|
|
31
52
|
const truncated = identifier.slice(0, maxLen - suffix.length) + suffix;
|
|
32
|
-
if (truncated !== identifier && !
|
|
53
|
+
if (truncated !== identifier && !seen.has(truncated)) {
|
|
54
|
+
seen.add(truncated);
|
|
33
55
|
candidates.push(truncated);
|
|
34
56
|
}
|
|
35
57
|
}
|
|
@@ -37,6 +59,184 @@ function createDefaultSuggest(pattern) {
|
|
|
37
59
|
return candidates;
|
|
38
60
|
};
|
|
39
61
|
}
|
|
62
|
+
var SUFFIX_WORDS = ["dev", "io", "app", "hq", "pro", "team", "labs", "hub", "go", "one"];
|
|
63
|
+
function createRandomDigitsStrategy(pattern) {
|
|
64
|
+
const maxLen = extractMaxLength(pattern);
|
|
65
|
+
return (identifier) => {
|
|
66
|
+
const seen = /* @__PURE__ */ new Set();
|
|
67
|
+
const candidates = [];
|
|
68
|
+
for (let i = 0; i < 15; i++) {
|
|
69
|
+
const digits = String(Math.floor(100 + Math.random() * 9900));
|
|
70
|
+
const candidate = `${identifier}-${digits}`;
|
|
71
|
+
if (candidate.length <= maxLen && !seen.has(candidate)) {
|
|
72
|
+
seen.add(candidate);
|
|
73
|
+
candidates.push(candidate);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return candidates;
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
function createSuffixWordsStrategy(pattern) {
|
|
80
|
+
const maxLen = extractMaxLength(pattern);
|
|
81
|
+
return (identifier) => {
|
|
82
|
+
const candidates = [];
|
|
83
|
+
for (const word of SUFFIX_WORDS) {
|
|
84
|
+
const candidate = `${identifier}-${word}`;
|
|
85
|
+
if (candidate.length <= maxLen) {
|
|
86
|
+
candidates.push(candidate);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return candidates;
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
function createShortRandomStrategy(pattern) {
|
|
93
|
+
const maxLen = extractMaxLength(pattern);
|
|
94
|
+
const chars = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
95
|
+
return (identifier) => {
|
|
96
|
+
const seen = /* @__PURE__ */ new Set();
|
|
97
|
+
const candidates = [];
|
|
98
|
+
for (let i = 0; i < 10; i++) {
|
|
99
|
+
let suffix = "";
|
|
100
|
+
for (let j = 0; j < 3; j++) {
|
|
101
|
+
suffix += chars[Math.floor(Math.random() * chars.length)];
|
|
102
|
+
}
|
|
103
|
+
const candidate = `${identifier}-${suffix}`;
|
|
104
|
+
if (candidate.length <= maxLen && !seen.has(candidate)) {
|
|
105
|
+
seen.add(candidate);
|
|
106
|
+
candidates.push(candidate);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return candidates;
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
function createScrambleStrategy(_pattern) {
|
|
113
|
+
return (identifier) => {
|
|
114
|
+
const seen = /* @__PURE__ */ new Set();
|
|
115
|
+
const candidates = [];
|
|
116
|
+
const chars = identifier.split("");
|
|
117
|
+
for (let i = 0; i < chars.length - 1; i++) {
|
|
118
|
+
if (chars[i] !== chars[i + 1]) {
|
|
119
|
+
const swapped = [...chars];
|
|
120
|
+
[swapped[i], swapped[i + 1]] = [swapped[i + 1], swapped[i]];
|
|
121
|
+
const candidate = swapped.join("");
|
|
122
|
+
if (candidate !== identifier && !seen.has(candidate)) {
|
|
123
|
+
seen.add(candidate);
|
|
124
|
+
candidates.push(candidate);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return candidates;
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
function createSimilarStrategy(pattern) {
|
|
132
|
+
const maxLen = extractMaxLength(pattern);
|
|
133
|
+
const nearby = {
|
|
134
|
+
a: "sqwz",
|
|
135
|
+
b: "vngh",
|
|
136
|
+
c: "xdfv",
|
|
137
|
+
d: "sfce",
|
|
138
|
+
e: "wrd",
|
|
139
|
+
f: "dgcv",
|
|
140
|
+
g: "fhtb",
|
|
141
|
+
h: "gjyn",
|
|
142
|
+
i: "uko",
|
|
143
|
+
j: "hknm",
|
|
144
|
+
k: "jli",
|
|
145
|
+
l: "kop",
|
|
146
|
+
m: "njk",
|
|
147
|
+
n: "bmhj",
|
|
148
|
+
o: "ipl",
|
|
149
|
+
p: "ol",
|
|
150
|
+
q: "wa",
|
|
151
|
+
r: "eft",
|
|
152
|
+
s: "adwz",
|
|
153
|
+
t: "rgy",
|
|
154
|
+
u: "yij",
|
|
155
|
+
v: "cfgb",
|
|
156
|
+
w: "qase",
|
|
157
|
+
x: "zsdc",
|
|
158
|
+
y: "tuh",
|
|
159
|
+
z: "xas",
|
|
160
|
+
"0": "19",
|
|
161
|
+
"1": "02",
|
|
162
|
+
"2": "13",
|
|
163
|
+
"3": "24",
|
|
164
|
+
"4": "35",
|
|
165
|
+
"5": "46",
|
|
166
|
+
"6": "57",
|
|
167
|
+
"7": "68",
|
|
168
|
+
"8": "79",
|
|
169
|
+
"9": "80"
|
|
170
|
+
};
|
|
171
|
+
const prefixes = ["the", "my", "x", "i"];
|
|
172
|
+
const suffixes = ["x", "o", "i", "z"];
|
|
173
|
+
return (identifier) => {
|
|
174
|
+
const seen = /* @__PURE__ */ new Set();
|
|
175
|
+
const candidates = [];
|
|
176
|
+
function add(c) {
|
|
177
|
+
if (c.length >= 2 && c.length <= maxLen && c !== identifier && pattern.test(c) && !seen.has(c)) {
|
|
178
|
+
seen.add(c);
|
|
179
|
+
candidates.push(c);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
for (let i = 0; i < identifier.length; i++) {
|
|
183
|
+
add(identifier.slice(0, i) + identifier.slice(i + 1));
|
|
184
|
+
}
|
|
185
|
+
for (let i = 0; i < identifier.length; i++) {
|
|
186
|
+
const ch = identifier[i];
|
|
187
|
+
const neighbours = nearby[ch] ?? "";
|
|
188
|
+
for (const n of neighbours) {
|
|
189
|
+
add(identifier.slice(0, i) + n + identifier.slice(i + 1));
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
for (const p of prefixes) {
|
|
193
|
+
add(p + identifier);
|
|
194
|
+
}
|
|
195
|
+
for (const s of suffixes) {
|
|
196
|
+
add(identifier + s);
|
|
197
|
+
}
|
|
198
|
+
return candidates;
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
function createStrategy(name, pattern) {
|
|
202
|
+
switch (name) {
|
|
203
|
+
case "sequential":
|
|
204
|
+
return createDefaultSuggest(pattern);
|
|
205
|
+
case "random-digits":
|
|
206
|
+
return createRandomDigitsStrategy(pattern);
|
|
207
|
+
case "suffix-words":
|
|
208
|
+
return createSuffixWordsStrategy(pattern);
|
|
209
|
+
case "short-random":
|
|
210
|
+
return createShortRandomStrategy(pattern);
|
|
211
|
+
case "scramble":
|
|
212
|
+
return createScrambleStrategy(pattern);
|
|
213
|
+
case "similar":
|
|
214
|
+
return createSimilarStrategy(pattern);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
function resolveGenerator(suggest, pattern) {
|
|
218
|
+
if (suggest?.generate) return suggest.generate;
|
|
219
|
+
const strategyInput = suggest?.strategy ?? ["sequential", "random-digits"];
|
|
220
|
+
if (typeof strategyInput === "function") return strategyInput;
|
|
221
|
+
const names = Array.isArray(strategyInput) ? strategyInput : [strategyInput];
|
|
222
|
+
const generators = names.map((name) => createStrategy(name, pattern));
|
|
223
|
+
if (generators.length === 1) return generators[0];
|
|
224
|
+
return (identifier) => {
|
|
225
|
+
const lists = generators.map((g) => g(identifier));
|
|
226
|
+
const seen = /* @__PURE__ */ new Set();
|
|
227
|
+
const result = [];
|
|
228
|
+
const maxListLen = Math.max(...lists.map((l) => l.length));
|
|
229
|
+
for (let i = 0; i < maxListLen; i++) {
|
|
230
|
+
for (const list of lists) {
|
|
231
|
+
if (i < list.length && !seen.has(list[i])) {
|
|
232
|
+
seen.add(list[i]);
|
|
233
|
+
result.push(list[i]);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return result;
|
|
238
|
+
};
|
|
239
|
+
}
|
|
40
240
|
function normalize(raw) {
|
|
41
241
|
return raw.trim().toLowerCase().replace(/^@+/, "");
|
|
42
242
|
}
|
|
@@ -44,17 +244,16 @@ function createProfanityValidator(words, options) {
|
|
|
44
244
|
const message = options?.message ?? "That name is not allowed.";
|
|
45
245
|
const checkSubstrings = options?.checkSubstrings ?? true;
|
|
46
246
|
const wordSet = new Set(words.map((w) => w.toLowerCase()));
|
|
247
|
+
const substringRegex = checkSubstrings && wordSet.size > 0 ? new RegExp(
|
|
248
|
+
Array.from(wordSet).map((w) => w.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|")
|
|
249
|
+
) : null;
|
|
47
250
|
return async (value) => {
|
|
48
251
|
const normalized = value.toLowerCase();
|
|
49
252
|
if (wordSet.has(normalized)) {
|
|
50
253
|
return { available: false, message };
|
|
51
254
|
}
|
|
52
|
-
if (
|
|
53
|
-
|
|
54
|
-
if (normalized.includes(word)) {
|
|
55
|
-
return { available: false, message };
|
|
56
|
-
}
|
|
57
|
-
}
|
|
255
|
+
if (substringRegex && substringRegex.test(normalized)) {
|
|
256
|
+
return { available: false, message };
|
|
58
257
|
}
|
|
59
258
|
return null;
|
|
60
259
|
};
|
|
@@ -94,6 +293,8 @@ function createNamespaceGuard(config, adapter) {
|
|
|
94
293
|
const cached = cacheMap.get(key);
|
|
95
294
|
if (cached && cached.expires > now) {
|
|
96
295
|
cacheHits++;
|
|
296
|
+
cacheMap.delete(key);
|
|
297
|
+
cacheMap.set(key, cached);
|
|
97
298
|
return cached.promise;
|
|
98
299
|
}
|
|
99
300
|
cacheMisses++;
|
|
@@ -121,6 +322,24 @@ function createNamespaceGuard(config, adapter) {
|
|
|
121
322
|
}
|
|
122
323
|
return null;
|
|
123
324
|
}
|
|
325
|
+
async function checkDbOnly(value, scope) {
|
|
326
|
+
const findOptions = config.caseInsensitive ? { caseInsensitive: true } : void 0;
|
|
327
|
+
const checks = config.sources.map(async (source) => {
|
|
328
|
+
const existing = await cachedFindOne(source, value, findOptions);
|
|
329
|
+
if (!existing) return null;
|
|
330
|
+
if (source.scopeKey) {
|
|
331
|
+
const scopeValue = scope[source.scopeKey];
|
|
332
|
+
const idColumn = source.idColumn ?? "id";
|
|
333
|
+
const existingId = existing[idColumn];
|
|
334
|
+
if (scopeValue && existingId && scopeValue === String(existingId)) {
|
|
335
|
+
return null;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
return source.name;
|
|
339
|
+
});
|
|
340
|
+
const results = await Promise.all(checks);
|
|
341
|
+
return !results.some((r) => r !== null);
|
|
342
|
+
}
|
|
124
343
|
async function check(identifier, scope = {}, options) {
|
|
125
344
|
const normalized = normalize(identifier);
|
|
126
345
|
if (!pattern.test(normalized)) {
|
|
@@ -170,16 +389,45 @@ function createNamespaceGuard(config, adapter) {
|
|
|
170
389
|
source: collision
|
|
171
390
|
};
|
|
172
391
|
if (config.suggest && !options?.skipSuggestions) {
|
|
173
|
-
const generate = config.suggest
|
|
392
|
+
const generate = resolveGenerator(config.suggest, pattern);
|
|
174
393
|
const max = config.suggest.max ?? 3;
|
|
175
394
|
const candidates = generate(normalized);
|
|
176
395
|
const suggestions = [];
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
396
|
+
const passedSync = candidates.filter(
|
|
397
|
+
(c) => pattern.test(c) && !reservedMap.has(c)
|
|
398
|
+
);
|
|
399
|
+
for (let i = 0; i < passedSync.length && suggestions.length < max; i += max) {
|
|
400
|
+
const batch = passedSync.slice(i, i + max);
|
|
401
|
+
let validated = batch;
|
|
402
|
+
if (validators.length > 0) {
|
|
403
|
+
const validationResults = await Promise.all(
|
|
404
|
+
batch.map(async (c) => {
|
|
405
|
+
for (const validator of validators) {
|
|
406
|
+
try {
|
|
407
|
+
const rejection = await validator(c);
|
|
408
|
+
if (rejection) return null;
|
|
409
|
+
} catch {
|
|
410
|
+
return null;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
return c;
|
|
414
|
+
})
|
|
415
|
+
);
|
|
416
|
+
validated = validationResults.filter(
|
|
417
|
+
(c) => c !== null
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
if (validated.length > 0) {
|
|
421
|
+
const dbResults = await Promise.all(
|
|
422
|
+
validated.map(async (c) => ({
|
|
423
|
+
candidate: c,
|
|
424
|
+
available: await checkDbOnly(c, scope)
|
|
425
|
+
}))
|
|
426
|
+
);
|
|
427
|
+
for (const { candidate, available } of dbResults) {
|
|
428
|
+
if (suggestions.length >= max) break;
|
|
429
|
+
if (available) suggestions.push(candidate);
|
|
430
|
+
}
|
|
183
431
|
}
|
|
184
432
|
}
|
|
185
433
|
if (suggestions.length > 0) {
|