tabletcommand-backend-models 7.4.98 → 7.4.100
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/build/index.js +8 -5
- package/build/index.js.map +1 -1
- package/build/models/cad-interface-meta-data.js +17 -0
- package/build/models/cad-interface-meta-data.js.map +1 -0
- package/build/models/department.js +11 -0
- package/build/models/department.js.map +1 -1
- package/build/models/global-settings.js +15 -0
- package/build/models/global-settings.js.map +1 -1
- package/build/test/cad-interface-meta-data.js +51 -0
- package/build/test/cad-interface-meta-data.js.map +1 -0
- package/build/test/global-settings.js +5 -1
- package/build/test/global-settings.js.map +1 -1
- package/build/test/mock.js +4 -0
- package/build/test/mock.js.map +1 -1
- package/build/types/cad-interface-meta-data.js +3 -0
- package/build/types/cad-interface-meta-data.js.map +1 -0
- package/build/types/department.js +2 -0
- package/build/types/department.js.map +1 -1
- package/cad-interface-metadata-plan.md +327 -0
- package/definitions/index.d.ts +9 -1
- package/definitions/index.d.ts.map +1 -1
- package/definitions/models/cad-interface-meta-data.d.ts +13 -0
- package/definitions/models/cad-interface-meta-data.d.ts.map +1 -0
- package/definitions/models/department.d.ts.map +1 -1
- package/definitions/models/global-settings.d.ts.map +1 -1
- package/definitions/test/cad-interface-meta-data.d.ts +2 -0
- package/definitions/test/cad-interface-meta-data.d.ts.map +1 -0
- package/definitions/test/mock.d.ts.map +1 -1
- package/definitions/types/cad-interface-meta-data.d.ts +8 -0
- package/definitions/types/cad-interface-meta-data.d.ts.map +1 -0
- package/definitions/types/department.d.ts +4 -0
- package/definitions/types/department.d.ts.map +1 -1
- package/definitions/types/global-settings.d.ts +5 -0
- package/definitions/types/global-settings.d.ts.map +1 -1
- package/package.json +1 -1
- package/sc-33945-plan.md +189 -0
- package/src/index.ts +7 -0
- package/src/models/cad-interface-meta-data.ts +27 -0
- package/src/models/department.ts +12 -1
- package/src/models/global-settings.ts +17 -0
- package/src/test/cad-interface-meta-data.ts +53 -0
- package/src/test/global-settings.ts +5 -0
- package/src/test/mock.ts +4 -0
- package/src/types/cad-interface-meta-data.ts +9 -0
- package/src/types/department.ts +5 -0
- package/src/types/global-settings.ts +6 -0
package/sc-33945-plan.md
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
# Plan — sc-33945: Update Endpoint to include "Can Approve Sharing Agreements" permission
|
|
2
|
+
|
|
3
|
+
**Story:** [sc-33945](https://app.shortcut.com/tabletcommand/story/33945) (chore)
|
|
4
|
+
**Parent:** [sc-33735](https://app.shortcut.com/tabletcommand/story/33735) — FE checkbox on Superuser User Details page
|
|
5
|
+
**Epic:** [Data Sharing Agreement (33722)](https://app.shortcut.com/tabletcommand/epic/33722)
|
|
6
|
+
**Iteration:** Sprint 2026/14 (2026-07-05 → 2026-07-19)
|
|
7
|
+
**Owner:** Kevin Yoshiyama
|
|
8
|
+
|
|
9
|
+
## Goal
|
|
10
|
+
|
|
11
|
+
Extend the admin user-update API so a **superadmin** can grant/revoke a new boolean
|
|
12
|
+
permission `canApproveSharingAgreements` on a user, subject to guardrails, with the
|
|
13
|
+
change written to the searchable action log. This is the backend half; the parent
|
|
14
|
+
story (33735) is the frontend checkbox.
|
|
15
|
+
|
|
16
|
+
## Acceptance criteria (from the story)
|
|
17
|
+
|
|
18
|
+
- [ ] API rejects `canApproveSharingAgreements: true` for non-admin target users with `400`.
|
|
19
|
+
- [ ] Non-superadmin API callers receive `403` when attempting to set this field.
|
|
20
|
+
- [ ] The user-detail `GET` response includes `canApproveSharingAgreements` for superadmin callers.
|
|
21
|
+
- [ ] Grant/revoke is written to the audit log with the acting superadmin's ID + timestamp.
|
|
22
|
+
- [ ] The action is searchable in the superadmin action log.
|
|
23
|
+
- [ ] BE unit tests complete.
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Current state (what I found in the code)
|
|
28
|
+
|
|
29
|
+
| Concern | Location |
|
|
30
|
+
|---|---|
|
|
31
|
+
| `GET /api/a1/admin/user/:userId` | [server-admin/src/routes/admin.ts:342](server-admin/src/routes/admin.ts) |
|
|
32
|
+
| `POST /api/a1/admin/user/:userId/:departmentId?` (the "update" endpoint) | [server-admin/src/routes/admin.ts:374](server-admin/src/routes/admin.ts) |
|
|
33
|
+
| Request-body decode / field allow-list | `decodeUpdateUser()` [server-admin/src/lib/domain.ts:4201](server-admin/src/lib/domain.ts) |
|
|
34
|
+
| Auth resolution (`isSuper`, `isAdmin`, …) | `decodeAuth()` [server-admin/src/lib/domain.ts:389](server-admin/src/lib/domain.ts) |
|
|
35
|
+
| Persist + restricted-field revert + action log | `updateUserForDepartment()` [server-admin/src/lib/store/index.ts:2010](server-admin/src/lib/store/index.ts) |
|
|
36
|
+
| GET data fetch | `getUserForDepartment()` [server-admin/src/lib/store/index.ts:~1961](server-admin/src/lib/store/index.ts) |
|
|
37
|
+
| Action-log write helper | `logAction()` / `models.ActionLog` [server-admin/src/lib/store/index.ts:418](server-admin/src/lib/store/index.ts) |
|
|
38
|
+
| Action-log message formatter | [server-admin/src/lib/actionLogMessages.ts:287](server-admin/src/lib/actionLogMessages.ts) |
|
|
39
|
+
| BE tests | [server-admin/src/test/routes/admin.ts](server-admin/src/test/routes/admin.ts) (`node:test` + `chai` + `node-mocks-http`) |
|
|
40
|
+
| **User model / schema (EXTERNAL)** | `tabletcommand-backend-models` `~7.4.94` — `src/models/user.ts`, `src/types/user.ts`, collection `sys_user` |
|
|
41
|
+
|
|
42
|
+
### Key facts that shape the plan
|
|
43
|
+
|
|
44
|
+
1. **The User model is an external npm package**, not in this repo. The Mongoose schema is
|
|
45
|
+
**strict by default** (no `strict: false`), so a field absent from the schema is silently
|
|
46
|
+
dropped on save. `canApproveSharingAgreements` **must be added to
|
|
47
|
+
`tabletcommand-backend-models` first**, published, then the dependency bumped here. This is
|
|
48
|
+
a hard prerequisite — without it, saves will no-op.
|
|
49
|
+
|
|
50
|
+
2. **The existing pattern for privileged flags is a silent revert, not a 403.** For `admin` and
|
|
51
|
+
`canBlockIncidents`, a non-superadmin's edits are reverted to the previous value
|
|
52
|
+
([store/index.ts:2039-2044](server-admin/src/lib/store/index.ts)) — no error is returned.
|
|
53
|
+
The story asks for an explicit **`403`**. See Decision 1.
|
|
54
|
+
|
|
55
|
+
3. **Auth failures in these routes currently return `401`, not `403`** (see admin.ts:353/383).
|
|
56
|
+
The story says `403`. See Decision 1.
|
|
57
|
+
|
|
58
|
+
4. **The endpoint is `POST` (not `PUT`) at `/user/:userId/:departmentId?`** — the story's
|
|
59
|
+
`api/a1/admin/user/` shorthand omits this. No route change needed.
|
|
60
|
+
|
|
61
|
+
5. **Action logging is already generic.** `updateUserForDepartment` builds a `propertiesChanged`
|
|
62
|
+
array and writes one `ActionLog` (`action: "adminModifyUser"`) with `email: user.email`
|
|
63
|
+
(the actor) and `createdAt` timestamp. Adding a `canApproveSharingAgreements` diff to that
|
|
64
|
+
array satisfies the "audit log + searchable" criteria for free. Note the actor is recorded by
|
|
65
|
+
**email**, not `_id` — see Decision 2.
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## Decisions (confirmed)
|
|
70
|
+
|
|
71
|
+
**Decision 1 — Return `403` for unauthorized writes. ✅ CONFIRMED.**
|
|
72
|
+
Return `403` when a non-superadmin includes `canApproveSharingAgreements` in the body. Implemented
|
|
73
|
+
as an early guard in the route so it doesn't disturb the existing revert behavior for other flags.
|
|
74
|
+
(This intentionally diverges from the surrounding `401` convention for auth failures — accepted.)
|
|
75
|
+
|
|
76
|
+
**Decision 2 — Log both email and `_id` in the audit log. ✅ CONFIRMED.**
|
|
77
|
+
Existing `adminModifyUser` logs store the actor as `email`. Record **both** the acting user's
|
|
78
|
+
`email` and `_id` on the action item.
|
|
79
|
+
|
|
80
|
+
**Decision 3 — "target user is admin" check basis.**
|
|
81
|
+
`canApproveSharingAgreements: true` is only allowed if the target is an admin. Because admin status
|
|
82
|
+
can change in the same request, validate against the **effective** admin value:
|
|
83
|
+
`userData.admin` if present in the body, else `previousUser.admin`. Also mirror the parent story's
|
|
84
|
+
rule: **if admin is being set false, force `canApproveSharingAgreements = false`.**
|
|
85
|
+
|
|
86
|
+
**Decision 4 — Include a descriptive `message` in error response bodies. ✅ CONFIRMED.**
|
|
87
|
+
The `400` (non-admin target) and `403` (non-superadmin caller) responses should return a `message`
|
|
88
|
+
field explaining the rejection, e.g. `{ ok: false, message: "..." }`, rather than bare
|
|
89
|
+
`{ ok: false }`.
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## Implementation steps
|
|
94
|
+
|
|
95
|
+
### Step 0 — External model change (prerequisite, separate repo)
|
|
96
|
+
In `tabletcommand-backend-models`:
|
|
97
|
+
- Add to `src/models/user.ts` (next to `canBlockIncidents`, ~line 123):
|
|
98
|
+
```ts
|
|
99
|
+
canApproveSharingAgreements: {
|
|
100
|
+
type: Boolean,
|
|
101
|
+
default: false,
|
|
102
|
+
},
|
|
103
|
+
```
|
|
104
|
+
- Add `canApproveSharingAgreements: boolean` to the `User` type in `src/types/user.ts` (~line 103).
|
|
105
|
+
- Add it to the test mock (`src/test/mock.ts`) if permission flags are enumerated there.
|
|
106
|
+
- Publish a new version; bump `server-admin/package.json` (`~7.4.94` → new) and `npm install`.
|
|
107
|
+
|
|
108
|
+
> If publishing a new models version is out of scope for this sprint, coordinate with whoever
|
|
109
|
+
> owns that package. Nothing below persists without it.
|
|
110
|
+
|
|
111
|
+
### Step 1 — Route guard for superadmin-only write (`403`)
|
|
112
|
+
In `POST /user/:userId/:departmentId?` ([routes/admin.ts:374](server-admin/src/routes/admin.ts)),
|
|
113
|
+
after `decodeAuth`: if `req.body` contains the key `canApproveSharingAgreements` **and** `!isSuper`,
|
|
114
|
+
return `403` with `{ ok: false, message: "..." }` (Decision 4). Do this before
|
|
115
|
+
`decodeUpdateUser`/store call.
|
|
116
|
+
|
|
117
|
+
### Step 2 — Preserve the field through decode
|
|
118
|
+
`decodeUpdateUser()` ([domain.ts:4201](server-admin/src/lib/domain.ts)) uses an `ignoredFields`
|
|
119
|
+
allow-list; `canApproveSharingAgreements` is **not** in it, so it passes through — no change needed.
|
|
120
|
+
Add a brief comment noting it's intentionally allowed (superadmin-gated upstream). Do **not** add it
|
|
121
|
+
to `ignoredFields`.
|
|
122
|
+
|
|
123
|
+
### Step 3 — Validation + revert logic in the store
|
|
124
|
+
In `updateUserForDepartment()` ([store/index.ts:2010](server-admin/src/lib/store/index.ts)), after
|
|
125
|
+
`previousUser` is loaded (~line 2038):
|
|
126
|
+
- **400 guard:** if `userData.canApproveSharingAgreements === true`, compute effective admin
|
|
127
|
+
(`userData.admin ?? previousUser.admin`); if not truthy, reject with `400` and a descriptive
|
|
128
|
+
`message` in the body (Decision 4), e.g.
|
|
129
|
+
`{ ok: false, message: "canApproveSharingAgreements requires the target user to be an admin" }`.
|
|
130
|
+
Implement via a typed error the route maps to `400`, or short-circuit in the route before the
|
|
131
|
+
store call — either way the message must reach the body (the existing `catch` returns a bare
|
|
132
|
+
`{ ok: false }`, so adjust it or return earlier).
|
|
133
|
+
- **Cascade:** if effective admin is false, force `userData.canApproveSharingAgreements = false`
|
|
134
|
+
(mirrors parent-story rule and prevents orphaned permission).
|
|
135
|
+
- **Non-superadmin safety net:** extend the existing block at
|
|
136
|
+
[store/index.ts:2039-2044](server-admin/src/lib/store/index.ts) to also revert
|
|
137
|
+
`canApproveSharingAgreements` to `previousUser.canApproveSharingAgreements` when
|
|
138
|
+
`user.superuser !== true` (defense in depth behind the Step 1 route guard).
|
|
139
|
+
|
|
140
|
+
### Step 4 — Audit log entry
|
|
141
|
+
In the `propertiesChanged` block (~[store/index.ts:2099](server-admin/src/lib/store/index.ts)),
|
|
142
|
+
mirroring the `admin` diff:
|
|
143
|
+
```ts
|
|
144
|
+
if (foundUser.canApproveSharingAgreements !== userData.canApproveSharingAgreements) {
|
|
145
|
+
propertiesChanged.push({ canApproveSharingAgreements: userData.canApproveSharingAgreements });
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
This flows into the existing `adminModifyUser` `ActionLog` (actor email + `createdAt`), making it
|
|
149
|
+
searchable. Record both acting `user.email` (already present) and `user._id` per Decision 2.
|
|
150
|
+
Confirm `formatAdminModifyUserMessage`
|
|
151
|
+
([actionLogMessages.ts:287](server-admin/src/lib/actionLogMessages.ts)) renders the new key
|
|
152
|
+
acceptably (its generic loop already handles arbitrary keys).
|
|
153
|
+
|
|
154
|
+
### Step 5 — GET response
|
|
155
|
+
`getUserForDepartment()` returns the full Mongoose doc, so once the schema field exists (Step 0)
|
|
156
|
+
the value is included automatically. **Verify** no projection/`select` or serializer strips it, and
|
|
157
|
+
that it's only meaningfully surfaced for superadmin callers (the field is superadmin-managed; the FE
|
|
158
|
+
in 33735 only renders it for superadmins). No code change expected — confirm with a test.
|
|
159
|
+
|
|
160
|
+
### Step 6 — Backend unit tests
|
|
161
|
+
Add to [server-admin/src/test/routes/admin.ts](server-admin/src/test/routes/admin.ts), following
|
|
162
|
+
the existing `superUserTest` / `requestUser2` fixtures and `httpMocks` patterns:
|
|
163
|
+
1. Superadmin sets `canApproveSharingAgreements: true` on an admin target → `200`; DB value `true`;
|
|
164
|
+
`ActionLog` row written with the diff.
|
|
165
|
+
2. Set `true` on a **non-admin** target → `400`.
|
|
166
|
+
3. **Non-superadmin** (regular admin) sends the field → `403`.
|
|
167
|
+
4. Setting `admin: false` cascades `canApproveSharingAgreements` → `false`.
|
|
168
|
+
5. `GET /user/:userId` as superadmin includes the field.
|
|
169
|
+
6. Revoke (`true` → `false`) writes an audit-log diff.
|
|
170
|
+
|
|
171
|
+
Run: `gulp test` (or the `tsx --test` next-suite) in `server-admin`.
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
## Out of scope
|
|
176
|
+
- Frontend checkbox / Data Sharing tab visibility — parent story [sc-33735](https://app.shortcut.com/tabletcommand/story/33735).
|
|
177
|
+
- Bulk-edit endpoint (`/user-bulk-edit`) — **out of scope for this story** (confirmed).
|
|
178
|
+
|
|
179
|
+
## Risks / open questions
|
|
180
|
+
- **Models package release** (Step 0) is the critical path and lives in another repo.
|
|
181
|
+
- Whether the 400 error message should appear in the response body (current pattern returns
|
|
182
|
+
`{ ok: false }` only).
|
|
183
|
+
|
|
184
|
+
## Suggested commits / PR
|
|
185
|
+
1. (models repo) add `canApproveSharingAgreements` to schema + type + mock; publish.
|
|
186
|
+
2. `server-admin`: bump models dep; route `403` guard; store 400/cascade/revert; audit-log diff.
|
|
187
|
+
3. `server-admin`: unit tests.
|
|
188
|
+
|
|
189
|
+
Branch suggestion: `feature/sc-33945/can-approve-sharing-agreements`.
|
package/src/index.ts
CHANGED
|
@@ -35,6 +35,7 @@ async function wireModels(mongoose: MongooseModule) {
|
|
|
35
35
|
CADIncident: await getModel(import("./models/cad-incident")),
|
|
36
36
|
CADIncidentBlock: await getModel(import("./models/cad-incident-block")),
|
|
37
37
|
CADIncidentStream: await getModel(import("./models/cad-incident-stream")),
|
|
38
|
+
CADInterfaceMetaData: await getModel(import("./models/cad-interface-meta-data")),
|
|
38
39
|
CADSimulation: await getModel(import("./models/cad-simulation")),
|
|
39
40
|
CADStatus: await getModel(import("./models/cad-status")),
|
|
40
41
|
CADStatusMap: await getModel(import("./models/cad-status-map")),
|
|
@@ -100,6 +101,7 @@ export { BeaconLog, BeaconLogModel } from "./models/beacon-log";
|
|
|
100
101
|
export { CADIncident, CADIncidentModel } from "./models/cad-incident";
|
|
101
102
|
export { CADIncidentBlock, CADIncidentBlockModel } from "./models/cad-incident-block";
|
|
102
103
|
export { CADIncidentStream, CADIncidentStreamModel } from "./models/cad-incident-stream";
|
|
104
|
+
export { CADInterfaceMetaData, CADInterfaceMetaDataModel } from "./models/cad-interface-meta-data";
|
|
103
105
|
export { CADSimulation, CADSimulationModel } from "./models/cad-simulation";
|
|
104
106
|
export { CADStatus, CADStatusModel } from "./models/cad-status";
|
|
105
107
|
export { CADStatusMap, CADStatusMapModel } from "./models/cad-status-map";
|
|
@@ -154,6 +156,7 @@ export { UserRegistration, UserRegistrationModel } from "./models/user-registrat
|
|
|
154
156
|
export { ValidationReport, ValidationReportModel } from "./models/validation-report";
|
|
155
157
|
|
|
156
158
|
export * from "./helpers";
|
|
159
|
+
export { IntegrationTypes } from "./types/department";
|
|
157
160
|
|
|
158
161
|
export * from "./checklist-item-event-message";
|
|
159
162
|
|
|
@@ -162,7 +165,11 @@ export type {
|
|
|
162
165
|
ForwardingConnectionType,
|
|
163
166
|
ForwardingConfigType,
|
|
164
167
|
ForwardFieldsType,
|
|
168
|
+
IntegrationType,
|
|
165
169
|
} from "./types/department";
|
|
170
|
+
export type {
|
|
171
|
+
CADInterfaceMetaDataType,
|
|
172
|
+
} from "./types/cad-interface-meta-data";
|
|
166
173
|
|
|
167
174
|
// Export default vars
|
|
168
175
|
export {
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Model, Schema } from "mongoose";
|
|
2
|
+
import { MongooseModule } from "../helpers";
|
|
3
|
+
import { CADInterfaceMetaDataType } from "../types/cad-interface-meta-data";
|
|
4
|
+
|
|
5
|
+
export interface CADInterfaceMetaData extends CADInterfaceMetaDataType {}
|
|
6
|
+
|
|
7
|
+
export default async function CADInterfaceMetaDataModule(mongoose: MongooseModule) {
|
|
8
|
+
const modelSchema = new Schema<CADInterfaceMetaDataType>({
|
|
9
|
+
systemType: { type: String, required: true, trim: true },
|
|
10
|
+
}, {
|
|
11
|
+
autoIndex: false,
|
|
12
|
+
timestamps: true, // manages createdAt / updatedAt automatically (per story)
|
|
13
|
+
toJSON: { virtuals: true, versionKey: false },
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
// Uniqueness so the same CAD system can't be catalogued twice.
|
|
17
|
+
modelSchema.index({ systemType: 1 }, { unique: true });
|
|
18
|
+
|
|
19
|
+
return mongoose.model<CADInterfaceMetaData>(
|
|
20
|
+
"CADInterfaceMetaData",
|
|
21
|
+
modelSchema,
|
|
22
|
+
"massive_cad_interface_meta_data",
|
|
23
|
+
{ overwriteModels: true }
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface CADInterfaceMetaDataModel extends Model<CADInterfaceMetaData> {}
|
package/src/models/department.ts
CHANGED
|
@@ -50,6 +50,7 @@ import {
|
|
|
50
50
|
IntterraConnectionType,
|
|
51
51
|
IntterraFieldsType,
|
|
52
52
|
LicensingType,
|
|
53
|
+
IntegrationTypes,
|
|
53
54
|
ManagedIncidentMode,
|
|
54
55
|
ManagedIncidentModeItem,
|
|
55
56
|
ManagedIncidentPriority,
|
|
@@ -1741,7 +1742,7 @@ export default async function DepartmentModule(mongoose: MongooseModule) {
|
|
|
1741
1742
|
});
|
|
1742
1743
|
|
|
1743
1744
|
// Main schema
|
|
1744
|
-
const modelSchema = new Schema<
|
|
1745
|
+
const modelSchema = new Schema<Department>({
|
|
1745
1746
|
_id: {
|
|
1746
1747
|
type: Schema.Types.ObjectId,
|
|
1747
1748
|
auto: true,
|
|
@@ -2080,6 +2081,16 @@ export default async function DepartmentModule(mongoose: MongooseModule) {
|
|
|
2080
2081
|
ref: "Agency",
|
|
2081
2082
|
default: [],
|
|
2082
2083
|
}],
|
|
2084
|
+
cadInterfaceMetaDataId: {
|
|
2085
|
+
type: mongoose.Schema.Types.ObjectId,
|
|
2086
|
+
ref: "CADInterfaceMetaData",
|
|
2087
|
+
default: null,
|
|
2088
|
+
},
|
|
2089
|
+
integrationType: {
|
|
2090
|
+
type: String,
|
|
2091
|
+
enum: [...IntegrationTypes, ""],
|
|
2092
|
+
default: "",
|
|
2093
|
+
},
|
|
2083
2094
|
|
|
2084
2095
|
// Signup
|
|
2085
2096
|
signupKey: {
|
|
@@ -5,6 +5,7 @@ import { Model } from "mongoose";
|
|
|
5
5
|
import {
|
|
6
6
|
ArcGISKeyType,
|
|
7
7
|
GlobalSettingsType,
|
|
8
|
+
ZonehavenTokenType,
|
|
8
9
|
} from "../types/global-settings";
|
|
9
10
|
import { EncryptedDataType } from "../types/common";
|
|
10
11
|
|
|
@@ -40,6 +41,19 @@ export default async function GlobalSettingsModule(mongoose: MongooseModule) {
|
|
|
40
41
|
id: false,
|
|
41
42
|
});
|
|
42
43
|
|
|
44
|
+
const ZonehavenToken = new Schema<ZonehavenTokenType>({
|
|
45
|
+
token: {
|
|
46
|
+
type: String,
|
|
47
|
+
default: null,
|
|
48
|
+
},
|
|
49
|
+
expireAt: {
|
|
50
|
+
type: Date,
|
|
51
|
+
},
|
|
52
|
+
}, {
|
|
53
|
+
_id: false,
|
|
54
|
+
id: false,
|
|
55
|
+
});
|
|
56
|
+
|
|
43
57
|
const modelSchema = new Schema<GlobalSettingsType>({
|
|
44
58
|
_id: {
|
|
45
59
|
type: Schema.Types.ObjectId,
|
|
@@ -53,6 +67,9 @@ export default async function GlobalSettingsModule(mongoose: MongooseModule) {
|
|
|
53
67
|
arcgisApiKey: {
|
|
54
68
|
type: ArcGISKey,
|
|
55
69
|
},
|
|
70
|
+
zonehavenToken: {
|
|
71
|
+
type: ZonehavenToken,
|
|
72
|
+
},
|
|
56
73
|
updatedAt: {
|
|
57
74
|
type: Date,
|
|
58
75
|
},
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { assert } from "chai";
|
|
2
|
+
import { describe, it, beforeEach, afterEach } from "node:test";
|
|
3
|
+
import * as m from "../index";
|
|
4
|
+
import * as config from "./config";
|
|
5
|
+
import mockModule from "./mock";
|
|
6
|
+
|
|
7
|
+
describe("CADInterfaceMetaData", function() {
|
|
8
|
+
let models: m.BackendModels, mongoose: m.MongooseModule;
|
|
9
|
+
let testItem: Partial<m.CADInterfaceMetaData>;
|
|
10
|
+
beforeEach(async function() {
|
|
11
|
+
const c = await m.connect(config.url);
|
|
12
|
+
models = c.models;
|
|
13
|
+
mongoose = c.mongoose;
|
|
14
|
+
|
|
15
|
+
const mock = mockModule({
|
|
16
|
+
mongoose
|
|
17
|
+
});
|
|
18
|
+
testItem = {
|
|
19
|
+
systemType: "Tritech Inform"
|
|
20
|
+
};
|
|
21
|
+
await mock.beforeEach();
|
|
22
|
+
});
|
|
23
|
+
afterEach(async function() {
|
|
24
|
+
await mongoose.disconnect();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("is saved", async function() {
|
|
28
|
+
const item = new models.CADInterfaceMetaData(testItem);
|
|
29
|
+
const sut = await item.save();
|
|
30
|
+
|
|
31
|
+
assert.isNotNull(sut._id);
|
|
32
|
+
assert.equal(sut.systemType, testItem.systemType);
|
|
33
|
+
assert.isNotNull(sut.createdAt);
|
|
34
|
+
assert.isNotNull(sut.updatedAt);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("enforces unique systemType index", async function() {
|
|
38
|
+
const item1 = new models.CADInterfaceMetaData(testItem);
|
|
39
|
+
await item1.save();
|
|
40
|
+
|
|
41
|
+
const item2 = new models.CADInterfaceMetaData({
|
|
42
|
+
systemType: testItem.systemType
|
|
43
|
+
});
|
|
44
|
+
try {
|
|
45
|
+
await item2.save();
|
|
46
|
+
assert.fail("Should have failed unique index constraint");
|
|
47
|
+
} catch (err: any) {
|
|
48
|
+
assert.isNotNull(err);
|
|
49
|
+
// Mongo unique index error code is 11000
|
|
50
|
+
assert.equal(err.code, 11000);
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
});
|
|
@@ -43,5 +43,10 @@ describe("GlobalSettings", function() {
|
|
|
43
43
|
assert.isObject(sut.arcgisApiKey?.item);
|
|
44
44
|
assert.equal(sut.arcgisApiKey?.item?.iv, testItem.arcgisApiKey?.item?.iv);
|
|
45
45
|
assert.equal(sut.arcgisApiKey?.item?.encryptedData, testItem.arcgisApiKey?.item?.encryptedData);
|
|
46
|
+
|
|
47
|
+
assert.isObject(sut.zonehavenToken);
|
|
48
|
+
assert.isTrue(isDate(sut.zonehavenToken.expireAt));
|
|
49
|
+
assert.equal(sut.zonehavenToken?.expireAt?.toISOString(), "2025-12-12T01:02:03.000Z");
|
|
50
|
+
assert.equal(sut.zonehavenToken?.token, testItem.zonehavenToken?.token);
|
|
46
51
|
});
|
|
47
52
|
});
|
package/src/test/mock.ts
CHANGED
|
@@ -991,6 +991,10 @@ export default function mockModule(dependencies: { mongoose: Mongoose; }) {
|
|
|
991
991
|
},
|
|
992
992
|
expireAt: new Date("2025-12-12T01:02:03.000Z"),
|
|
993
993
|
},
|
|
994
|
+
zonehavenToken: {
|
|
995
|
+
token: "test-zonehaven-token",
|
|
996
|
+
expireAt: new Date("2025-12-12T01:02:03.000Z"),
|
|
997
|
+
},
|
|
994
998
|
};
|
|
995
999
|
|
|
996
1000
|
const audioStreamAuthentication = {
|
package/src/types/department.ts
CHANGED
|
@@ -9,6 +9,9 @@ import {
|
|
|
9
9
|
import { ReportNumberSchemaType } from "./common-incident";
|
|
10
10
|
import { GeoPolygon } from "./geojson";
|
|
11
11
|
|
|
12
|
+
export const IntegrationTypes = ["TC", "Vendor", "Hybrid"] as const;
|
|
13
|
+
export type IntegrationType = typeof IntegrationTypes[number];
|
|
14
|
+
|
|
12
15
|
export interface WSFeedConnectionOptionsType {
|
|
13
16
|
enabled: boolean;
|
|
14
17
|
url: string;
|
|
@@ -588,6 +591,8 @@ export interface DepartmentType {
|
|
|
588
591
|
addUserInstructions: string,
|
|
589
592
|
adsb: ADSBConfigType,
|
|
590
593
|
agencyIds: Types.ObjectId[],
|
|
594
|
+
cadInterfaceMetaDataId?: Types.ObjectId | null,
|
|
595
|
+
integrationType?: IntegrationType | "",
|
|
591
596
|
allowShareWith3rdParties: boolean,
|
|
592
597
|
apikey: string,
|
|
593
598
|
arcGISMapsEnabled: boolean,
|
|
@@ -6,9 +6,15 @@ export type ArcGISKeyType = {
|
|
|
6
6
|
expireAt: Date | null,
|
|
7
7
|
};
|
|
8
8
|
|
|
9
|
+
export type ZonehavenTokenType = {
|
|
10
|
+
token: string | null,
|
|
11
|
+
expireAt: Date | null,
|
|
12
|
+
};
|
|
13
|
+
|
|
9
14
|
export interface GlobalSettingsType {
|
|
10
15
|
_id: Types.ObjectId,
|
|
11
16
|
name: "global",
|
|
12
17
|
arcgisApiKey: ArcGISKeyType,
|
|
18
|
+
zonehavenToken: ZonehavenTokenType,
|
|
13
19
|
updatedAt: Date,
|
|
14
20
|
}
|