LLD: Fetch AI Audit Installations & Remarks APIs
1. Overview
Two paginated GET endpoints that read from the intermediate AI audit collections created by the Run AI Audit API (POST /audit/transformers/:id/ai-audit). These power the two tables on the "Audit with AI" page:
- Table 1: Installation Tagging — shows all installations involved in the AI audit with their GPS-based tagging suggestions (TAG / UNTAG / NO_CHANGE)
- Table 2: Installations with Remarks — shows installations that have billing remarks (MNR, unbilled, vacant, etc.) with average consumption suggestions
Both endpoints read from the ai-audit-installation collection. Neither endpoint modifies any data.
Related LLDs
| LLD | Relationship |
|---|---|
LLD-ai-audit-schemas.md | Data model — defines ai-audit-result and ai-audit-installation schemas |
LLD-ai-audit-run.md | The Run AI Audit API that creates the data these endpoints read |
LLD-fetch-installations.md | Existing installations fetch endpoint (pagination pattern reference) |
2. Endpoints
| Route | Method | Purpose |
|---|---|---|
GET /audit/transformers/:id/ai-audit/installations | GET | Table 1 — Installation Tagging |
GET /audit/transformers/:id/ai-audit/remarks | GET | Table 2 — Installations with Remarks |
Both routes resolve user type from request.user.userType. AE users are validated against their assigned section.
3. Endpoint 1 — Fetch AI Audit Installations (Tagging Table)
3.1 Request
3.1.1 Headers
| Header | Required | Description |
|---|---|---|
Authorization | Yes | Bearer <sessionToken> |
3.1.2 Path Parameters
| Param | Type | Required | Description |
|---|---|---|---|
id | string | Yes | The id field of the transformer document (same as tcId in ai-audit-result) |
3.1.3 Query Parameters
| Param | Type | Required | Default | Description |
|---|---|---|---|---|
month | string | No | Current month (YYYY-MM) | Month to fetch AI audit installations for |
page | integer | No | 1 | Page number (1-indexed) |
limit | integer | No | 10 | Items per page (max 100) |
filter | string | No | "ALL" | Filter by AI suggestion type (see mapping below) |
search | string | No | — | Search by RR Number (case-insensitive partial match) |
Filter mapping:
| Filter Value | Query Condition |
|---|---|
ALL | No filter — return all installations |
CHANGES_SUGGESTED | aiSuggestion is TAG or UNTAG |
NO_CHANGES_REQUIRED | aiSuggestion is NO_CHANGE |
3.2 Response
3.2.1 Success (200)
{
"success": true,
"message": "AI audit installations fetched successfully",
"data": {
"installations": [
{
"accountId": "ACC-001",
"installationId": "INST-001",
"rrNumber": "RR-12345",
"consumerName": "John Doe",
"gpsCoordinates": { "type": "Point", "coordinates": [74.8421, 12.8765] },
"distanceFromTC": 2340,
"withinRange": true,
"currentTaggingStatus": "TAGGED",
"aiSuggestion": "NO_CHANGE",
"sourceTcId": null,
"sourceTcNumber": null,
"consumption": 450,
"sanctionedLoad": { "kw": 5, "hp": 6.7 }
}
],
"pagination": {
"page": 1,
"limit": 10,
"totalDocs": 25,
"totalPages": 3
},
"filterCounts": {
"all": 25,
"changesSuggested": 8,
"noChangesRequired": 17
}
}
}
3.2.2 Not Found (404)
{
"success": false,
"message": "AI audit result not found"
}
3.2.3 Forbidden (403)
When AE requests installations for a TC outside their section:
{
"success": false,
"message": "TC does not belong to your assigned section"
}
3.3 Business Logic
3.3.1 Default Month
IF month not provided:
month = current date formatted as "YYYY-MM"
3.3.2 Find AI Audit Result
const aiAuditResult = await aiAuditResultModel
.findOne({ tcId: id, month })
.lean();
if (!aiAuditResult) {
return 404 — "AI audit result not found";
}
3.3.3 AE Section Validation
IF userType === "AE":
1. Use aiAuditResult.location.sectionCode (already on the result doc)
2. Fetch user doc → extract user.location.sectionCode
3. Verify aiAuditResult.location.sectionCode === user.location.sectionCode
4. If mismatch → 403 Forbidden
Note: The
ai-audit-resultdocument contains the fulllocationobject (copied from the transformer at audit time), so there is no need to separately fetch the transformer document for section validation.
3.3.4 Build Query Filter
const aiAuditResultId = aiAuditResult._id.toString();
const filter = { aiAuditResultId };
// Apply filter based on query param
if (filterParam === "CHANGES_SUGGESTED") {
filter.aiSuggestion = { $in: ["TAG", "UNTAG"] };
} else if (filterParam === "NO_CHANGES_REQUIRED") {
filter.aiSuggestion = "NO_CHANGE";
}
// "ALL" — no additional filter on aiSuggestion
// RR number search (case-insensitive partial match)
if (search) {
filter.rrNumber = { $regex: escapeRegex(search), $options: "i" };
}
3.3.5 DB Queries
Three parallel queries — data + count + filter counts:
const skip = (page - 1) * limit;
const projection = {
_id: 0,
__v: 0,
createdAt: 0,
updatedAt: 0,
aiAuditResultId: 0,
tcId: 0,
month: 0,
// Exclude Table 2 (remarks) fields — not needed for tagging table
hasRemark: 0,
remarkType: 0,
avgConsumption6Months: 0,
aiSuggestsConsumption: 0,
// Exclude raw billing flags
unbilled: 0,
mnr: 0,
vacant: 0,
doorLock: 0,
zeroConsumption: 0,
emin: 0,
emax: 0,
// Exclude revenue fields not needed
demand: 0,
collection: 0,
};
const [installations, totalDocs, filterCounts] = await Promise.all([
aiAuditInstallationModel
.find(filter, projection)
.sort({ distanceFromTC: 1 }) // closest first
.skip(skip)
.limit(limit)
.lean(),
aiAuditInstallationModel.countDocuments(filter),
// Filter counts — compute counts for all filter tabs
computeInstallationFilterCounts(aiAuditResultId, search),
]);
3.3.6 Filter Counts
Compute counts for the filter tab badges. If a search term is active, the counts should reflect the search-filtered subset.
async function computeInstallationFilterCounts(aiAuditResultId, search) {
const baseFilter = { aiAuditResultId };
if (search) {
baseFilter.rrNumber = { $regex: escapeRegex(search), $options: "i" };
}
const [all, changesSuggested, noChangesRequired] = await Promise.all([
aiAuditInstallationModel.countDocuments(baseFilter),
aiAuditInstallationModel.countDocuments({
...baseFilter,
aiSuggestion: { $in: ["TAG", "UNTAG"] },
}),
aiAuditInstallationModel.countDocuments({
...baseFilter,
aiSuggestion: "NO_CHANGE",
}),
]);
return { all, changesSuggested, noChangesRequired };
}
Alternative — single aggregation with $facet:
async function computeInstallationFilterCounts(aiAuditResultId, search) {
const matchStage = { aiAuditResultId };
if (search) {
matchStage.rrNumber = { $regex: escapeRegex(search), $options: "i" };
}
const [result] = await aiAuditInstallationModel.aggregate([
{ $match: matchStage },
{
$facet: {
all: [{ $count: "count" }],
changesSuggested: [
{ $match: { aiSuggestion: { $in: ["TAG", "UNTAG"] } } },
{ $count: "count" },
],
noChangesRequired: [
{ $match: { aiSuggestion: "NO_CHANGE" } },
{ $count: "count" },
],
},
},
]);
return {
all: result.all[0]?.count || 0,
changesSuggested: result.changesSuggested[0]?.count || 0,
noChangesRequired: result.noChangesRequired[0]?.count || 0,
};
}
Either approach is fine for demo scale (~200-500 docs per AI audit).
3.3.7 Pagination Response
{
page,
limit,
totalDocs,
totalPages: Math.ceil(totalDocs / limit),
}
4. Endpoint 2 — Fetch AI Audit Remarks (Remarks Table)
4.1 Request
4.1.1 Headers
| Header | Required | Description |
|---|---|---|
Authorization | Yes | Bearer <sessionToken> |
4.1.2 Path Parameters
Same as Endpoint 1.
| Param | Type | Required | Description |
|---|---|---|---|
id | string | Yes | The id field of the transformer document (same as tcId in ai-audit-result) |
4.1.3 Query Parameters
| Param | Type | Required | Default | Description |
|---|---|---|---|---|
month | string | No | Current month (YYYY-MM) | Month to fetch AI audit remarks for |
page | integer | No | 1 | Page number (1-indexed) |
limit | integer | No | 10 | Items per page (max 100) |
filter | string | No | "ALL" | Filter by remark type (see mapping below) |
search | string | No | — | Search by RR Number (case-insensitive partial match) |
Filter mapping:
| Filter Value | Query Condition |
|---|---|
ALL | hasRemark: true (all installations with any remark) |
UNBILLED | remarkType: "UNBILLED" |
MNR | remarkType: "MNR" |
VACANT | remarkType: "VACANT" |
ZERO_CONSUMPTION | remarkType: "ZERO_CONSUMPTION" |
DOORLOCK | remarkType: "DOORLOCK" |
ABNORMAL | remarkType: "ABNORMAL" |
SUBNORMAL | remarkType: "SUBNORMAL" |
4.2 Response
4.2.1 Success (200)
{
"success": true,
"message": "AI audit remarks fetched successfully",
"data": {
"installations": [
{
"accountId": "ACC-005",
"installationId": "INST-005",
"rrNumber": "RR-67890",
"consumerName": "Jane Smith",
"remarkType": "MNR",
"avgConsumption6Months": 4564,
"aiSuggestsConsumption": true,
"consumption": 0,
"withinRange": true,
"currentTaggingStatus": "TAGGED",
"aiSuggestion": "NO_CHANGE"
}
],
"pagination": {
"page": 1,
"limit": 10,
"totalDocs": 13,
"totalPages": 2
},
"filterCounts": {
"all": 13,
"unbilled": 3,
"mnr": 2,
"vacant": 1,
"zeroConsumption": 3,
"doorlock": 1,
"abnormal": 2,
"subnormal": 1
}
}
}
Important: The remarks response includes withinRange, currentTaggingStatus, and aiSuggestion fields. The frontend needs these to determine if an installation should be grayed out (e.g., if it was suggested to be untagged in Table 1, it should appear grayed out in Table 2).
4.2.2 Not Found (404)
{
"success": false,
"message": "AI audit result not found"
}
4.2.3 Forbidden (403)
{
"success": false,
"message": "TC does not belong to your assigned section"
}
4.3 Business Logic
4.3.1 Default Month
IF month not provided:
month = current date formatted as "YYYY-MM"
4.3.2 Find AI Audit Result
Same as Endpoint 1:
const aiAuditResult = await aiAuditResultModel
.findOne({ tcId: id, month })
.lean();
if (!aiAuditResult) {
return 404 — "AI audit result not found";
}
4.3.3 AE Section Validation
Same pattern as Endpoint 1 — use aiAuditResult.location.sectionCode for validation.
4.3.4 Build Query Filter
const aiAuditResultId = aiAuditResult._id.toString();
const filter = {
aiAuditResultId,
hasRemark: true, // always — this table only shows remarked installations
};
// Apply remarkType filter if not "ALL"
const remarkTypeMap = {
UNBILLED: "UNBILLED",
MNR: "MNR",
VACANT: "VACANT",
ZERO_CONSUMPTION: "ZERO_CONSUMPTION",
DOORLOCK: "DOORLOCK",
ABNORMAL: "ABNORMAL",
SUBNORMAL: "SUBNORMAL",
};
if (filterParam !== "ALL" && remarkTypeMap[filterParam]) {
filter.remarkType = remarkTypeMap[filterParam];
}
// RR number search (case-insensitive partial match)
if (search) {
filter.rrNumber = { $regex: escapeRegex(search), $options: "i" };
}
4.3.5 DB Queries
Three parallel queries — data + count + filter counts:
const skip = (page - 1) * limit;
const projection = {
_id: 0,
__v: 0,
createdAt: 0,
updatedAt: 0,
aiAuditResultId: 0,
tcId: 0,
month: 0,
// Exclude GPS coordinate details (not needed in remarks table)
gpsCoordinates: 0,
distanceFromTC: 0,
// Exclude source TC fields (not needed in remarks table)
sourceTcId: 0,
sourceTcNumber: 0,
// Exclude raw billing flags (remarkType is the derived summary)
unbilled: 0,
mnr: 0,
vacant: 0,
doorLock: 0,
zeroConsumption: 0,
emin: 0,
emax: 0,
// Exclude revenue fields
demand: 0,
collection: 0,
// Exclude hasRemark (redundant — all docs in this response have it)
hasRemark: 0,
};
const [installations, totalDocs, filterCounts] = await Promise.all([
aiAuditInstallationModel
.find(filter, projection)
.sort({ remarkType: 1, rrNumber: 1 }) // group by remark type, then by RR number
.skip(skip)
.limit(limit)
.lean(),
aiAuditInstallationModel.countDocuments(filter),
// Filter counts — compute counts for all remark type tabs
computeRemarkFilterCounts(aiAuditResultId, search),
]);
4.3.6 Filter Counts
Compute counts for each remark type tab. If a search term is active, the counts reflect the search-filtered subset.
async function computeRemarkFilterCounts(aiAuditResultId, search) {
const baseFilter = { aiAuditResultId, hasRemark: true };
if (search) {
baseFilter.rrNumber = { $regex: escapeRegex(search), $options: "i" };
}
const [all, unbilled, mnr, vacant, zeroConsumption, doorlock, abnormal, subnormal] =
await Promise.all([
aiAuditInstallationModel.countDocuments(baseFilter),
aiAuditInstallationModel.countDocuments({ ...baseFilter, remarkType: "UNBILLED" }),
aiAuditInstallationModel.countDocuments({ ...baseFilter, remarkType: "MNR" }),
aiAuditInstallationModel.countDocuments({ ...baseFilter, remarkType: "VACANT" }),
aiAuditInstallationModel.countDocuments({ ...baseFilter, remarkType: "ZERO_CONSUMPTION" }),
aiAuditInstallationModel.countDocuments({ ...baseFilter, remarkType: "DOORLOCK" }),
aiAuditInstallationModel.countDocuments({ ...baseFilter, remarkType: "ABNORMAL" }),
aiAuditInstallationModel.countDocuments({ ...baseFilter, remarkType: "SUBNORMAL" }),
]);
return { all, unbilled, mnr, vacant, zeroConsumption, doorlock, abnormal, subnormal };
}
Alternative — single aggregation with $facet:
async function computeRemarkFilterCounts(aiAuditResultId, search) {
const matchStage = { aiAuditResultId, hasRemark: true };
if (search) {
matchStage.rrNumber = { $regex: escapeRegex(search), $options: "i" };
}
const [result] = await aiAuditInstallationModel.aggregate([
{ $match: matchStage },
{
$facet: {
all: [{ $count: "count" }],
unbilled: [{ $match: { remarkType: "UNBILLED" } }, { $count: "count" }],
mnr: [{ $match: { remarkType: "MNR" } }, { $count: "count" }],
vacant: [{ $match: { remarkType: "VACANT" } }, { $count: "count" }],
zeroConsumption: [{ $match: { remarkType: "ZERO_CONSUMPTION" } }, { $count: "count" }],
doorlock: [{ $match: { remarkType: "DOORLOCK" } }, { $count: "count" }],
abnormal: [{ $match: { remarkType: "ABNORMAL" } }, { $count: "count" }],
subnormal: [{ $match: { remarkType: "SUBNORMAL" } }, { $count: "count" }],
},
},
]);
return {
all: result.all[0]?.count || 0,
unbilled: result.unbilled[0]?.count || 0,
mnr: result.mnr[0]?.count || 0,
vacant: result.vacant[0]?.count || 0,
zeroConsumption: result.zeroConsumption[0]?.count || 0,
doorlock: result.doorlock[0]?.count || 0,
abnormal: result.abnormal[0]?.count || 0,
subnormal: result.subnormal[0]?.count || 0,
};
}
4.3.7 Pagination Response
{
page,
limit,
totalDocs,
totalPages: Math.ceil(totalDocs / limit),
}
5. Validation Schemas (AJV)
5.1 Endpoint 1 — Fetch AI Audit Installations
5.1.1 Path Parameters Schema
{
type: "object",
properties: {
id: {
type: "string",
transform: ["trim"],
}
},
required: ["id"]
}
5.1.2 Query String Schema
{
type: "object",
properties: {
month: {
type: "string",
pattern: "^\\d{4}-(0[1-9]|1[0-2])$",
errorMessage: { pattern: "month must be in YYYY-MM format" }
},
page: {
type: "integer",
minimum: 1,
default: 1
},
limit: {
type: "integer",
minimum: 1,
maximum: 100,
default: 10
},
filter: {
type: "string",
enum: ["ALL", "CHANGES_SUGGESTED", "NO_CHANGES_REQUIRED"],
default: "ALL"
},
search: {
type: "string",
transform: ["trim"],
minLength: 1
}
},
additionalProperties: false,
errorMessage: {
additionalProperties: "Unsupported properties in query parameters"
}
}
5.1.3 Response Serialization
{
"2xx": responseSerializer("2xx", {
type: "object",
properties: {
installations: {
type: "array",
items: {
type: "object",
properties: {
accountId: { type: "string" },
installationId: { type: ["string", "null"] },
rrNumber: { type: ["string", "null"] },
consumerName: { type: ["string", "null"] },
gpsCoordinates: {
type: "object",
properties: {
type: { type: "string" },
coordinates: { type: "array", items: { type: "number" } },
},
},
distanceFromTC: { type: ["number", "null"] },
withinRange: { type: ["boolean", "null"] },
currentTaggingStatus: { type: ["string", "null"] },
aiSuggestion: { type: ["string", "null"] },
sourceTcId: { type: ["string", "null"] },
sourceTcNumber: { type: ["string", "null"] },
consumption: { type: ["number", "null"] },
sanctionedLoad: {
type: "object",
properties: {
kw: { type: ["number", "null"] },
hp: { type: ["number", "null"] },
},
},
},
},
},
pagination: {
type: "object",
properties: {
page: { type: "integer" },
limit: { type: "integer" },
totalDocs: { type: "integer" },
totalPages: { type: "integer" },
},
},
filterCounts: {
type: "object",
properties: {
all: { type: "integer" },
changesSuggested: { type: "integer" },
noChangesRequired: { type: "integer" },
},
},
},
}),
"4xx": responseSerializer("4xx"),
"5xx": responseSerializer("5xx"),
}
5.2 Endpoint 2 — Fetch AI Audit Remarks
5.2.1 Path Parameters Schema
Same as Endpoint 1.
{
type: "object",
properties: {
id: {
type: "string",
transform: ["trim"],
}
},
required: ["id"]
}
5.2.2 Query String Schema
{
type: "object",
properties: {
month: {
type: "string",
pattern: "^\\d{4}-(0[1-9]|1[0-2])$",
errorMessage: { pattern: "month must be in YYYY-MM format" }
},
page: {
type: "integer",
minimum: 1,
default: 1
},
limit: {
type: "integer",
minimum: 1,
maximum: 100,
default: 10
},
filter: {
type: "string",
enum: [
"ALL", "UNBILLED", "MNR", "VACANT",
"ZERO_CONSUMPTION", "DOORLOCK",
"ABNORMAL", "SUBNORMAL"
],
default: "ALL"
},
search: {
type: "string",
transform: ["trim"],
minLength: 1
}
},
additionalProperties: false,
errorMessage: {
additionalProperties: "Unsupported properties in query parameters"
}
}
5.2.3 Response Serialization
{
"2xx": responseSerializer("2xx", {
type: "object",
properties: {
installations: {
type: "array",
items: {
type: "object",
properties: {
accountId: { type: "string" },
installationId: { type: ["string", "null"] },
rrNumber: { type: ["string", "null"] },
consumerName: { type: ["string", "null"] },
remarkType: { type: ["string", "null"] },
avgConsumption6Months: { type: ["number", "null"] },
aiSuggestsConsumption: { type: "boolean" },
consumption: { type: ["number", "null"] },
withinRange: { type: ["boolean", "null"] },
currentTaggingStatus: { type: ["string", "null"] },
aiSuggestion: { type: ["string", "null"] },
sanctionedLoad: {
type: "object",
properties: {
kw: { type: ["number", "null"] },
hp: { type: ["number", "null"] },
},
},
},
},
},
pagination: {
type: "object",
properties: {
page: { type: "integer" },
limit: { type: "integer" },
totalDocs: { type: "integer" },
totalPages: { type: "integer" },
},
},
filterCounts: {
type: "object",
properties: {
all: { type: "integer" },
unbilled: { type: "integer" },
mnr: { type: "integer" },
vacant: { type: "integer" },
zeroConsumption: { type: "integer" },
doorlock: { type: "integer" },
abnormal: { type: "integer" },
subnormal: { type: "integer" },
},
},
},
}),
"4xx": responseSerializer("4xx"),
"5xx": responseSerializer("5xx"),
}
6. Route Config
6.1 Endpoint 1
{
routeId: "fetchAiAuditInstallations",
authentication: {
// No excludeFrom — requires full auth
},
authorization: {
action: "read",
resource: "audit"
}
}
6.2 Endpoint 2
{
routeId: "fetchAiAuditRemarks",
authentication: {
// No excludeFrom — requires full auth
},
authorization: {
action: "read",
resource: "audit"
}
}
7. File Structure
api/src/entities/audit/
audit.route.v1.js — Add two new route entries
controller/
fetchAuditSummary.js — Existing
fetchTransformers.js — Existing
fetchTransformerDetail.js — Existing
fetchInstallations.js — Existing
auditTC.js — Existing
fetchConsumptionTrend.js — Existing
fetchRevenueTrend.js — Existing
aiAuditTC.js — Existing (Run AI Audit)
fetchAiAuditInstallations.js — New: handler, schema, config exports
fetchAiAuditRemarks.js — New: handler, schema, config exports
helpers/
resolveLocationFilter.js — Existing (reused for AE section lookup)
8. Error Handling
| Scenario | Status | Error Type |
|---|---|---|
| Invalid month format | 400 | ValidationError (AJV) |
Invalid filter enum value | 400 | ValidationError (AJV) |
Invalid page or limit | 400 | ValidationError (AJV) |
| Invalid/expired session | 401 | UnauthorizedError |
| AE accessing TC outside their section | 403 | ForbiddenError |
| AI audit result not found for TC + month | 404 | ResourceUnavailableError |
| AE user doc not found | 500 | DatabaseError |
| DB connection failure | 500 | DatabaseError |
9. Performance Notes
9.1 DB Operations per Request
| Operation | Collection | Type | Count |
|---|---|---|---|
| Find AI audit result | ai-audit-result | Read | 1 |
| Find user (AE only) | user | Read | 0 or 1 |
| Find installations (paginated) | ai-audit-installation | Read | 1 |
| Count documents (for pagination) | ai-audit-installation | Read | 1 |
| Count documents (filter counts) | ai-audit-installation | Read | 3 (Endpoint 1) or 8 (Endpoint 2) |
Total — Endpoint 1: 5-6 reads, 0 writes Total — Endpoint 2: 10-11 reads, 0 writes
9.2 Index Usage
| Query | Index Hit |
|---|---|
aiAuditResultModel.findOne({ tcId, month }) | { tcId: 1, month: 1 } unique index |
find({ aiAuditResultId }) with sort on distanceFromTC | { aiAuditResultId: 1, aiSuggestion: 1 } — prefix match for filter; in-memory sort (acceptable for ~200-500 docs) |
find({ aiAuditResultId, hasRemark: true }) with sort | { aiAuditResultId: 1, hasRemark: 1 } — exact match |
countDocuments({ aiAuditResultId, aiSuggestion }) | { aiAuditResultId: 1, aiSuggestion: 1 } — exact match |
countDocuments({ aiAuditResultId, hasRemark, remarkType }) | { aiAuditResultId: 1, hasRemark: 1 } — prefix match, remarkType filtered in-memory |
9.3 Why Multiple countDocuments Instead of $facet?
Both approaches are provided in the business logic sections. For the demo data scale (~200-500 docs per AI audit result), multiple countDocuments calls run in parallel via Promise.all are comparably fast to a single $facet aggregation. Choose based on code readability preference.
9.4 Pagination
Offset-based (skip/limit). Acceptable since installations per AI audit are typically in the hundreds (average ~22 per TC in the section, ~20 TCs per section = ~200-500 total). Deep pagination is unlikely.
9.5 Latency Estimate
For an AI audit with ~300 installations:
- Find
ai-audit-result: ~5ms (indexed unique lookup) - Paginated find: ~10-20ms (indexed + skip/limit on small collection)
- Count queries (3 or 8 parallel): ~10-20ms total (indexed)
- Total: ~25-45ms
10. Regex Escape Utility
The search param is user input used in a $regex query. Reuse the same escapeRegex helper from fetchTransformers / fetchInstallations:
function escapeRegex(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}