@serve.zone/dcrouter 17.2.4 → 17.4.0
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/deno.json +1 -1
- package/dist_serve/bundle.js +2024 -1758
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.dcrouter.d.ts +11 -1
- package/dist_ts/classes.dcrouter.js +63 -3
- package/dist_ts/config/classes.api-token-manager.d.ts +20 -2
- package/dist_ts/config/classes.api-token-manager.js +201 -6
- package/dist_ts/config/classes.gateway-client-manager.d.ts +36 -2
- package/dist_ts/config/classes.gateway-client-manager.js +391 -27
- package/dist_ts/db/documents/classes.api-token.doc.d.ts +2 -1
- package/dist_ts/db/documents/classes.api-token.doc.js +8 -2
- package/dist_ts/db/documents/classes.cached.email.d.ts +10 -1
- package/dist_ts/db/documents/classes.cached.email.js +27 -2
- package/dist_ts/db/documents/classes.gateway-client.doc.d.ts +25 -0
- package/dist_ts/db/documents/classes.gateway-client.doc.js +73 -2
- package/dist_ts/db/documents/classes.ops-config-event.doc.d.ts +55 -0
- package/dist_ts/db/documents/classes.ops-config-event.doc.js +296 -0
- package/dist_ts/db/documents/index.d.ts +1 -0
- package/dist_ts/db/documents/index.js +2 -1
- package/dist_ts/dns/classes.gateway-route-dns-reconciler.d.ts +52 -0
- package/dist_ts/dns/classes.gateway-route-dns-reconciler.js +609 -0
- package/dist_ts/dns/index.d.ts +1 -0
- package/dist_ts/dns/index.js +2 -1
- package/dist_ts/dns/manager.dns.d.ts +19 -0
- package/dist_ts/dns/manager.dns.js +25 -1
- package/dist_ts/email/classes.accepted-email-spool.d.ts +8 -1
- package/dist_ts/email/classes.accepted-email-spool.js +93 -1
- package/dist_ts/email/classes.smartmta-storage-manager.d.ts +7 -1
- package/dist_ts/email/classes.smartmta-storage-manager.js +46 -1
- package/dist_ts/email/smartmta-storage-policy.js +3 -1
- package/dist_ts/monitoring/classes.ops-event-manager.d.ts +32 -0
- package/dist_ts/monitoring/classes.ops-event-manager.js +71 -0
- package/dist_ts/monitoring/index.d.ts +1 -0
- package/dist_ts/monitoring/index.js +2 -1
- package/dist_ts/opsserver/classes.opsserver.d.ts +1 -0
- package/dist_ts/opsserver/classes.opsserver.js +3 -1
- package/dist_ts/opsserver/handlers/certificate.handler.d.ts +1 -0
- package/dist_ts/opsserver/handlers/certificate.handler.js +35 -5
- package/dist_ts/opsserver/handlers/config-event.handler.d.ts +12 -0
- package/dist_ts/opsserver/handlers/config-event.handler.js +47 -0
- package/dist_ts/opsserver/handlers/dns-record.handler.d.ts +1 -0
- package/dist_ts/opsserver/handlers/dns-record.handler.js +82 -3
- package/dist_ts/opsserver/handlers/email-ops.handler.d.ts +6 -0
- package/dist_ts/opsserver/handlers/email-ops.handler.js +40 -9
- package/dist_ts/opsserver/handlers/gatewayclient.handler.d.ts +9 -1
- package/dist_ts/opsserver/handlers/gatewayclient.handler.js +714 -162
- package/dist_ts/opsserver/handlers/index.d.ts +1 -0
- package/dist_ts/opsserver/handlers/index.js +2 -1
- package/dist_ts/opsserver/helpers/gateway-client-auth.d.ts +25 -0
- package/dist_ts/opsserver/helpers/gateway-client-auth.js +168 -0
- package/dist_ts/plugins.d.ts +2 -1
- package/dist_ts/plugins.js +3 -2
- package/dist_ts_apiclient/classes.gatewayclient.d.ts +13 -1
- package/dist_ts_apiclient/classes.gatewayclient.js +18 -2
- package/dist_ts_interfaces/data/config-events.d.ts +34 -0
- package/dist_ts_interfaces/data/config-events.js +8 -0
- package/dist_ts_interfaces/data/index.d.ts +1 -0
- package/dist_ts_interfaces/data/index.js +2 -1
- package/dist_ts_interfaces/data/route-management.d.ts +28 -1
- package/dist_ts_interfaces/data/route-management.js +3 -1
- package/dist_ts_interfaces/data/workhoster.d.ts +1 -0
- package/dist_ts_interfaces/requests/config-events.d.ts +35 -0
- package/dist_ts_interfaces/requests/config-events.js +4 -0
- package/dist_ts_interfaces/requests/index.d.ts +1 -0
- package/dist_ts_interfaces/requests/index.js +2 -1
- package/dist_ts_interfaces/requests/realtime.d.ts +1 -1
- package/dist_ts_interfaces/requests/realtime.js +2 -1
- package/dist_ts_migrations/index.js +148 -9
- package/dist_ts_web/00_commitinfo_data.js +1 -1
- package/dist_ts_web/appstate/config-events.d.ts +19 -0
- package/dist_ts_web/appstate/config-events.js +68 -0
- package/dist_ts_web/appstate/runtime.js +7 -1
- package/dist_ts_web/appstate.d.ts +1 -0
- package/dist_ts_web/appstate.js +2 -1
- package/dist_ts_web/elements/ops-platform-events.d.ts +21 -0
- package/dist_ts_web/elements/ops-platform-events.js +361 -0
- package/dist_ts_web/elements/ops-view-logs.d.ts +4 -0
- package/dist_ts_web/elements/ops-view-logs.js +96 -9
- package/package.json +4 -4
- package/readme.plan.md +22 -0
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.dcrouter.ts +72 -2
- package/ts/config/classes.api-token-manager.ts +237 -4
- package/ts/config/classes.gateway-client-manager.ts +463 -19
- package/ts/db/documents/classes.api-token.doc.ts +8 -1
- package/ts/db/documents/classes.cached.email.ts +21 -1
- package/ts/db/documents/classes.gateway-client.doc.ts +86 -0
- package/ts/db/documents/classes.ops-config-event.doc.ts +257 -0
- package/ts/db/documents/index.ts +1 -0
- package/ts/dns/classes.gateway-route-dns-reconciler.ts +801 -0
- package/ts/dns/index.ts +1 -0
- package/ts/dns/manager.dns.ts +36 -0
- package/ts/email/classes.accepted-email-spool.ts +111 -0
- package/ts/email/classes.smartmta-storage-manager.ts +57 -1
- package/ts/email/smartmta-storage-policy.ts +2 -0
- package/ts/monitoring/classes.ops-event-manager.ts +99 -0
- package/ts/monitoring/index.ts +2 -1
- package/ts/opsserver/classes.opsserver.ts +2 -0
- package/ts/opsserver/handlers/certificate.handler.ts +51 -4
- package/ts/opsserver/handlers/config-event.handler.ts +62 -0
- package/ts/opsserver/handlers/dns-record.handler.ts +101 -2
- package/ts/opsserver/handlers/email-ops.handler.ts +42 -8
- package/ts/opsserver/handlers/gatewayclient.handler.ts +804 -136
- package/ts/opsserver/handlers/index.ts +1 -0
- package/ts/opsserver/helpers/gateway-client-auth.ts +198 -0
- package/ts/plugins.ts +2 -0
- package/ts_apiclient/classes.gatewayclient.ts +45 -0
- package/ts_web/00_commitinfo_data.ts +1 -1
- package/ts_web/appstate/config-events.ts +98 -0
- package/ts_web/appstate/runtime.ts +8 -1
- package/ts_web/appstate.ts +1 -0
- package/ts_web/elements/ops-platform-events.ts +315 -0
- package/ts_web/elements/ops-view-logs.ts +84 -7
|
@@ -2,6 +2,21 @@ import * as plugins from '../../plugins.js';
|
|
|
2
2
|
import { DcRouterDb } from '../classes.dcrouter-db.js';
|
|
3
3
|
import type { IApiTokenPolicy, TGatewayClientType } from '../../../ts_interfaces/data/route-management.js';
|
|
4
4
|
|
|
5
|
+
export interface IGatewayClientPendingProvisioning {
|
|
6
|
+
type: TGatewayClientType;
|
|
7
|
+
name: string;
|
|
8
|
+
description: string | null;
|
|
9
|
+
hostnamePatterns: string[];
|
|
10
|
+
allowedRouteTargets: NonNullable<IApiTokenPolicy['allowedRouteTargets']>;
|
|
11
|
+
capabilities: NonNullable<IApiTokenPolicy['capabilities']>;
|
|
12
|
+
enabled: boolean;
|
|
13
|
+
policyGeneration: number;
|
|
14
|
+
policyDigest: string;
|
|
15
|
+
credentialSequence: number;
|
|
16
|
+
stagedAt: number;
|
|
17
|
+
stagedBy: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
5
20
|
const getDb = () => DcRouterDb.getInstance().getDb();
|
|
6
21
|
|
|
7
22
|
@plugins.smartdata.Collection(() => getDb())
|
|
@@ -31,6 +46,23 @@ export class GatewayClientDoc extends plugins.smartdata.SmartDataDbDoc<GatewayCl
|
|
|
31
46
|
@plugins.smartdata.svDb()
|
|
32
47
|
public enabled: boolean = true;
|
|
33
48
|
|
|
49
|
+
@plugins.smartdata.svDb()
|
|
50
|
+
public policyGeneration: number = 1;
|
|
51
|
+
|
|
52
|
+
@plugins.smartdata.svDb()
|
|
53
|
+
public credentialSequence: number = 0;
|
|
54
|
+
|
|
55
|
+
/** Exact provisioned credential sequence authorized for the active policy. */
|
|
56
|
+
@plugins.smartdata.svDb()
|
|
57
|
+
public activeCredentialSequence: number = 0;
|
|
58
|
+
|
|
59
|
+
/** Monotonic compare-and-set authority for every mutable client field. */
|
|
60
|
+
@plugins.smartdata.svDb()
|
|
61
|
+
public mutationRevision: number = 0;
|
|
62
|
+
|
|
63
|
+
@plugins.smartdata.svDb()
|
|
64
|
+
public pendingProvisioning: IGatewayClientPendingProvisioning | null = null;
|
|
65
|
+
|
|
34
66
|
@plugins.smartdata.svDb()
|
|
35
67
|
public createdAt!: number;
|
|
36
68
|
|
|
@@ -48,6 +80,60 @@ export class GatewayClientDoc extends plugins.smartdata.SmartDataDbDoc<GatewayCl
|
|
|
48
80
|
return await GatewayClientDoc.getInstance({ id });
|
|
49
81
|
}
|
|
50
82
|
|
|
83
|
+
private static async getNativeCollection() {
|
|
84
|
+
const smartdataCollection = (GatewayClientDoc as typeof GatewayClientDoc & {
|
|
85
|
+
collection: plugins.smartdata.SmartdataCollection<GatewayClientDoc>;
|
|
86
|
+
}).collection;
|
|
87
|
+
await smartdataCollection.init();
|
|
88
|
+
return smartdataCollection.mongoDbCollection;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
private static buildRevisionSelector(idArg: string, revisionArg: number): Record<string, unknown> {
|
|
92
|
+
if (revisionArg === 0) {
|
|
93
|
+
return {
|
|
94
|
+
id: idArg,
|
|
95
|
+
$or: [
|
|
96
|
+
{ mutationRevision: 0 },
|
|
97
|
+
{ mutationRevision: { $exists: false } },
|
|
98
|
+
],
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
return { id: idArg, mutationRevision: revisionArg };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
public static async compareAndSet(
|
|
105
|
+
idArg: string,
|
|
106
|
+
expectedRevisionArg: number,
|
|
107
|
+
setArg: Record<string, unknown>,
|
|
108
|
+
unsetArg: string[] = [],
|
|
109
|
+
): Promise<GatewayClientDoc | null> {
|
|
110
|
+
const collection = await GatewayClientDoc.getNativeCollection();
|
|
111
|
+
const update: Record<string, unknown> = {
|
|
112
|
+
$set: {
|
|
113
|
+
...setArg,
|
|
114
|
+
mutationRevision: expectedRevisionArg + 1,
|
|
115
|
+
_updatedAt: new Date().toISOString(),
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
if (unsetArg.length > 0) {
|
|
119
|
+
update.$unset = Object.fromEntries(unsetArg.map((fieldArg) => [fieldArg, '']));
|
|
120
|
+
}
|
|
121
|
+
const result = await collection.updateOne(
|
|
122
|
+
GatewayClientDoc.buildRevisionSelector(idArg, expectedRevisionArg),
|
|
123
|
+
update,
|
|
124
|
+
);
|
|
125
|
+
if (result.matchedCount !== 1) return null;
|
|
126
|
+
return await GatewayClientDoc.findById(idArg);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
public static async deleteIfRevision(idArg: string, expectedRevisionArg: number): Promise<boolean> {
|
|
130
|
+
const collection = await GatewayClientDoc.getNativeCollection();
|
|
131
|
+
const result = await collection.deleteOne(
|
|
132
|
+
GatewayClientDoc.buildRevisionSelector(idArg, expectedRevisionArg),
|
|
133
|
+
);
|
|
134
|
+
return result.deletedCount === 1;
|
|
135
|
+
}
|
|
136
|
+
|
|
51
137
|
public static async findAll(): Promise<GatewayClientDoc[]> {
|
|
52
138
|
return await GatewayClientDoc.getInstances({});
|
|
53
139
|
}
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import * as plugins from '../../plugins.js';
|
|
2
|
+
import { DcRouterDb } from '../classes.dcrouter-db.js';
|
|
3
|
+
import type {
|
|
4
|
+
IOpsConfigEvent,
|
|
5
|
+
IOpsConfigEventCursor,
|
|
6
|
+
IOpsConfigEventContext,
|
|
7
|
+
TOpsConfigEventSeverity,
|
|
8
|
+
} from '../../../ts_interfaces/data/config-events.js';
|
|
9
|
+
|
|
10
|
+
const getDb = () => DcRouterDb.getInstance().getDb();
|
|
11
|
+
|
|
12
|
+
export interface IOpsConfigEventFilter {
|
|
13
|
+
acknowledged?: boolean;
|
|
14
|
+
severity?: TOpsConfigEventSeverity;
|
|
15
|
+
category?: string;
|
|
16
|
+
limit?: number;
|
|
17
|
+
cursor?: IOpsConfigEventCursor;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Durable operator-facing platform configuration event (see
|
|
22
|
+
* ts_interfaces/data/config-events.ts). Unacknowledged events persist
|
|
23
|
+
* indefinitely; acknowledged events are pruned after a retention window.
|
|
24
|
+
*/
|
|
25
|
+
@plugins.smartdata.Collection(() => getDb())
|
|
26
|
+
export class OpsConfigEventDoc extends plugins.smartdata.SmartDataDbDoc<OpsConfigEventDoc, OpsConfigEventDoc> implements IOpsConfigEvent {
|
|
27
|
+
@plugins.smartdata.unI()
|
|
28
|
+
@plugins.smartdata.svDb()
|
|
29
|
+
public id!: string;
|
|
30
|
+
|
|
31
|
+
@plugins.smartdata.index()
|
|
32
|
+
@plugins.smartdata.svDb()
|
|
33
|
+
public severity!: TOpsConfigEventSeverity;
|
|
34
|
+
|
|
35
|
+
@plugins.smartdata.index()
|
|
36
|
+
@plugins.smartdata.svDb()
|
|
37
|
+
public category!: string;
|
|
38
|
+
|
|
39
|
+
@plugins.smartdata.svDb()
|
|
40
|
+
public title!: string;
|
|
41
|
+
|
|
42
|
+
@plugins.smartdata.svDb()
|
|
43
|
+
public detail!: string;
|
|
44
|
+
|
|
45
|
+
@plugins.smartdata.svDb()
|
|
46
|
+
public context: IOpsConfigEventContext = {};
|
|
47
|
+
|
|
48
|
+
@plugins.smartdata.index()
|
|
49
|
+
@plugins.smartdata.svDb()
|
|
50
|
+
public createdAt: number = Date.now();
|
|
51
|
+
|
|
52
|
+
@plugins.smartdata.svDb()
|
|
53
|
+
public acknowledgedBy?: string;
|
|
54
|
+
|
|
55
|
+
@plugins.smartdata.index()
|
|
56
|
+
@plugins.smartdata.svDb()
|
|
57
|
+
public acknowledgedAt?: number;
|
|
58
|
+
|
|
59
|
+
/** Sparse uniqueness authority for the current open occurrence. */
|
|
60
|
+
@plugins.smartdata.index({ unique: true, sparse: true })
|
|
61
|
+
@plugins.smartdata.svDb()
|
|
62
|
+
public openDedupeKey?: string;
|
|
63
|
+
|
|
64
|
+
constructor() {
|
|
65
|
+
super();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
public toApiObject(): IOpsConfigEvent {
|
|
69
|
+
return {
|
|
70
|
+
id: this.id,
|
|
71
|
+
severity: this.severity,
|
|
72
|
+
category: this.category,
|
|
73
|
+
title: this.title,
|
|
74
|
+
detail: this.detail,
|
|
75
|
+
context: this.context || {},
|
|
76
|
+
createdAt: this.createdAt,
|
|
77
|
+
acknowledgedBy: this.acknowledgedBy ?? undefined,
|
|
78
|
+
acknowledgedAt: this.acknowledgedAt ?? undefined,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
public override async createSavableObject(): Promise<OpsConfigEventDoc> {
|
|
83
|
+
const savable = await super.createSavableObject() as OpsConfigEventDoc;
|
|
84
|
+
// Presence is the uniqueness authority for an open event. Acknowledged
|
|
85
|
+
// events must not leave a null-valued sparse-index key behind.
|
|
86
|
+
if (!this.openDedupeKey) delete (savable as any).openDedupeKey;
|
|
87
|
+
return savable;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
private static async getNativeCollection() {
|
|
91
|
+
const smartdataCollection = (OpsConfigEventDoc as typeof OpsConfigEventDoc & {
|
|
92
|
+
collection: plugins.smartdata.SmartdataCollection<OpsConfigEventDoc>;
|
|
93
|
+
}).collection;
|
|
94
|
+
await smartdataCollection.init();
|
|
95
|
+
const probe = new OpsConfigEventDoc();
|
|
96
|
+
await smartdataCollection.markUniqueIndexes(probe.uniqueIndexes || []);
|
|
97
|
+
await smartdataCollection.createRegularIndexes(probe.regularIndexes || []);
|
|
98
|
+
return smartdataCollection.mongoDbCollection;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
private static hydrate(rawArg: Record<string, unknown>): OpsConfigEventDoc {
|
|
102
|
+
return OpsConfigEventDoc.createInstanceFromMongoDbNativeDoc(rawArg as any);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
public static async upsertOpenEvent(optionsArg: {
|
|
106
|
+
id: string;
|
|
107
|
+
openDedupeKey: string;
|
|
108
|
+
severity: TOpsConfigEventSeverity;
|
|
109
|
+
category: string;
|
|
110
|
+
title: string;
|
|
111
|
+
detail: string;
|
|
112
|
+
context: IOpsConfigEventContext;
|
|
113
|
+
createdAt: number;
|
|
114
|
+
}): Promise<{ event: OpsConfigEventDoc; created: boolean }> {
|
|
115
|
+
const collection = await OpsConfigEventDoc.getNativeCollection();
|
|
116
|
+
const timestamp = new Date(optionsArg.createdAt).toISOString();
|
|
117
|
+
const result = await collection.updateOne(
|
|
118
|
+
{ openDedupeKey: optionsArg.openDedupeKey },
|
|
119
|
+
{
|
|
120
|
+
$set: {
|
|
121
|
+
severity: optionsArg.severity,
|
|
122
|
+
detail: optionsArg.detail,
|
|
123
|
+
context: optionsArg.context,
|
|
124
|
+
createdAt: optionsArg.createdAt,
|
|
125
|
+
_updatedAt: timestamp,
|
|
126
|
+
},
|
|
127
|
+
$setOnInsert: {
|
|
128
|
+
id: optionsArg.id,
|
|
129
|
+
category: optionsArg.category,
|
|
130
|
+
title: optionsArg.title,
|
|
131
|
+
openDedupeKey: optionsArg.openDedupeKey,
|
|
132
|
+
_createdAt: timestamp,
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
{ upsert: true },
|
|
136
|
+
);
|
|
137
|
+
const raw = await collection.findOne({ openDedupeKey: optionsArg.openDedupeKey });
|
|
138
|
+
if (!raw) throw new Error('Failed to read the recorded platform configuration event');
|
|
139
|
+
return {
|
|
140
|
+
event: OpsConfigEventDoc.hydrate(raw as Record<string, unknown>),
|
|
141
|
+
created: result.upsertedCount > 0,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
public static async findFiltered(
|
|
146
|
+
filter: IOpsConfigEventFilter = {},
|
|
147
|
+
): Promise<{ events: OpsConfigEventDoc[]; total: number; nextCursor?: IOpsConfigEventCursor }> {
|
|
148
|
+
const selector: Record<string, unknown> = {};
|
|
149
|
+
if (filter.acknowledged === true) selector.acknowledgedAt = { $gt: 0 };
|
|
150
|
+
if (filter.acknowledged === false) selector.acknowledgedAt = null;
|
|
151
|
+
if (filter.severity) selector.severity = filter.severity;
|
|
152
|
+
if (filter.category) selector.category = filter.category;
|
|
153
|
+
const requestedLimit = Number.isSafeInteger(filter.limit) && filter.limit! > 0
|
|
154
|
+
? filter.limit!
|
|
155
|
+
: 100;
|
|
156
|
+
const limit = Math.min(requestedLimit, 200);
|
|
157
|
+
const countSelector = structuredClone(selector);
|
|
158
|
+
if (filter.cursor) {
|
|
159
|
+
if (!Number.isSafeInteger(filter.cursor.createdAt) || filter.cursor.createdAt < 0
|
|
160
|
+
|| typeof filter.cursor.id !== 'string' || !filter.cursor.id.trim()) {
|
|
161
|
+
throw new Error('Invalid platform configuration event cursor');
|
|
162
|
+
}
|
|
163
|
+
selector.$or = [
|
|
164
|
+
{ createdAt: { $lt: filter.cursor.createdAt } },
|
|
165
|
+
{ createdAt: filter.cursor.createdAt, id: { $lt: filter.cursor.id } },
|
|
166
|
+
];
|
|
167
|
+
}
|
|
168
|
+
const collection = await OpsConfigEventDoc.getNativeCollection();
|
|
169
|
+
const cursor = collection
|
|
170
|
+
.find(selector)
|
|
171
|
+
.sort({ createdAt: -1, id: -1 })
|
|
172
|
+
.limit(limit + 1);
|
|
173
|
+
try {
|
|
174
|
+
const [rawEvents, total] = await Promise.all([
|
|
175
|
+
cursor.toArray(),
|
|
176
|
+
collection.countDocuments(countSelector),
|
|
177
|
+
]);
|
|
178
|
+
const hasMore = rawEvents.length > limit;
|
|
179
|
+
const pageRows = rawEvents.slice(0, limit);
|
|
180
|
+
const lastRow = pageRows.at(-1) as Record<string, unknown> | undefined;
|
|
181
|
+
return {
|
|
182
|
+
events: pageRows.map((rawArg) => OpsConfigEventDoc.hydrate(rawArg as Record<string, unknown>)),
|
|
183
|
+
total,
|
|
184
|
+
...(hasMore && lastRow ? {
|
|
185
|
+
nextCursor: {
|
|
186
|
+
createdAt: Number(lastRow.createdAt),
|
|
187
|
+
id: String(lastRow.id),
|
|
188
|
+
},
|
|
189
|
+
} : {}),
|
|
190
|
+
};
|
|
191
|
+
} finally {
|
|
192
|
+
await cursor.close();
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
public static async countUnacknowledged(): Promise<number> {
|
|
197
|
+
const collection = await OpsConfigEventDoc.getNativeCollection();
|
|
198
|
+
return await collection.countDocuments({ acknowledgedAt: null });
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** Acknowledge the given event ids; returns how many were newly acknowledged. */
|
|
202
|
+
public static async acknowledgeByIds(ids: string[], userId: string): Promise<number> {
|
|
203
|
+
if (typeof userId !== 'string' || !userId.trim()) {
|
|
204
|
+
throw new Error('acknowledgeByIds requires a non-empty user id');
|
|
205
|
+
}
|
|
206
|
+
if (ids.length > 500 || ids.some((idArg) => typeof idArg !== 'string' || !idArg.trim())) {
|
|
207
|
+
throw new Error('acknowledgeByIds requires 0 to 500 non-empty event ids');
|
|
208
|
+
}
|
|
209
|
+
const uniqueIds = Array.from(new Set(ids));
|
|
210
|
+
if (uniqueIds.length === 0) return 0;
|
|
211
|
+
const collection = await OpsConfigEventDoc.getNativeCollection();
|
|
212
|
+
const now = Date.now();
|
|
213
|
+
const result = await collection.updateMany(
|
|
214
|
+
{ id: { $in: uniqueIds }, acknowledgedAt: null },
|
|
215
|
+
{
|
|
216
|
+
$set: {
|
|
217
|
+
acknowledgedBy: userId,
|
|
218
|
+
acknowledgedAt: now,
|
|
219
|
+
_updatedAt: new Date(now).toISOString(),
|
|
220
|
+
},
|
|
221
|
+
$unset: { openDedupeKey: '' },
|
|
222
|
+
},
|
|
223
|
+
);
|
|
224
|
+
return result.modifiedCount;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** Delete acknowledged events older than the retention window. */
|
|
228
|
+
public static async pruneAcknowledged(maxAgeMs: number): Promise<number> {
|
|
229
|
+
const cutoff = Date.now() - maxAgeMs;
|
|
230
|
+
let pruned = 0;
|
|
231
|
+
const collection = await OpsConfigEventDoc.getNativeCollection();
|
|
232
|
+
while (true) {
|
|
233
|
+
const cursor = collection
|
|
234
|
+
.find(
|
|
235
|
+
{ acknowledgedAt: { $lt: cutoff } },
|
|
236
|
+
{ projection: { _id: 1 } },
|
|
237
|
+
)
|
|
238
|
+
.sort({ acknowledgedAt: 1, _id: 1 })
|
|
239
|
+
.limit(500);
|
|
240
|
+
let rows: Array<{ _id: unknown }>;
|
|
241
|
+
try {
|
|
242
|
+
rows = await cursor.toArray() as Array<{ _id: unknown }>;
|
|
243
|
+
} finally {
|
|
244
|
+
await cursor.close();
|
|
245
|
+
}
|
|
246
|
+
if (rows.length === 0) break;
|
|
247
|
+
const deleteFilter: Record<string, any> = {
|
|
248
|
+
_id: { $in: rows.map((rowArg) => rowArg._id) },
|
|
249
|
+
acknowledgedAt: { $lt: cutoff },
|
|
250
|
+
};
|
|
251
|
+
const result = await collection.deleteMany(deleteFilter);
|
|
252
|
+
if (result.deletedCount === 0) break;
|
|
253
|
+
pruned += result.deletedCount;
|
|
254
|
+
}
|
|
255
|
+
return pruned;
|
|
256
|
+
}
|
|
257
|
+
}
|
package/ts/db/documents/index.ts
CHANGED