@larc-iu/plaid-client 0.2.0-alpha.3 → 0.2.0-alpha.30

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.
Files changed (73) hide show
  1. package/README.md +39 -0
  2. package/package.json +13 -6
  3. package/src/apiAnnotations.d.ts +508 -0
  4. package/src/apiAnnotations.js +543 -0
  5. package/src/apiDocuments.d.ts +569 -0
  6. package/src/apiDocuments.js +551 -0
  7. package/src/apiLayers.d.ts +369 -0
  8. package/src/apiLayers.js +222 -0
  9. package/src/apiMessages.d.ts +293 -0
  10. package/src/apiMessages.js +265 -0
  11. package/src/apiPeople.d.ts +626 -0
  12. package/src/apiPeople.js +698 -0
  13. package/src/apiProject.d.ts +761 -0
  14. package/src/apiProject.js +774 -0
  15. package/src/apiServer.d.ts +455 -0
  16. package/src/apiServer.js +422 -0
  17. package/src/apiVocab.d.ts +456 -0
  18. package/src/apiVocab.js +425 -0
  19. package/src/batch.d.ts +54 -0
  20. package/src/batch.js +323 -0
  21. package/src/batchRef.d.ts +30 -0
  22. package/src/batchRef.js +128 -0
  23. package/src/client.d.ts +1321 -0
  24. package/src/client.js +743 -0
  25. package/src/codepoint.d.ts +94 -0
  26. package/src/codepoint.js +131 -1
  27. package/src/constraints.d.ts +85 -0
  28. package/src/constraints.js +112 -0
  29. package/src/created.d.ts +2 -0
  30. package/src/created.js +29 -0
  31. package/src/documentLock.d.ts +172 -0
  32. package/src/documentLock.js +384 -0
  33. package/src/entryForms.d.ts +63 -0
  34. package/src/entryForms.js +152 -0
  35. package/src/events.d.ts +60 -0
  36. package/src/events.js +271 -0
  37. package/src/families.d.ts +349 -0
  38. package/src/families.js +396 -0
  39. package/src/http.d.ts +233 -0
  40. package/src/http.js +809 -93
  41. package/src/ids.d.ts +26 -0
  42. package/src/ids.js +96 -0
  43. package/src/index.d.ts +101 -0
  44. package/src/index.js +183 -2105
  45. package/src/metadataOps.d.ts +15 -0
  46. package/src/metadataOps.js +124 -0
  47. package/src/operations.d.ts +36 -0
  48. package/src/operations.js +164 -0
  49. package/src/pagination.d.ts +49 -0
  50. package/src/pagination.js +20 -53
  51. package/src/provenance.d.ts +243 -0
  52. package/src/provenance.js +458 -36
  53. package/src/replayed.d.ts +3 -0
  54. package/src/replayed.js +31 -0
  55. package/src/requester.d.ts +60 -0
  56. package/src/requester.js +74 -0
  57. package/src/roles.d.ts +104 -0
  58. package/src/roles.js +64 -2
  59. package/src/serviceSchema.d.ts +337 -0
  60. package/src/serviceSchema.js +177 -60
  61. package/src/services.d.ts +319 -0
  62. package/src/services.js +742 -58
  63. package/src/sse.d.ts +30 -0
  64. package/src/sse.js +80 -3
  65. package/src/testing.d.ts +128 -0
  66. package/src/testing.js +379 -0
  67. package/src/textEdits.d.ts +59 -0
  68. package/src/textEdits.js +234 -0
  69. package/src/transforms.d.ts +25 -0
  70. package/src/transforms.js +11 -1
  71. package/src/types.d.ts +867 -0
  72. package/src/types.js +663 -0
  73. package/index.d.ts +0 -532
@@ -0,0 +1,422 @@
1
+ /**
2
+ * The server bundles: the server's facts and health, the instance admin
3
+ * controls and the instance-wide audit log, each a factory of the client
4
+ * (client.js installs them).
5
+ */
6
+
7
+ import { auditQuery } from "./families.js";
8
+ import { iterPages, listAll, listPage } from "./pagination.js";
9
+ import { PlaidClient } from "./client.js";
10
+
11
+ /**
12
+ * How long `admin.backup()` waits for the server to finish writing a backup:
13
+ * minutes on a large database, past the usual per-request timeout.
14
+ */
15
+ export const BACKUP_TIMEOUT_MS = 30 * 60 * 1000;
16
+
17
+ /**
18
+ * @typedef {import("./types.js").TextEditOp} TextEditOp
19
+ * @typedef {import("./types.js").TextGap} TextGap
20
+ * @typedef {import("./types.js").MetadataOp} MetadataOp
21
+ * @typedef {import("./types.js").LayerConstraint} LayerConstraint
22
+ * @typedef {import("./types.js").ConstraintWriteOptions} ConstraintWriteOptions
23
+ * @typedef {import("./types.js").ConstraintViolation} ConstraintViolation
24
+ * @typedef {import("./types.js").Requester} Requester
25
+ * @typedef {import("./types.js").BatchRef} BatchRef
26
+ * @typedef {import("./types.js").ServiceParam} ServiceParam
27
+ * @typedef {import("./types.js").ServiceInfo} ServiceInfo
28
+ * @typedef {import("./types.js").DiscoveredService} DiscoveredService
29
+ * @typedef {import("./types.js").ResponseHelper} ResponseHelper
30
+ * @typedef {import("./types.js").ServiceRegistration} ServiceRegistration
31
+ * @typedef {import("./types.js").ServiceStatusEvent} ServiceStatusEvent
32
+ * @typedef {import("./types.js").OperationGroupsBundle} OperationGroupsBundle
33
+ * @typedef {import("./types.js").PlaidClientOptions} PlaidClientOptions
34
+ * @typedef {import("./types.js").OperationKind} OperationKind
35
+ * @typedef {import("./types.js").KeySeed} KeySeed
36
+ * @typedef {import("./types.js").PlaidBatch} PlaidBatch
37
+ * @typedef {import("./types.js").AuditPageOptions} AuditPageOptions
38
+ * @typedef {import("./types.js").VocabItemRestoreSummary} VocabItemRestoreSummary
39
+ * @typedef {import("./types.js").UserDataEntry} UserDataEntry
40
+ * @typedef {import("./types.js").AdminUserDataEntry} AdminUserDataEntry
41
+ * @typedef {import("./types.js").ActivityTallyRow} ActivityTallyRow
42
+ * @typedef {import("./types.js").ConstraintRepair} ConstraintRepair
43
+ * @typedef {import("./types.js").ValueSetRule} ValueSetRule
44
+ * @typedef {import("./types.js").RefusedValues} RefusedValues
45
+ * @typedef {import("./types.js").CommentableType} CommentableType
46
+ * @typedef {import("./types.js").Comment} Comment
47
+ * @typedef {import("./types.js").Guideline} Guideline
48
+ * @typedef {import("./types.js").ClientEventType} ClientEventType
49
+ * @typedef {import("./types.js").ClientEvent} ClientEvent
50
+ * @typedef {import("./types.js").Invite} Invite
51
+ * @typedef {import("./types.js").ServerLimits} ServerLimits
52
+ * @typedef {import("./types.js").ServerHealth} ServerHealth
53
+ * @typedef {import("./types.js").RateLimitBucket} RateLimitBucket
54
+ * @typedef {import("./types.js").RateLimitSnapshot} RateLimitSnapshot
55
+ * @typedef {import("./types.js").BackupStatus} BackupStatus
56
+ * @typedef {import("./types.js").ServerReport} ServerReport
57
+ * @typedef {import("./types.js").LiveLog} LiveLog
58
+ * @typedef {import("./types.js").SSEConnection} SSEConnection
59
+ * @typedef {import("./types.js").SSEStats} SSEStats
60
+ * @typedef {import("./types.js").AtomicValue} AtomicValue
61
+ * @typedef {import("./types.js").Created} Created
62
+ * @typedef {import("./types.js").Project} Project
63
+ * @typedef {import("./types.js").ProjectTextLayer} ProjectTextLayer
64
+ * @typedef {import("./types.js").ProjectTokenLayer} ProjectTokenLayer
65
+ * @typedef {import("./types.js").ProjectSpanLayer} ProjectSpanLayer
66
+ * @typedef {import("./types.js").ProjectRelationLayer} ProjectRelationLayer
67
+ * @typedef {import("./types.js").ProjectVocab} ProjectVocab
68
+ * @typedef {import("./types.js").PlaidDocument} PlaidDocument
69
+ * @typedef {import("./types.js").DocumentTextLayer} DocumentTextLayer
70
+ * @typedef {import("./types.js").DocumentTokenLayer} DocumentTokenLayer
71
+ * @typedef {import("./types.js").DocumentSpanLayer} DocumentSpanLayer
72
+ * @typedef {import("./types.js").PlaidText} PlaidText
73
+ * @typedef {import("./types.js").TextReshape} TextReshape
74
+ * @typedef {import("./types.js").Token} Token
75
+ * @typedef {import("./types.js").Span} Span
76
+ * @typedef {import("./types.js").Relation} Relation
77
+ * @typedef {import("./types.js").VocabItem} VocabItem
78
+ * @typedef {import("./types.js").VocabLink} VocabLink
79
+ * @typedef {import("./types.js").VocabLayer} VocabLayer
80
+ * @typedef {import("./types.js").TextLayer} TextLayer
81
+ * @typedef {import("./types.js").TokenLayer} TokenLayer
82
+ * @typedef {import("./types.js").SpanLayer} SpanLayer
83
+ * @typedef {import("./types.js").RelationLayer} RelationLayer
84
+ * @typedef {import("./types.js").MetadataRecords} MetadataRecords
85
+ * @typedef {import("./types.js").LayerRecords} LayerRecords
86
+ * @typedef {import("./types.js").User} User
87
+ * @typedef {import("./types.js").ProjectListEntry} ProjectListEntry
88
+ * @typedef {import("./types.js").DocumentListEntry} DocumentListEntry
89
+ * @typedef {import("./types.js").ApiToken} ApiToken
90
+ * @typedef {import("./types.js").AuditUser} AuditUser
91
+ * @typedef {import("./types.js").AuditTarget} AuditTarget
92
+ * @typedef {import("./types.js").AuditOp} AuditOp
93
+ * @typedef {import("./types.js").AuditEntry} AuditEntry
94
+ * @typedef {import("./types.js").LockGrant} LockGrant
95
+ * @typedef {import("./types.js").DocumentRestoreSummary} DocumentRestoreSummary
96
+ * @typedef {import("./types.js").RestoreCounts} RestoreCounts
97
+ * @typedef {import("./types.js").OperationGroup} OperationGroup
98
+ * @typedef {import("./types.js").QueryResult} QueryResult
99
+ */
100
+
101
+ /**
102
+ * @template [T=any]
103
+ * @typedef {import("./types.js").Page<T>} Page
104
+ */
105
+
106
+ // Server-level facts. Read once, and again only when they may have
107
+ // changed: after this client's connection to the server was lost and came
108
+ // back (a restart, the only way the limits change, always drops it), or
109
+ // after the server refused a private data write as too large. A caller
110
+ // asking "will this file be accepted" does not pay a round trip to find
111
+ // out. The facts live on the client (`_serverFacts`), so a batch opened
112
+ // on it reads the same.
113
+ /** @param {import("./client.js").PlaidClient} client */
114
+ export function serverApi(client) {
115
+ return {
116
+ /**
117
+ * This server's version and the limits it enforces, e.g.
118
+ * `{ limits: { mediaFileBytes, jsonBodyBytes, batchOperations, … } }`.
119
+ * Sizes are in bytes. Unauthenticated.
120
+ * @returns {Promise<{limits: ServerLimits}>}
121
+ */
122
+ info: () => {
123
+ const facts = client._serverFacts;
124
+ if (!facts.info) {
125
+ const asked = client._request("GET", "/api/v1/info").catch((err) => {
126
+ // A failure must not be kept: a client that starts before the
127
+ // server is up would never see the limits at all.
128
+ if (facts.info === asked) facts.info = null;
129
+ throw err;
130
+ });
131
+ facts.info = asked;
132
+ }
133
+ return facts.info;
134
+ },
135
+ /**
136
+ * Read `GET /info` again and resolve to it. Until it answers, a reader
137
+ * waits for it. The facts known before are kept when the server does
138
+ * not answer, and the error is thrown.
139
+ * @returns {Promise<{limits: ServerLimits}>}
140
+ */
141
+ refresh: () => {
142
+ const facts = client._serverFacts;
143
+ const before = facts.info;
144
+ facts.stale = false;
145
+ const fresh = client._request("GET", "/api/v1/info");
146
+ const held = fresh.catch((err) => {
147
+ if (facts.info === held) facts.info = before;
148
+ if (before) return before;
149
+ throw err;
150
+ });
151
+ held.catch(() => {});
152
+ facts.info = held;
153
+ return fresh;
154
+ },
155
+ /**
156
+ * Just the limits, which is what a caller almost always wants.
157
+ * @returns {Promise<ServerLimits>}
158
+ */
159
+ limits: async () => {
160
+ // Through `self`: tsc types a bundle that names itself as
161
+ // `this.server` in its own methods `any` in the declarations.
162
+ const self = client;
163
+ return (await self.server.info()).limits;
164
+ },
165
+ /**
166
+ * Liveness, version and database size. Unauthenticated, and served
167
+ * outside the REST router at /health, so it answers even while the API
168
+ * is refusing requests. Never cached — the point is that it is current.
169
+ * @returns {Promise<ServerHealth>}
170
+ */
171
+ health: () => PlaidClient.health(client.baseUrl, { timeout: client.timeout }),
172
+ };
173
+ }
174
+
175
+ /** @param {import("./client.js").PlaidClient} client */
176
+ export function adminApi(client) {
177
+ return {
178
+ /**
179
+ * Everything about the server in one read: version and JVM uptime,
180
+ * database size and per-table row counts, media directory usage, backup
181
+ * configuration and the backups on disk, and the settings an operator
182
+ * gets asked to confirm. Carries no secrets. Runs a count per table and
183
+ * walks the media directory, so open it, do not poll it. Admin only.
184
+ * @returns {Promise<ServerReport>}
185
+ */
186
+ server: () => client._request("GET", "/api/v1/admin/server"),
187
+ /**
188
+ * Take a database backup right now, outside the nightly schedule.
189
+ * Resolves to the backup block with `ok` reporting whether the snapshot
190
+ * succeeded. Uses VACUUM INTO, which only reads, so it is safe while
191
+ * people are working. Admin only.
192
+ *
193
+ * The server answers once the backup is written, which takes minutes on
194
+ * a large database, so this waits up to `BACKUP_TIMEOUT_MS` (30 minutes)
195
+ * rather than the client's usual timeout. A proxy in front of the server
196
+ * may still give up first (a 504): the backup goes on, and its file
197
+ * shows in `server().backup.backups` once it is written.
198
+ * @returns {Promise<BackupStatus>}
199
+ *
200
+ * outOfBand, as every admin action on the server itself is: none of them
201
+ * writes project data (see the note at the top of http.js).
202
+ */
203
+ backup: () =>
204
+ client._request("POST", "/api/v1/admin/backup", {
205
+ outOfBand: true,
206
+ timeout: BACKUP_TIMEOUT_MS,
207
+ }),
208
+ /**
209
+ * Documents currently held by an editing lock, with who holds each and
210
+ * when it expires on its own. Admin only.
211
+ * @returns {Promise<{entries: Array<{documentId: string, userId: string, expiresAt: number}>}>}
212
+ */
213
+ locks: () => client._request("GET", "/api/v1/admin/locks"),
214
+ /**
215
+ * Drop the lock on a document whoever holds it. Idempotent. For a client
216
+ * that went away without releasing one. Admin only.
217
+ * @param {string} documentId - The document ID
218
+ * @returns {Promise<{result: string}>}
219
+ */
220
+ releaseLock: (documentId) =>
221
+ client._request("DELETE", `/api/v1/admin/locks/${documentId}`, {
222
+ outOfBand: true,
223
+ }),
224
+ /**
225
+ * Live login and invite rate-limit buckets: the address, the account
226
+ * where there is one, failures inside the window, the limit, and whether
227
+ * it is currently blocking. Admin only.
228
+ * @returns {Promise<RateLimitSnapshot>}
229
+ */
230
+ rateLimits: () => client._request("GET", "/api/v1/admin/rate-limits"),
231
+ /**
232
+ * Forget recorded rate-limit failures. With `ip`, clears that address,
233
+ * narrowed to one account with `userId`. With neither, clears every
234
+ * bucket. Only ever unblocks. Admin only.
235
+ * @param {object} [opts]
236
+ * @param {string} [opts.ip] - Clear only this address
237
+ * @param {string} [opts.userId] - Narrow to one account on that address
238
+ * @returns {Promise<{result: string}>}
239
+ */
240
+ clearRateLimits: ({ ip, userId } = {}) =>
241
+ client._request("DELETE", "/api/v1/admin/rate-limits", {
242
+ outOfBand: true,
243
+ queryParams: { ip, "user-id": userId },
244
+ }),
245
+ /**
246
+ * What the server has logged, structured and filtered, from an in-memory
247
+ * buffer kept whether or not a log file is configured. `requests` is one
248
+ * entry per HTTP request (method, path, status, duration, and who made
249
+ * it), `events` is everything else, with a stack trace where there was
250
+ * one. They are buffered separately so a burst of requests cannot evict
251
+ * an error. Both come newest first, `matched` counts what passed the
252
+ * filters, and request `stats` describe the filtered set. Covers Plaid's
253
+ * own log stream since the last restart: for library messages and older
254
+ * history, see `logFile`. Admin only.
255
+ * @param {object} [opts]
256
+ * @param {number} [opts.limit] - Max entries per kind (default 200, max 2000)
257
+ * @param {string} [opts.q] - Substring of any field, case-insensitive
258
+ * @param {string} [opts.level] - Minimum level for events, e.g. "warn"
259
+ * @param {string} [opts.status] - "2xx".."5xx", an exact code, or "failures"
260
+ * @param {string} [opts.user] - Only requests made by this account
261
+ * @param {string} [opts.method] - Only requests with this HTTP method
262
+ * @returns {Promise<LiveLog>}
263
+ */
264
+ logs: ({ limit, q, level, status, user, method } = {}) =>
265
+ client._request("GET", "/api/v1/admin/logs", {
266
+ queryParams: { limit, q, level, status, user, method },
267
+ }),
268
+ /**
269
+ * The tail of the configured log file, as text lines. The only place
270
+ * third-party library messages and anything from before the last restart
271
+ * can be read. Resolves with an `error` string instead of lines when no
272
+ * log file is configured or it does not exist yet. Admin only.
273
+ * @param {object} [opts]
274
+ * @param {number} [opts.lines] - How many lines (default 200, max 2000)
275
+ * @returns {Promise<{file: (string|null), lines: string[], error?: string}>}
276
+ */
277
+ logFile: ({ lines } = {}) =>
278
+ client._request("GET", "/api/v1/admin/logs/file", {
279
+ queryParams: { lines },
280
+ }),
281
+ /**
282
+ * Private user-data entries across every account, each carrying its
283
+ * `userId`. `/users/:userId/data` has always been owner-or-admin; this
284
+ * is the same reach across accounts at once. Transparently follows
285
+ * pagination cursors and returns the full flat array. Admin only.
286
+ *
287
+ * Narrow with `prefix` (the literal head of a key) and/or `pattern`, a
288
+ * GLOB over the whole key (`*` any run, `?` one character) — the way to
289
+ * ask for a key convention identified by a segment in the middle, e.g.
290
+ * `igt:assistant:*:meta:*`. Values come only with `includeValues`, and
291
+ * are recased like any other body (see `userData.put`).
292
+ * @param {object} [opts]
293
+ * @param {string} [opts.prefix] - Only keys starting with this
294
+ * @param {string} [opts.pattern] - Only keys matching this GLOB
295
+ * @param {boolean} [opts.includeValues] - Also return each entry's value
296
+ * @returns {Promise<AdminUserDataEntry[]>}
297
+ */
298
+ userData: ({ prefix, pattern, includeValues } = {}) =>
299
+ listAll(client, "/api/v1/admin/user-data", {
300
+ query: {
301
+ prefix,
302
+ pattern,
303
+ "include-values": includeValues,
304
+ },
305
+ }),
306
+ /**
307
+ * One page of private user-data entries across accounts, ordered by
308
+ * (user, key). Admin only.
309
+ * @param {object} [opts]
310
+ * @param {string} [opts.prefix] - Only keys starting with this
311
+ * @param {string} [opts.pattern] - Only keys matching this GLOB
312
+ * @param {boolean} [opts.includeValues] - Also return each entry's value
313
+ * @param {number} [opts.limit] - Page size (1..1000; server default 100)
314
+ * @param {string} [opts.cursor] - Opaque cursor from a previous page
315
+ * @returns {Promise<Page<AdminUserDataEntry>>}
316
+ */
317
+ userDataPage: ({ prefix, pattern, includeValues, limit, cursor } = {}) =>
318
+ listPage(client, "/api/v1/admin/user-data", {
319
+ limit,
320
+ cursor,
321
+ query: {
322
+ prefix,
323
+ pattern,
324
+ "include-values": includeValues,
325
+ },
326
+ }),
327
+ };
328
+ }
329
+
330
+ /** @param {import("./client.js").PlaidClient} client */
331
+ export function auditApi(client) {
332
+ return {
333
+ /**
334
+ * The audit log across every project, oldest first. Same fold, window
335
+ * and op-type filter as the per-project read, with the entity scope
336
+ * dropped. Admin only. Transparently follows pagination cursors and
337
+ * returns the full flat array.
338
+ * @param {object} [opts]
339
+ * @param {string} [opts.startTime] - Only operations at or after this instant
340
+ * @param {string} [opts.endTime] - Only operations at or before this instant
341
+ * @param {string[]|string} [opts.opTypes] - Only these op types (e.g.
342
+ * `['span-layer/create']`). An entry appears when one of its operations
343
+ * matches, carrying only the ones that did.
344
+ * @param {string[]|string} [opts.kinds] - Only the entries of operations
345
+ * of these kinds (e.g. `['review']`), each whole
346
+ * @returns {Promise<AuditEntry[]>}
347
+ */
348
+ list: ({ startTime, endTime, opTypes, kinds } = {}) =>
349
+ listAll(client, "/api/v1/audit", {
350
+ query: auditQuery({ startTime, endTime, opTypes, kinds }),
351
+ }),
352
+ /**
353
+ * Fetch a single page of the instance-wide audit log. Admin only.
354
+ * @param {object} [opts]
355
+ * @param {string} [opts.startTime] - Only entries at or after this instant
356
+ * @param {string} [opts.endTime] - Only entries at or before this instant
357
+ * @param {string[]|string} [opts.opTypes] - Only these op types
358
+ * @param {OperationKind[]|string} [opts.kinds] - Only the entries of operations of these kinds, each whole
359
+ * @param {"asc"|"desc"} [opts.order] - "desc" pages newest-first, which
360
+ * is what a feed wants. A cursor belongs to the direction that made it.
361
+ * @param {number} [opts.limit] - Page size (1..1000; server default 100)
362
+ * @param {string} [opts.cursor] - Opaque cursor from a previous page
363
+ * @param {number} [opts.opsLimit] - Keep only each entry's oldest N
364
+ * operations in `ops` (1..1000). `opCount` on every entry says how
365
+ * many it has
366
+ * @param {string} [opts.entryId] - Read only this entry, not a page
367
+ * @returns {Promise<Page<AuditEntry>>}
368
+ */
369
+ listPage: ({ startTime, endTime, opTypes, kinds, order, limit, cursor, opsLimit, entryId } = {}) =>
370
+ listPage(client, "/api/v1/audit", {
371
+ limit,
372
+ cursor,
373
+ query: auditQuery({ startTime, endTime, opTypes, kinds, order, opsLimit, entryId }),
374
+ }),
375
+ /**
376
+ * Async-iterate the instance-wide audit log page by page. Admin only.
377
+ * @param {object} [opts]
378
+ * @param {string} [opts.startTime] - Only entries at or after this instant
379
+ * @param {string} [opts.endTime] - Only entries at or before this instant
380
+ * @param {string[]|string} [opts.opTypes] - Only these op types
381
+ * @param {OperationKind[]|string} [opts.kinds] - Only the entries of operations of these kinds, each whole
382
+ * @param {number} [opts.pageSize] - Per-request page size
383
+ * @returns {AsyncGenerator<AuditEntry[]>}
384
+ */
385
+ iterPages: ({ startTime, endTime, opTypes, kinds, pageSize } = {}) =>
386
+ iterPages(client, "/api/v1/audit", {
387
+ pageSize,
388
+ query: auditQuery({ startTime, endTime, opTypes, kinds }),
389
+ }),
390
+ /**
391
+ * Per-user activity counts. `changes` is the number of logical actions,
392
+ * folded the way the audit feed folds them, so one "Confirm word
393
+ * analysis" counts once however many rows it wrote; `operations` is the
394
+ * unfolded row count. Only users who did something appear — subtract
395
+ * from the roster you already hold to find the ones who did not.
396
+ *
397
+ * With `projectId`, scoped to that project and open to its maintainers.
398
+ * Without one, instance-wide and admin only.
399
+ * @param {object} [opts]
400
+ * @param {string} [opts.projectId] - Scope to one project
401
+ * @param {string} [opts.startTime] - Only count at or after this instant
402
+ * @param {string} [opts.endTime] - Only count at or before this instant
403
+ * @param {boolean} [opts.daily] - Also return `byDay`, a list of
404
+ * `{date, changes}` per user, oldest first, at the cost of a second
405
+ * grouped scan
406
+ * @returns {Promise<ActivityTallyRow[]>}
407
+ */
408
+ tally: async ({ projectId, startTime, endTime, daily } = {}) => {
409
+ const path = projectId
410
+ ? `/api/v1/projects/${projectId}/audit/tally`
411
+ : "/api/v1/audit/tally";
412
+ const result = await client._request("GET", path, {
413
+ queryParams: {
414
+ "start-time": startTime,
415
+ "end-time": endTime,
416
+ daily,
417
+ },
418
+ });
419
+ return result.entries;
420
+ },
421
+ };
422
+ }