@7365admin1/core 3.52.5 → 3.52.6
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 +28 -0
- package/dist/index.d.ts +47 -4
- package/dist/index.js +113 -37
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +112 -37
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/test/bulk-upsert-fail-closed.test.mjs +134 -0
- package/test/expired-vehicle-filter.test.mjs +114 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@7365admin1/core",
|
|
3
3
|
"license": "MIT",
|
|
4
|
-
"version": "3.52.
|
|
4
|
+
"version": "3.52.6",
|
|
5
5
|
"author": "7365admin1",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"module": "dist/index.mjs",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"build": "tsup src/index.ts --format cjs,esm --dts && shx cp -r src/public dist && shx cp -r src/utils/handlebars dist",
|
|
14
14
|
"release": "yarn run build && changeset publish",
|
|
15
15
|
"lint": "tsc",
|
|
16
|
-
"test": "tsup src/config.ts src/utils/anpr-revoke.util.ts src/utils/vehicle-request.util.ts src/utils/camera-view.util.ts src/utils/camera-bulk-import.util.ts src/utils/dashboard-metrics.util.ts src/utils/camera-capability.util.ts src/services/camera-device-http.service.ts src/utils/camera-alert.util.ts src/utils/dahua-protocol.util.ts src/utils/site-name.util.ts src/utils/service-provider-invite.util.ts src/utils/notification-category.util.ts src/utils/hid-card.util.ts src/utils/hid-json.util.ts src/utils/hid-access.util.ts src/models/customer-site.model.ts src/models/site-camera.model.ts src/models/member.model.ts src/models/subscription.model.ts src/utils/session-id.util.ts src/utils/occurrence-query.util.ts src/utils/update-result.util.ts src/utils/hid-amico-authz.util.ts src/utils/promo-code-currency.util.ts src/utils/console-audit.util.ts src/utils/org-suspension.util.ts src/utils/session-revoke.util.ts src/models/hid-amico.model.ts src/models/patrol-email.model.ts src/utils/patrol-email-pdf.util.ts src/utils/visitor-status.util.ts src/utils/self-service-email.util.ts src/utils/hid-secret.util.ts src/models/visitor-invite.model.ts src/utils/bulletin-service-apps.util.ts src/models/bulletin-board.model.ts src/utils/vehicle-import.util.ts src/utils/expired-vehicle-sweep.util.ts --format esm --out-dir test/.build --no-dts --silent && node --test test/*.test.mjs",
|
|
16
|
+
"test": "tsup src/config.ts src/utils/anpr-revoke.util.ts src/utils/vehicle-request.util.ts src/utils/camera-view.util.ts src/utils/camera-bulk-import.util.ts src/utils/dashboard-metrics.util.ts src/utils/camera-capability.util.ts src/services/camera-device-http.service.ts src/utils/camera-alert.util.ts src/utils/dahua-protocol.util.ts src/utils/site-name.util.ts src/utils/service-provider-invite.util.ts src/utils/notification-category.util.ts src/utils/hid-card.util.ts src/utils/hid-json.util.ts src/utils/hid-access.util.ts src/models/customer-site.model.ts src/models/site-camera.model.ts src/models/member.model.ts src/models/subscription.model.ts src/utils/session-id.util.ts src/utils/occurrence-query.util.ts src/utils/update-result.util.ts src/utils/hid-amico-authz.util.ts src/utils/promo-code-currency.util.ts src/utils/console-audit.util.ts src/utils/org-suspension.util.ts src/utils/session-revoke.util.ts src/models/hid-amico.model.ts src/models/patrol-email.model.ts src/utils/patrol-email-pdf.util.ts src/utils/visitor-status.util.ts src/utils/self-service-email.util.ts src/utils/hid-secret.util.ts src/models/visitor-invite.model.ts src/utils/bulletin-service-apps.util.ts src/models/bulletin-board.model.ts src/utils/vehicle-import.util.ts src/utils/expired-vehicle-sweep.util.ts src/models/vehicle.model.ts --format esm --out-dir test/.build --no-dts --silent && node --test test/*.test.mjs",
|
|
17
17
|
"test:camera": "yarn test",
|
|
18
18
|
"test:e2e": "yarn build && node --test --test-concurrency=1 --test-timeout=600000 \"test/e2e/*.test.mjs\""
|
|
19
19
|
},
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
|
|
4
|
+
import { sweepExpiredVehicles } from "./.build/utils/expired-vehicle-sweep.util.mjs";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The production defect this covers: `bulkUpsertVehicles` (spreadsheet import)
|
|
8
|
+
* wrote EVERY row to the database whatever the cameras answered. The only thing
|
|
9
|
+
* read from the device reply was a `recNo`, which is simply `undefined` on a
|
|
10
|
+
* 401/500/no-answer — so an imported BLOCKLIST plate that no camera ever
|
|
11
|
+
* accepted was saved as blocked while the car still opened the barrier.
|
|
12
|
+
*
|
|
13
|
+
* The service now runs the rows through `sweepExpiredVehicles` — the same
|
|
14
|
+
* all-cameras-must-confirm decision manual delete, the expired-vehicle cron and
|
|
15
|
+
* the visitor sweep already share — with the device call being ADD instead of
|
|
16
|
+
* REMOVE, and saves only the rows it returns as confirmed.
|
|
17
|
+
*
|
|
18
|
+
* These tests drive that exact composition: light {index, site, plate} rows in,
|
|
19
|
+
* a savable/skipped partition out, re-associated to the real rows by index.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const cameraLabel = (camera) => camera.name;
|
|
23
|
+
|
|
24
|
+
/** How the service builds the sweep rows: the index is the join key. */
|
|
25
|
+
const sweepRowsFor = (values, site) =>
|
|
26
|
+
values.map((value, index) => ({
|
|
27
|
+
_id: String(index),
|
|
28
|
+
site,
|
|
29
|
+
plateNumber: value.plateNumber,
|
|
30
|
+
}));
|
|
31
|
+
|
|
32
|
+
/** How the service picks what to save. */
|
|
33
|
+
const savableFrom = (values, savableIds) => {
|
|
34
|
+
const savable = new Set(savableIds);
|
|
35
|
+
return values.filter((_, index) => savable.has(String(index)));
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
test("a row no camera accepted is NOT saved", async () => {
|
|
39
|
+
const values = [
|
|
40
|
+
{ plateNumber: "GOOD111", type: "whitelist" },
|
|
41
|
+
{ plateNumber: "BAD222", type: "blocklist" },
|
|
42
|
+
];
|
|
43
|
+
const cameras = [{ name: "entry" }];
|
|
44
|
+
|
|
45
|
+
const { deletableIds, skipped } = await sweepExpiredVehicles(
|
|
46
|
+
sweepRowsFor(values, "siteA"),
|
|
47
|
+
{
|
|
48
|
+
camerasForSite: async () => cameras,
|
|
49
|
+
cameraLabel,
|
|
50
|
+
revoke: async (_camera, row) =>
|
|
51
|
+
values[Number(row._id)].plateNumber === "BAD222"
|
|
52
|
+
? { ok: false, reason: "the camera could not be reached" }
|
|
53
|
+
: { ok: true, reason: "" },
|
|
54
|
+
},
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
assert.deepEqual(
|
|
58
|
+
savableFrom(values, deletableIds).map((v) => v.plateNumber),
|
|
59
|
+
["GOOD111"],
|
|
60
|
+
);
|
|
61
|
+
assert.equal(skipped.length, 1);
|
|
62
|
+
assert.equal(skipped[0].plateNumber, "BAD222");
|
|
63
|
+
assert.deepEqual(skipped[0].failures, [
|
|
64
|
+
{ camera: "entry", reason: "the camera could not be reached" },
|
|
65
|
+
]);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("one camera refusing is enough to hold the whole row back", async () => {
|
|
69
|
+
const values = [{ plateNumber: "AAA111", type: "blocklist" }];
|
|
70
|
+
|
|
71
|
+
const { deletableIds, skipped } = await sweepExpiredVehicles(
|
|
72
|
+
sweepRowsFor(values, "siteA"),
|
|
73
|
+
{
|
|
74
|
+
camerasForSite: async () => [{ name: "entry" }, { name: "exit" }],
|
|
75
|
+
cameraLabel,
|
|
76
|
+
revoke: async (camera) =>
|
|
77
|
+
camera.name === "exit"
|
|
78
|
+
? { ok: false, reason: "the camera could not be reached" }
|
|
79
|
+
: { ok: true, reason: "" },
|
|
80
|
+
},
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
assert.deepEqual(deletableIds, []);
|
|
84
|
+
assert.deepEqual(savableFrom(values, deletableIds), []);
|
|
85
|
+
// The camera that DID take it is the one the service then rolls back, so the
|
|
86
|
+
// plate is not left live on a barrier with no record of it anywhere.
|
|
87
|
+
assert.equal(skipped[0].failures.length, 1);
|
|
88
|
+
assert.equal(skipped[0].failures[0].camera, "exit");
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("the failed row does not shift the rows around it", async () => {
|
|
92
|
+
const values = [
|
|
93
|
+
{ plateNumber: "AAA111" },
|
|
94
|
+
{ plateNumber: "BBB222" },
|
|
95
|
+
{ plateNumber: "CCC333" },
|
|
96
|
+
];
|
|
97
|
+
|
|
98
|
+
const { deletableIds, skipped } = await sweepExpiredVehicles(
|
|
99
|
+
sweepRowsFor(values, "siteA"),
|
|
100
|
+
{
|
|
101
|
+
camerasForSite: async () => [{ name: "entry" }],
|
|
102
|
+
cameraLabel,
|
|
103
|
+
revoke: async (_camera, row) =>
|
|
104
|
+
row._id === "1"
|
|
105
|
+
? { ok: false, reason: "the camera could not be reached" }
|
|
106
|
+
: { ok: true, reason: "" },
|
|
107
|
+
},
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
assert.deepEqual(
|
|
111
|
+
savableFrom(values, deletableIds).map((v) => v.plateNumber),
|
|
112
|
+
["AAA111", "CCC333"],
|
|
113
|
+
);
|
|
114
|
+
assert.deepEqual(
|
|
115
|
+
skipped.map((entry) => entry.plateNumber),
|
|
116
|
+
["BBB222"],
|
|
117
|
+
);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test("every camera confirming saves every row", async () => {
|
|
121
|
+
const values = [{ plateNumber: "AAA111" }, { plateNumber: "BBB222" }];
|
|
122
|
+
|
|
123
|
+
const { deletableIds, skipped } = await sweepExpiredVehicles(
|
|
124
|
+
sweepRowsFor(values, "siteA"),
|
|
125
|
+
{
|
|
126
|
+
camerasForSite: async () => [{ name: "entry" }],
|
|
127
|
+
cameraLabel,
|
|
128
|
+
revoke: async () => ({ ok: true, reason: "" }),
|
|
129
|
+
},
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
assert.equal(savableFrom(values, deletableIds).length, 2);
|
|
133
|
+
assert.deepEqual(skipped, []);
|
|
134
|
+
});
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
expiredVehicleSweepFilter,
|
|
6
|
+
MVehicle,
|
|
7
|
+
VehicleStatus,
|
|
8
|
+
} from "./.build/models/vehicle.model.mjs";
|
|
9
|
+
|
|
10
|
+
// The live defect. `end` is written as a BSON Date, but both sweep queries
|
|
11
|
+
// compared it against `new Date().toISOString()` - a String. String sorts
|
|
12
|
+
// before Date in BSON type order, so a Date is never `$lte` a String: the
|
|
13
|
+
// hourly cron selected NOTHING and an expired season pass kept opening the
|
|
14
|
+
// barrier. Measured read-only on staging on 2026-09-02: the old query returned
|
|
15
|
+
// 0 rows while 3 vehicles with a real past `end` were still live.
|
|
16
|
+
const sweptAt = new Date("2026-09-02T10:00:00.000Z");
|
|
17
|
+
const filter = expiredVehicleSweepFilter(sweptAt);
|
|
18
|
+
|
|
19
|
+
// Enough of a matcher for the operators this filter uses. `$type: "date"` is
|
|
20
|
+
// the whole point of the fix, so it is checked the way Mongo checks it: on the
|
|
21
|
+
// stored BSON type, not on whether the value looks like a date.
|
|
22
|
+
const bsonType = (v) => {
|
|
23
|
+
if (v === undefined) return "missing";
|
|
24
|
+
if (v === null) return "null";
|
|
25
|
+
if (v instanceof Date) return "date";
|
|
26
|
+
return typeof v === "string" ? "string" : typeof v;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const matches = (query, doc) =>
|
|
30
|
+
Object.entries(query).every(([key, want]) => {
|
|
31
|
+
const got = doc[key];
|
|
32
|
+
if (want && typeof want === "object" && !(want instanceof Date)) {
|
|
33
|
+
if ("$type" in want && bsonType(got) !== want.$type) return false;
|
|
34
|
+
if ("$lte" in want && !(got !== undefined && got !== null && got <= want.$lte))
|
|
35
|
+
return false;
|
|
36
|
+
if ("$ne" in want && got === want.$ne) return false;
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
return got === want;
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const live = { status: VehicleStatus.ACTIVE };
|
|
43
|
+
|
|
44
|
+
test("a genuinely expired vehicle IS selected by the sweep", () => {
|
|
45
|
+
assert.equal(
|
|
46
|
+
matches(filter, { ...live, end: new Date("2026-06-24T23:59:00.000Z") }),
|
|
47
|
+
true,
|
|
48
|
+
);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("a vehicle whose expiry is in the future is NOT selected", () => {
|
|
52
|
+
assert.equal(
|
|
53
|
+
matches(filter, { ...live, end: new Date("2026-12-31T23:59:00.000Z") }),
|
|
54
|
+
false,
|
|
55
|
+
);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("a vehicle with a blank expiry is NOT selected", () => {
|
|
59
|
+
// These are the ONLY rows the broken query ever matched - nine were deleted
|
|
60
|
+
// at one site in a single run on 2026-09-01. A vehicle with no expiry date
|
|
61
|
+
// is not an expired vehicle.
|
|
62
|
+
assert.equal(matches(filter, { ...live, end: "" }), false);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("a vehicle with a missing expiry is NOT selected", () => {
|
|
66
|
+
assert.equal(matches(filter, { ...live }), false);
|
|
67
|
+
assert.equal(matches(filter, { ...live, end: null }), false);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("a blank expiry is not selected even when it sorts before the sweep time", () => {
|
|
71
|
+
// The regression guard proper: under the old string comparison `"" <= now`
|
|
72
|
+
// was true, which is exactly how the nine rows were swept up.
|
|
73
|
+
assert.equal("" <= sweptAt.toISOString(), true);
|
|
74
|
+
assert.equal(matches(filter, { ...live, end: "" }), false);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("an already-deleted vehicle is not swept again", () => {
|
|
78
|
+
assert.equal(
|
|
79
|
+
matches(filter, {
|
|
80
|
+
status: VehicleStatus.DELETED,
|
|
81
|
+
end: new Date("2026-06-24T23:59:00.000Z"),
|
|
82
|
+
}),
|
|
83
|
+
false,
|
|
84
|
+
);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// The other half of the same defect: where the `end: ""` rows came from.
|
|
88
|
+
const base = {
|
|
89
|
+
plateNumber: "SGZ680",
|
|
90
|
+
type: "whitelist",
|
|
91
|
+
category: "resident",
|
|
92
|
+
site: "6923d6664150ca6a69b9f2b2",
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
test("a blank expiry is stored as a Date, never as an empty string", () => {
|
|
96
|
+
// `value.end ?? expiredDate` kept `""`, because `??` only falls through on
|
|
97
|
+
// null/undefined. That put a String into a Date field, which is what the
|
|
98
|
+
// broken query then matched.
|
|
99
|
+
const v = MVehicle({ ...base, end: "" });
|
|
100
|
+
assert.ok(v.end instanceof Date);
|
|
101
|
+
assert.equal(v.end.getUTCFullYear(), new Date().getUTCFullYear() + 10);
|
|
102
|
+
assert.equal(matches(filter, { ...v, status: VehicleStatus.ACTIVE }), false);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("a null or absent expiry still takes the ten-year sentinel", () => {
|
|
106
|
+
assert.ok(MVehicle({ ...base, end: null }).end instanceof Date);
|
|
107
|
+
assert.ok(MVehicle({ ...base }).end instanceof Date);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("a real expiry is still honoured verbatim", () => {
|
|
111
|
+
const v = MVehicle({ ...base, end: "2026-06-24T23:59:00.000Z" });
|
|
112
|
+
assert.equal(v.end.toISOString(), "2026-06-24T23:59:00.000Z");
|
|
113
|
+
assert.equal(matches(filter, { ...v, status: VehicleStatus.ACTIVE }), true);
|
|
114
|
+
});
|