@topolo/sdk 0.4.6 → 0.4.8
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/dist/index.cjs +161 -144
- package/dist/index.d.cts +96 -104
- package/dist/index.d.ts +96 -104
- package/dist/index.js +161 -144
- package/package.json +1 -1
- package/src/application-requirements.ts +12 -6
- package/src/applications.generated.ts +61 -114
- package/src/client.test.ts +74 -0
- package/src/client.ts +101 -0
- package/src/index.test.ts +69 -0
- package/src/index.ts +1 -0
- package/src/modules/crm.test.ts +208 -0
- package/src/modules/identity.test.ts +165 -0
- package/src/modules/p2p.test.ts +286 -0
- package/src/oauth.test.ts +103 -0
- package/src/services.generated.ts +14 -15
- package/src/services.test.ts +19 -8
- package/src/widgets.test.ts +255 -1
- package/src/widgets.ts +5 -3
package/src/widgets.test.ts
CHANGED
|
@@ -2,11 +2,23 @@ import { describe, expect, it } from "vitest";
|
|
|
2
2
|
import {
|
|
3
3
|
createTopoloWidgetError,
|
|
4
4
|
createTopoloWidgetResponse,
|
|
5
|
+
defineTopoloChartWidget,
|
|
6
|
+
defineTopoloNotificationsWidget,
|
|
7
|
+
defineTopoloQuickActionsWidget,
|
|
8
|
+
defineTopoloRecentActivityWidget,
|
|
9
|
+
defineTopoloStatsWidget,
|
|
5
10
|
TOPOLO_WIDGET_ENDPOINT,
|
|
6
11
|
TOPOLO_WIDGET_REFRESH_INTERVAL_MS,
|
|
7
12
|
validateTopoloWidgetResponse,
|
|
8
13
|
} from "./widgets.js";
|
|
9
14
|
|
|
15
|
+
const BASE = {
|
|
16
|
+
serviceId: "service_x",
|
|
17
|
+
serviceName: "X",
|
|
18
|
+
lastUpdated: "2026-04-25T00:00:00.000Z",
|
|
19
|
+
snapshot: { summary: "A summary that is long enough to read." },
|
|
20
|
+
};
|
|
21
|
+
|
|
10
22
|
describe("Topolo widget contract", () => {
|
|
11
23
|
it("exports the platform widget endpoint defaults", () => {
|
|
12
24
|
expect(TOPOLO_WIDGET_ENDPOINT).toBe("/api/widget");
|
|
@@ -21,6 +33,9 @@ describe("Topolo widget contract", () => {
|
|
|
21
33
|
widgets: [
|
|
22
34
|
{
|
|
23
35
|
type: "stats",
|
|
36
|
+
snapshot: {
|
|
37
|
+
summary: "Bytes is reporting 12 files in the current workspace.",
|
|
38
|
+
},
|
|
24
39
|
stats: [{ key: "files", label: "Files", value: 12 }],
|
|
25
40
|
},
|
|
26
41
|
],
|
|
@@ -34,6 +49,9 @@ describe("Topolo widget contract", () => {
|
|
|
34
49
|
serviceName: "Bytes",
|
|
35
50
|
lastUpdated: "2026-04-25T00:00:00.000Z",
|
|
36
51
|
type: "stats",
|
|
52
|
+
snapshot: {
|
|
53
|
+
summary: "Bytes is reporting 12 files in the current workspace.",
|
|
54
|
+
},
|
|
37
55
|
stats: [{ key: "files", label: "Files", value: 12 }],
|
|
38
56
|
},
|
|
39
57
|
],
|
|
@@ -51,6 +69,9 @@ describe("Topolo widget contract", () => {
|
|
|
51
69
|
serviceName: "CRM",
|
|
52
70
|
lastUpdated: "2026-04-24T23:00:00.000Z",
|
|
53
71
|
type: "stats",
|
|
72
|
+
snapshot: {
|
|
73
|
+
summary: "CRM is tracking 3 contacts for this workspace.",
|
|
74
|
+
},
|
|
54
75
|
stats: [{ label: "Contacts", value: 3 }],
|
|
55
76
|
},
|
|
56
77
|
],
|
|
@@ -79,6 +100,9 @@ describe("Topolo widget contract", () => {
|
|
|
79
100
|
widgets: [
|
|
80
101
|
{
|
|
81
102
|
type: "notifications",
|
|
103
|
+
snapshot: {
|
|
104
|
+
summary: "CRM has one unread lead notification for review.",
|
|
105
|
+
},
|
|
82
106
|
unreadCount: 1,
|
|
83
107
|
notifications: [
|
|
84
108
|
{
|
|
@@ -100,7 +124,7 @@ describe("Topolo widget contract", () => {
|
|
|
100
124
|
});
|
|
101
125
|
});
|
|
102
126
|
|
|
103
|
-
it("keeps
|
|
127
|
+
it("keeps required snapshot metadata for configurable TopoloOne datapoints", () => {
|
|
104
128
|
const response = createTopoloWidgetResponse({
|
|
105
129
|
serviceId: "service_admin",
|
|
106
130
|
serviceName: "Admin",
|
|
@@ -111,6 +135,8 @@ describe("Topolo widget contract", () => {
|
|
|
111
135
|
stats: [{ label: "Team Members", value: 5 }],
|
|
112
136
|
snapshot: {
|
|
113
137
|
presetKey: "admin_morning_snapshot",
|
|
138
|
+
summary:
|
|
139
|
+
"Admin has 5 team members visible in the current workspace snapshot.",
|
|
114
140
|
defaultMetricKeys: ["team_members"],
|
|
115
141
|
availableMetrics: [
|
|
116
142
|
{
|
|
@@ -131,6 +157,8 @@ describe("Topolo widget contract", () => {
|
|
|
131
157
|
});
|
|
132
158
|
expect(response.widgets[0]).toMatchObject({
|
|
133
159
|
snapshot: {
|
|
160
|
+
summary:
|
|
161
|
+
"Admin has 5 team members visible in the current workspace snapshot.",
|
|
134
162
|
defaultMetricKeys: ["team_members"],
|
|
135
163
|
availableMetrics: [
|
|
136
164
|
{ key: "team_members", label: "Team Members", value: 5 },
|
|
@@ -156,7 +184,233 @@ describe("Topolo widget contract", () => {
|
|
|
156
184
|
expect(result.errors).toEqual([
|
|
157
185
|
"widgets[0].serviceId must be a non-empty string",
|
|
158
186
|
"widgets[0].lastUpdated must be a non-empty string",
|
|
187
|
+
"widgets[0].snapshot must be an object",
|
|
159
188
|
"widgets[0].type must be a supported Topolo widget type",
|
|
160
189
|
]);
|
|
161
190
|
});
|
|
191
|
+
|
|
192
|
+
it("stamps the literal type onto each define* helper", () => {
|
|
193
|
+
expect(defineTopoloStatsWidget({ ...BASE, stats: [] }).type).toBe("stats");
|
|
194
|
+
expect(
|
|
195
|
+
defineTopoloRecentActivityWidget({ ...BASE, activities: [] }).type,
|
|
196
|
+
).toBe("recent-activity");
|
|
197
|
+
expect(
|
|
198
|
+
defineTopoloQuickActionsWidget({ ...BASE, actions: [] }).type,
|
|
199
|
+
).toBe("quick-actions");
|
|
200
|
+
expect(
|
|
201
|
+
defineTopoloNotificationsWidget({ ...BASE, notifications: [], unreadCount: 0 })
|
|
202
|
+
.type,
|
|
203
|
+
).toBe("notifications");
|
|
204
|
+
expect(
|
|
205
|
+
defineTopoloChartWidget({
|
|
206
|
+
...BASE,
|
|
207
|
+
chartType: "line",
|
|
208
|
+
title: "Trend",
|
|
209
|
+
data: { labels: ["a"], values: [1] },
|
|
210
|
+
}).type,
|
|
211
|
+
).toBe("chart");
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it("derives lastUpdated from a Date instance passed as now", () => {
|
|
215
|
+
const response = createTopoloWidgetResponse({
|
|
216
|
+
serviceId: "service_x",
|
|
217
|
+
serviceName: "X",
|
|
218
|
+
now: new Date("2026-05-01T12:00:00.000Z"),
|
|
219
|
+
widgets: [
|
|
220
|
+
{
|
|
221
|
+
type: "stats",
|
|
222
|
+
snapshot: { summary: "Date branch summary content here." },
|
|
223
|
+
stats: [],
|
|
224
|
+
},
|
|
225
|
+
],
|
|
226
|
+
});
|
|
227
|
+
expect(response.widgets[0]!.lastUpdated).toBe("2026-05-01T12:00:00.000Z");
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it("generates a current timestamp when now is omitted", () => {
|
|
231
|
+
const before = Date.now();
|
|
232
|
+
const response = createTopoloWidgetResponse({
|
|
233
|
+
serviceId: "service_x",
|
|
234
|
+
serviceName: "X",
|
|
235
|
+
widgets: [
|
|
236
|
+
{
|
|
237
|
+
type: "stats",
|
|
238
|
+
snapshot: { summary: "Omitted-now summary content here." },
|
|
239
|
+
stats: [],
|
|
240
|
+
},
|
|
241
|
+
],
|
|
242
|
+
});
|
|
243
|
+
const stamped = Date.parse(response.widgets[0]!.lastUpdated);
|
|
244
|
+
expect(stamped).toBeGreaterThanOrEqual(before);
|
|
245
|
+
expect(Number.isNaN(stamped)).toBe(false);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it("rejects a non-object response", () => {
|
|
249
|
+
expect(validateTopoloWidgetResponse(null)).toEqual({
|
|
250
|
+
ok: false,
|
|
251
|
+
errors: ["response must be an object"],
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it("rejects a non-boolean success and a non-array widgets field", () => {
|
|
256
|
+
const result = validateTopoloWidgetResponse({ success: "yes", widgets: "nope" });
|
|
257
|
+
expect(result.ok).toBe(false);
|
|
258
|
+
expect(result.errors).toContain("success must be a boolean");
|
|
259
|
+
expect(result.errors).toContain("widgets must be an array");
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it("flags a non-string error on a failed response", () => {
|
|
263
|
+
const result = validateTopoloWidgetResponse({
|
|
264
|
+
success: false,
|
|
265
|
+
widgets: [],
|
|
266
|
+
error: 42,
|
|
267
|
+
});
|
|
268
|
+
expect(result.errors).toContain("error must be a string when provided");
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it("flags a chart widget whose data is not an object", () => {
|
|
272
|
+
const result = validateTopoloWidgetResponse({
|
|
273
|
+
success: true,
|
|
274
|
+
widgets: [
|
|
275
|
+
{
|
|
276
|
+
...BASE,
|
|
277
|
+
type: "chart",
|
|
278
|
+
chartType: "bar",
|
|
279
|
+
title: "T",
|
|
280
|
+
data: "not-an-object",
|
|
281
|
+
},
|
|
282
|
+
],
|
|
283
|
+
});
|
|
284
|
+
expect(result.errors).toContain("widgets[0].data must be an object");
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it("flags non-number unreadCount and missing notifications array", () => {
|
|
288
|
+
const result = validateTopoloWidgetResponse({
|
|
289
|
+
success: true,
|
|
290
|
+
widgets: [
|
|
291
|
+
{
|
|
292
|
+
...BASE,
|
|
293
|
+
type: "notifications",
|
|
294
|
+
notifications: "x",
|
|
295
|
+
unreadCount: "many",
|
|
296
|
+
},
|
|
297
|
+
],
|
|
298
|
+
});
|
|
299
|
+
expect(result.errors).toContain("widgets[0].notifications must be an array");
|
|
300
|
+
expect(result.errors).toContain("widgets[0].unreadCount must be a number");
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
it("flags malformed snapshot metadata arrays and metric entries", () => {
|
|
304
|
+
const result = validateTopoloWidgetResponse({
|
|
305
|
+
success: true,
|
|
306
|
+
widgets: [
|
|
307
|
+
{
|
|
308
|
+
...BASE,
|
|
309
|
+
snapshot: {
|
|
310
|
+
summary: "Snapshot with bad metadata arrays present here.",
|
|
311
|
+
defaultMetricKeys: "not-array",
|
|
312
|
+
highlights: "not-array",
|
|
313
|
+
availableMetrics: [{ key: "", label: "", value: "NaN" }],
|
|
314
|
+
},
|
|
315
|
+
type: "stats",
|
|
316
|
+
stats: [],
|
|
317
|
+
},
|
|
318
|
+
],
|
|
319
|
+
});
|
|
320
|
+
expect(result.errors).toContain(
|
|
321
|
+
"widgets[0].snapshot.defaultMetricKeys must be an array when provided",
|
|
322
|
+
);
|
|
323
|
+
expect(result.errors).toContain(
|
|
324
|
+
"widgets[0].snapshot.highlights must be an array when provided",
|
|
325
|
+
);
|
|
326
|
+
expect(result.errors).toContain(
|
|
327
|
+
"widgets[0].snapshot.availableMetrics[0].key must be a non-empty string",
|
|
328
|
+
);
|
|
329
|
+
expect(result.errors).toContain(
|
|
330
|
+
"widgets[0].snapshot.availableMetrics[0].value must be a number",
|
|
331
|
+
);
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
it("flags availableMetrics when it is present but not an array", () => {
|
|
335
|
+
const result = validateTopoloWidgetResponse({
|
|
336
|
+
success: true,
|
|
337
|
+
widgets: [
|
|
338
|
+
{
|
|
339
|
+
...BASE,
|
|
340
|
+
snapshot: {
|
|
341
|
+
summary: "Snapshot summary with bad availableMetrics field.",
|
|
342
|
+
availableMetrics: "nope",
|
|
343
|
+
},
|
|
344
|
+
type: "stats",
|
|
345
|
+
stats: [],
|
|
346
|
+
},
|
|
347
|
+
],
|
|
348
|
+
});
|
|
349
|
+
expect(result.errors).toContain(
|
|
350
|
+
"widgets[0].snapshot.availableMetrics must be an array when provided",
|
|
351
|
+
);
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
it("flags a non-object widget entry and a missing snapshot", () => {
|
|
355
|
+
const result = validateTopoloWidgetResponse({
|
|
356
|
+
success: true,
|
|
357
|
+
widgets: [42],
|
|
358
|
+
});
|
|
359
|
+
expect(result.errors).toContain("widgets[0] must be an object");
|
|
360
|
+
|
|
361
|
+
const result2 = validateTopoloWidgetResponse({
|
|
362
|
+
success: true,
|
|
363
|
+
widgets: [
|
|
364
|
+
{
|
|
365
|
+
serviceId: "s",
|
|
366
|
+
serviceName: "n",
|
|
367
|
+
lastUpdated: "2026-04-25T00:00:00.000Z",
|
|
368
|
+
type: "stats",
|
|
369
|
+
stats: [],
|
|
370
|
+
},
|
|
371
|
+
],
|
|
372
|
+
});
|
|
373
|
+
expect(result2.errors).toContain("widgets[0].snapshot must be an object");
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
it("flags a non-object availableMetrics entry", () => {
|
|
377
|
+
const result = validateTopoloWidgetResponse({
|
|
378
|
+
success: true,
|
|
379
|
+
widgets: [
|
|
380
|
+
{
|
|
381
|
+
...BASE,
|
|
382
|
+
snapshot: {
|
|
383
|
+
summary: "Snapshot with a non-object metric entry here.",
|
|
384
|
+
availableMetrics: [42],
|
|
385
|
+
},
|
|
386
|
+
type: "stats",
|
|
387
|
+
stats: [],
|
|
388
|
+
},
|
|
389
|
+
],
|
|
390
|
+
});
|
|
391
|
+
expect(result.errors).toContain(
|
|
392
|
+
"widgets[0].snapshot.availableMetrics[0] must be an object",
|
|
393
|
+
);
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
it("reports missing snapshot summaries", () => {
|
|
397
|
+
const result = validateTopoloWidgetResponse({
|
|
398
|
+
success: true,
|
|
399
|
+
widgets: [
|
|
400
|
+
{
|
|
401
|
+
serviceId: "service_bad",
|
|
402
|
+
serviceName: "Bad App",
|
|
403
|
+
lastUpdated: "2026-04-25T00:00:00.000Z",
|
|
404
|
+
type: "stats",
|
|
405
|
+
snapshot: { summary: "" },
|
|
406
|
+
stats: [],
|
|
407
|
+
},
|
|
408
|
+
],
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
expect(result.ok).toBe(false);
|
|
412
|
+
expect(result.errors).toEqual([
|
|
413
|
+
"widgets[0].snapshot.summary must be a non-empty string",
|
|
414
|
+
]);
|
|
415
|
+
});
|
|
162
416
|
});
|
package/src/widgets.ts
CHANGED
|
@@ -14,7 +14,7 @@ export interface TopoloWidgetBase {
|
|
|
14
14
|
serviceIcon?: string;
|
|
15
15
|
type: TopoloWidgetType;
|
|
16
16
|
lastUpdated: string;
|
|
17
|
-
snapshot
|
|
17
|
+
snapshot: TopoloWidgetSnapshotMetadata;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
export interface TopoloWidgetMetric {
|
|
@@ -41,7 +41,7 @@ export interface TopoloWidgetSnapshotMetadata {
|
|
|
41
41
|
scope?: string;
|
|
42
42
|
scopeLabel?: string;
|
|
43
43
|
generatedAt?: string;
|
|
44
|
-
summary
|
|
44
|
+
summary: string;
|
|
45
45
|
defaultMetricKeys?: string[];
|
|
46
46
|
availableMetrics?: TopoloWidgetSnapshotMetric[];
|
|
47
47
|
highlights?: string[];
|
|
@@ -291,12 +291,14 @@ function validateSnapshotMetadata(
|
|
|
291
291
|
errors: string[],
|
|
292
292
|
) {
|
|
293
293
|
if (snapshot === undefined) {
|
|
294
|
+
errors.push(`${path} must be an object`);
|
|
294
295
|
return;
|
|
295
296
|
}
|
|
296
297
|
if (!isRecord(snapshot)) {
|
|
297
|
-
errors.push(`${path} must be an object
|
|
298
|
+
errors.push(`${path} must be an object`);
|
|
298
299
|
return;
|
|
299
300
|
}
|
|
301
|
+
validateNonEmptyString(snapshot.summary, `${path}.summary`, errors);
|
|
300
302
|
if (
|
|
301
303
|
snapshot.defaultMetricKeys !== undefined &&
|
|
302
304
|
!Array.isArray(snapshot.defaultMetricKeys)
|