@7365admin1/core 3.41.0 → 3.41.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/CHANGELOG.md +27 -0
- package/dist/index.js +61 -12
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +61 -12
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/test/site-name.util.test.mjs +146 -0
package/dist/index.mjs
CHANGED
|
@@ -11249,8 +11249,9 @@ function useSiteRepo() {
|
|
|
11249
11249
|
} catch (error2) {
|
|
11250
11250
|
throw new BadRequestError18("Invalid org ID format.");
|
|
11251
11251
|
}
|
|
11252
|
+
const escapedName = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
11252
11253
|
const query = {
|
|
11253
|
-
name: { $regex: new RegExp(`^${
|
|
11254
|
+
name: { $regex: new RegExp(`^${escapedName}$`, "i") },
|
|
11254
11255
|
// Case-insensitive exact match
|
|
11255
11256
|
orgId,
|
|
11256
11257
|
status: { $ne: "deleted" }
|
|
@@ -32346,7 +32347,57 @@ import {
|
|
|
32346
32347
|
useAtlas as useAtlas46,
|
|
32347
32348
|
NotFoundError as NotFoundError20
|
|
32348
32349
|
} from "@7365admin1/node-server-utils";
|
|
32350
|
+
|
|
32351
|
+
// src/utils/site-name.util.ts
|
|
32349
32352
|
import levenshtein from "fast-levenshtein";
|
|
32353
|
+
var ROMAN = /^(?=[ivx])(x{0,3})(ix|iv|v?i{0,3})$/;
|
|
32354
|
+
var ROMAN_VALUE = { i: 1, v: 5, x: 10 };
|
|
32355
|
+
function normalizeSiteName(name) {
|
|
32356
|
+
return String(name ?? "").normalize("NFKC").toLowerCase().replace(/[‘’']/g, "").replace(/[^a-z0-9]+/g, " ").trim();
|
|
32357
|
+
}
|
|
32358
|
+
function romanToNumber(token) {
|
|
32359
|
+
let total = 0;
|
|
32360
|
+
for (let i = 0; i < token.length; i++) {
|
|
32361
|
+
const value = ROMAN_VALUE[token[i]];
|
|
32362
|
+
const next = ROMAN_VALUE[token[i + 1]];
|
|
32363
|
+
total += next && next > value ? -value : value;
|
|
32364
|
+
}
|
|
32365
|
+
return total;
|
|
32366
|
+
}
|
|
32367
|
+
function distinguishingTokens(normalized) {
|
|
32368
|
+
if (!normalized)
|
|
32369
|
+
return [];
|
|
32370
|
+
return normalized.split(" ").filter((t) => /^\d+$/.test(t) || ROMAN.test(t) || /^[a-z]$/.test(t)).map((t) => ROMAN.test(t) ? String(romanToNumber(t)) : t);
|
|
32371
|
+
}
|
|
32372
|
+
function matchSiteName(candidate, existing) {
|
|
32373
|
+
const a = normalizeSiteName(candidate);
|
|
32374
|
+
const b = normalizeSiteName(existing);
|
|
32375
|
+
if (!a || !b)
|
|
32376
|
+
return null;
|
|
32377
|
+
if (a === b)
|
|
32378
|
+
return "duplicate";
|
|
32379
|
+
const tokensA = distinguishingTokens(a).join(" ");
|
|
32380
|
+
const tokensB = distinguishingTokens(b).join(" ");
|
|
32381
|
+
if (tokensA !== tokensB)
|
|
32382
|
+
return null;
|
|
32383
|
+
return levenshtein.get(a, b) <= 2 ? "near-duplicate" : null;
|
|
32384
|
+
}
|
|
32385
|
+
function findSiteNameClash(candidate, existingNames) {
|
|
32386
|
+
for (const name of existingNames) {
|
|
32387
|
+
const match = matchSiteName(candidate, name);
|
|
32388
|
+
if (match)
|
|
32389
|
+
return { name, match };
|
|
32390
|
+
}
|
|
32391
|
+
return null;
|
|
32392
|
+
}
|
|
32393
|
+
function siteNameClashMessage(existingName, match) {
|
|
32394
|
+
if (match === "duplicate") {
|
|
32395
|
+
return `A site called "${existingName}" already exists in this organisation. Give the new site a different name, or edit "${existingName}" instead.`;
|
|
32396
|
+
}
|
|
32397
|
+
return `This name is within a character or two of "${existingName}", which already exists in this organisation. If it is the same property, edit "${existingName}" instead. If it is a different one, include what tells them apart \u2014 the block, tower, phase, or number \u2014 and save again.`;
|
|
32398
|
+
}
|
|
32399
|
+
|
|
32400
|
+
// src/services/customer-site.service.ts
|
|
32350
32401
|
function useCustomerSiteService() {
|
|
32351
32402
|
const { add: _add, updateCustomerSiteById, getById: _getById } = useCustomerSiteRepo();
|
|
32352
32403
|
const {
|
|
@@ -32368,20 +32419,18 @@ function useCustomerSiteService() {
|
|
|
32368
32419
|
const exactMatches = await getSiteByExactName(value.name, value.siteOrg);
|
|
32369
32420
|
if (exactMatches && exactMatches.length > 0) {
|
|
32370
32421
|
throw new BadRequestError91(
|
|
32371
|
-
|
|
32422
|
+
siteNameClashMessage(exactMatches[0].name, "duplicate")
|
|
32372
32423
|
);
|
|
32373
32424
|
}
|
|
32374
|
-
const threshold = 2;
|
|
32375
32425
|
const sites = await getSiteByName(value.name);
|
|
32376
|
-
|
|
32377
|
-
|
|
32378
|
-
|
|
32379
|
-
|
|
32380
|
-
|
|
32381
|
-
|
|
32382
|
-
|
|
32383
|
-
|
|
32384
|
-
}
|
|
32426
|
+
const candidates = (sites ?? []).filter(
|
|
32427
|
+
(doc) => doc.orgId && doc.name && doc.orgId.toString() === value.siteOrg.toString()
|
|
32428
|
+
).map((doc) => doc.name);
|
|
32429
|
+
const clash = findSiteNameClash(value.name, candidates);
|
|
32430
|
+
if (clash) {
|
|
32431
|
+
throw new BadRequestError91(
|
|
32432
|
+
siteNameClashMessage(clash.name, clash.match)
|
|
32433
|
+
);
|
|
32385
32434
|
}
|
|
32386
32435
|
const siteId = await createSite(
|
|
32387
32436
|
{
|