@dudousxd/nestjs-catalog 0.5.0 → 0.6.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/README.md +163 -0
- package/dist/access.controller.js +25 -0
- package/dist/catalog.controller.d.ts +46 -0
- package/dist/catalog.controller.js +199 -23
- package/dist/catalog.environment.d.ts +65 -12
- package/dist/catalog.environment.js +14 -1
- package/dist/catalog.events.d.ts +67 -3
- package/dist/catalog.events.js +15 -2
- package/dist/catalog.principal.d.ts +49 -1
- package/dist/catalog.principal.js +92 -0
- package/dist/catalog.service.d.ts +53 -10
- package/dist/catalog.service.js +180 -15
- package/dist/catalog.types.d.ts +7 -1
- package/dist/catalog.workspace.d.ts +68 -8
- package/dist/catalog.workspace.js +23 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +4 -2
- package/package.json +1 -1
package/dist/catalog.events.d.ts
CHANGED
|
@@ -14,15 +14,16 @@
|
|
|
14
14
|
*/
|
|
15
15
|
export declare const CATALOG_LIB = "catalog";
|
|
16
16
|
/** Every event name this package emits. Exported so a watcher can claim them. */
|
|
17
|
-
export declare const CATALOG_EVENTS: readonly ["schema.changed", "snapshot.written", "snapshot.committed", "snapshot.dropped", "type.curated", "connector.run.started", "connector.run.finished", "transform.changed", "workflow.changed"];
|
|
17
|
+
export declare const CATALOG_EVENTS: readonly ["schema.changed", "snapshot.written", "snapshot.committed", "snapshot.dropped", "type.curated", "connector.run.started", "connector.run.finished", "transform.changed", "workflow.changed", "query.shared", "dashboard.shared"];
|
|
18
18
|
export type CatalogEvent = (typeof CATALOG_EVENTS)[number];
|
|
19
19
|
/**
|
|
20
20
|
* Where each event sits in the life of one load.
|
|
21
21
|
*
|
|
22
22
|
* This exists because a timestamp is not enough to order a trace. A fast load
|
|
23
23
|
* emits its whole story inside a single tick of whatever clock the recorder
|
|
24
|
-
* writes — the bundled MySQL store keeps
|
|
25
|
-
*
|
|
24
|
+
* writes — the bundled MySQL store keeps milliseconds, and a commit of a few
|
|
25
|
+
* hundred rows lands well inside one — so sorting by time alone leaves the
|
|
26
|
+
* events in insertion order, and insertion order is arbitrary.
|
|
26
27
|
* The observed result is a trace that reads `finished → written → committed →
|
|
27
28
|
* started`: causally impossible, rendered with total confidence. Sorting by
|
|
28
29
|
* time *and then* by this rank puts a same-tick story back in the only order it
|
|
@@ -142,6 +143,69 @@ export interface CatalogEventPayloads {
|
|
|
142
143
|
nodeCount: number;
|
|
143
144
|
changedBy: string;
|
|
144
145
|
};
|
|
146
|
+
/**
|
|
147
|
+
* A saved query started or stopped being fetchable through the embed API.
|
|
148
|
+
*
|
|
149
|
+
* The single act that hands an outside application data from this catalog, and
|
|
150
|
+
* for a long time the only one that left no trace: `SavedQuery.shared` is the
|
|
151
|
+
* whole embed boundary, it is set by a person, and nothing recorded that they
|
|
152
|
+
* had set it.
|
|
153
|
+
*
|
|
154
|
+
* One event for both directions rather than a `query.unshared` beside it,
|
|
155
|
+
* following `connector.run.finished`: anybody watching for shares has to watch
|
|
156
|
+
* for revocations too — a trail that records only the grants cannot answer
|
|
157
|
+
* "was this still shared last Tuesday", which is the question an incident
|
|
158
|
+
* actually asks. The direction is in {@link shared}.
|
|
159
|
+
*
|
|
160
|
+
* Emitted on the transition only. A save that leaves the flag where it was is
|
|
161
|
+
* not a sharing decision, and a trail that logged every keystroke on a query
|
|
162
|
+
* would be a trail nobody reads.
|
|
163
|
+
*
|
|
164
|
+
* **Deleting a shared query is one of those transitions**, and it emits here
|
|
165
|
+
* rather than under a name of its own. The question this event answers is
|
|
166
|
+
* "when did this stop being reachable from outside", and anybody asking it
|
|
167
|
+
* filters on `query.shared` and reads the last entry. A separate
|
|
168
|
+
* `query.deleted` would leave that filter reporting `shared: true` forever
|
|
169
|
+
* for something nobody can fetch — the revocation would be in a channel the
|
|
170
|
+
* asker did not subscribe to, which is the same absence this event exists to
|
|
171
|
+
* remove. {@link deleted} says which kind of ending it was.
|
|
172
|
+
*/
|
|
173
|
+
'query.shared': {
|
|
174
|
+
savedQueryId: string;
|
|
175
|
+
/** As it read at the moment of the change, so the trail is legible alone. */
|
|
176
|
+
name: string;
|
|
177
|
+
/** True when access was granted, false when it was taken back. */
|
|
178
|
+
shared: boolean;
|
|
179
|
+
/**
|
|
180
|
+
* Who decided. The host-resolved principal where there is one, and named
|
|
181
|
+
* `principalId` because that is the field a recorder lifts into the audit
|
|
182
|
+
* table's indexed column — spell it anything else and the entry lands
|
|
183
|
+
* attributed to nobody.
|
|
184
|
+
*/
|
|
185
|
+
principalId: string;
|
|
186
|
+
/**
|
|
187
|
+
* Set only when the revocation was a deletion. Absent means the thing still
|
|
188
|
+
* exists and was merely un-shared.
|
|
189
|
+
*
|
|
190
|
+
* Typed as `true` rather than `boolean` so the flag cannot be written the
|
|
191
|
+
* other way round: "not a deletion" is the absence of this key, matching
|
|
192
|
+
* how the rest of these payloads treat a fact that did not happen.
|
|
193
|
+
*
|
|
194
|
+
* Worth carrying rather than leaving to be looked up, because after a
|
|
195
|
+
* deletion there is nothing left to look up. An entry that said only
|
|
196
|
+
* `shared: false` would send its reader to a row that is gone, and the
|
|
197
|
+
* empty result reads as a broken trail rather than as the answer.
|
|
198
|
+
*/
|
|
199
|
+
deleted?: true;
|
|
200
|
+
};
|
|
201
|
+
/** The same decision, about a whole board. See {@link CatalogEventPayloads['query.shared']}. */
|
|
202
|
+
'dashboard.shared': {
|
|
203
|
+
dashboardId: string;
|
|
204
|
+
name: string;
|
|
205
|
+
shared: boolean;
|
|
206
|
+
principalId: string;
|
|
207
|
+
deleted?: true;
|
|
208
|
+
};
|
|
145
209
|
}
|
|
146
210
|
/**
|
|
147
211
|
* Teach the diagnostics channel what this library puts on it.
|
package/dist/catalog.events.js
CHANGED
|
@@ -31,14 +31,17 @@ exports.CATALOG_EVENTS = [
|
|
|
31
31
|
'connector.run.finished',
|
|
32
32
|
'transform.changed',
|
|
33
33
|
'workflow.changed',
|
|
34
|
+
'query.shared',
|
|
35
|
+
'dashboard.shared',
|
|
34
36
|
];
|
|
35
37
|
/**
|
|
36
38
|
* Where each event sits in the life of one load.
|
|
37
39
|
*
|
|
38
40
|
* This exists because a timestamp is not enough to order a trace. A fast load
|
|
39
41
|
* emits its whole story inside a single tick of whatever clock the recorder
|
|
40
|
-
* writes — the bundled MySQL store keeps
|
|
41
|
-
*
|
|
42
|
+
* writes — the bundled MySQL store keeps milliseconds, and a commit of a few
|
|
43
|
+
* hundred rows lands well inside one — so sorting by time alone leaves the
|
|
44
|
+
* events in insertion order, and insertion order is arbitrary.
|
|
42
45
|
* The observed result is a trace that reads `finished → written → committed →
|
|
43
46
|
* started`: causally impossible, rendered with total confidence. Sorting by
|
|
44
47
|
* time *and then* by this rank puts a same-tick story back in the only order it
|
|
@@ -64,6 +67,16 @@ exports.CATALOG_EVENT_PHASE = {
|
|
|
64
67
|
// be complete is exactly the mechanism that will fail the build the day a new
|
|
65
68
|
// event is added and nobody thinks about where it belongs.
|
|
66
69
|
'type.curated': 2,
|
|
70
|
+
// Sharing carries no snapshot id either, for the same reason curation does
|
|
71
|
+
// not: it is a standalone act on a saved query or a board, not a step of any
|
|
72
|
+
// load. So these ranks are never consulted, and they are written out for the
|
|
73
|
+
// reason the note above gives — the `Record` has to be complete, and that is
|
|
74
|
+
// what makes the build fail the day an event is added and nobody thinks about
|
|
75
|
+
// where it belongs. Ranked beside curation because they are the same kind of
|
|
76
|
+
// thing: a person deciding something about the catalog rather than data moving
|
|
77
|
+
// through it.
|
|
78
|
+
'query.shared': 2,
|
|
79
|
+
'dashboard.shared': 2,
|
|
67
80
|
'schema.changed': 3,
|
|
68
81
|
'snapshot.written': 4,
|
|
69
82
|
'snapshot.committed': 5,
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
* to survive into the audit trail, because "the console renamed this column" is
|
|
17
17
|
* not an answer anybody accepts.
|
|
18
18
|
*/
|
|
19
|
+
import type { CatalogObjectPage } from './catalog.types';
|
|
19
20
|
export type CatalogScope =
|
|
20
21
|
/** Read object metadata and rows. */
|
|
21
22
|
'catalog:read'
|
|
@@ -118,7 +119,9 @@ export interface CatalogPrincipal {
|
|
|
118
119
|
readTypes?: string[];
|
|
119
120
|
/**
|
|
120
121
|
* Classifications this principal may see. A column marked with anything
|
|
121
|
-
* outside this list is dropped from its reads
|
|
122
|
+
* outside this list is to be dropped from its reads — *by whoever serves
|
|
123
|
+
* them*, which is not this library. See the note above {@link mayWrite} on
|
|
124
|
+
* who enforces, and {@link readableObjectPage} for the drop itself.
|
|
122
125
|
*
|
|
123
126
|
* Undefined means "no classified columns", not "all of them" — the safe
|
|
124
127
|
* default for a caller nobody has thought about yet.
|
|
@@ -198,12 +201,57 @@ export declare function expandScopes(scopes: CatalogScope[]): CatalogScope[];
|
|
|
198
201
|
* something that happens because an administrator logged in.
|
|
199
202
|
*/
|
|
200
203
|
export declare function delegatePrincipal(application: CatalogPrincipal, actor: CatalogActor, grants: CatalogGrants): CatalogPrincipal;
|
|
204
|
+
/** Enforced by the library, because the publish API is handed a principal. */
|
|
201
205
|
export declare function mayWrite(principal: CatalogPrincipal, typeName: string): boolean;
|
|
206
|
+
/**
|
|
207
|
+
* Whether this principal may read a type at all.
|
|
208
|
+
*
|
|
209
|
+
* A predicate for a host to apply, not something the catalog's own read routes
|
|
210
|
+
* consult — see the note above. `readableObjectPage` is the ready-made
|
|
211
|
+
* application of it.
|
|
212
|
+
*/
|
|
202
213
|
export declare function mayRead(principal: CatalogPrincipal, typeName: string): boolean;
|
|
203
214
|
/**
|
|
204
215
|
* Whether a column is visible to this principal.
|
|
205
216
|
*
|
|
206
217
|
* Unclassified columns are visible to everyone; a classified one requires the
|
|
207
218
|
* principal to name that classification. Absence is denial.
|
|
219
|
+
*
|
|
220
|
+
* The same caveat as {@link mayRead}: answering the question is all this does.
|
|
221
|
+
* No read path in this library asks it.
|
|
208
222
|
*/
|
|
209
223
|
export declare function maySeeClassification(principal: CatalogPrincipal, classification: string | undefined): boolean;
|
|
224
|
+
/**
|
|
225
|
+
* A page of objects as this principal is allowed to see it: the page, minus
|
|
226
|
+
* every column carrying a classification they do not hold — and `null` when
|
|
227
|
+
* they may not read the type at all.
|
|
228
|
+
*
|
|
229
|
+
* This exists so that "who applies `mayRead` and `maySeeClassification`?" has an
|
|
230
|
+
* answer a host can call rather than a rule a host has to re-derive. Wrap the
|
|
231
|
+
* service:
|
|
232
|
+
*
|
|
233
|
+
* ```ts
|
|
234
|
+
* const page = await this.catalog.readObjects(name, query);
|
|
235
|
+
* const visible = readableObjectPage(principal, page);
|
|
236
|
+
* if (!visible) throw new ForbiddenException(`${principal.id} may not read ${name}.`);
|
|
237
|
+
* return visible;
|
|
238
|
+
* ```
|
|
239
|
+
*
|
|
240
|
+
* `null` rather than a thrown exception because the status code is the host's
|
|
241
|
+
* decision: a 403 confirms to the caller that the type exists, and a deployment
|
|
242
|
+
* that considers the type list itself sensitive would rather answer 404. `null`
|
|
243
|
+
* rather than an empty page because a page with no columns is what a type whose
|
|
244
|
+
* every column is hidden legitimately returns, and a denial must not be
|
|
245
|
+
* mistakable for one.
|
|
246
|
+
*
|
|
247
|
+
* Hidden values are **deleted** from each row rather than blanked. A key present
|
|
248
|
+
* with `null` asserts that the column exists and happens to be empty, which for
|
|
249
|
+
* a classified column is itself a disclosure.
|
|
250
|
+
*
|
|
251
|
+
* What it does not do: rows can carry keys no column declares, because
|
|
252
|
+
* `readObjects` always fetches the primary key so a UI has a row identity even
|
|
253
|
+
* when that key is hidden. Those survive. This removes what a classification
|
|
254
|
+
* hides; it is not a whitelist, and a classified column that is also part of a
|
|
255
|
+
* hidden primary key is outside what a page alone can express.
|
|
256
|
+
*/
|
|
257
|
+
export declare function readableObjectPage(principal: CatalogPrincipal, page: CatalogObjectPage): CatalogObjectPage | null;
|
|
@@ -27,6 +27,7 @@ exports.delegatePrincipal = delegatePrincipal;
|
|
|
27
27
|
exports.mayWrite = mayWrite;
|
|
28
28
|
exports.mayRead = mayRead;
|
|
29
29
|
exports.maySeeClassification = maySeeClassification;
|
|
30
|
+
exports.readableObjectPage = readableObjectPage;
|
|
30
31
|
/** Every scope, in one place, so `catalog:admin` can be expanded exactly. */
|
|
31
32
|
const ALL_SCOPES = [
|
|
32
33
|
'catalog:read',
|
|
@@ -222,9 +223,45 @@ function matches(list, typeName) {
|
|
|
222
223
|
return false;
|
|
223
224
|
return list.includes('*') || list.includes(typeName);
|
|
224
225
|
}
|
|
226
|
+
// -----------------------------------------------------------------------------
|
|
227
|
+
// The grant predicates, and who is expected to call them.
|
|
228
|
+
//
|
|
229
|
+
// **This library declares and evaluates; the host enforces.** The same split
|
|
230
|
+
// `catalog.route-auth.ts` states for scopes holds for grants: nothing in this
|
|
231
|
+
// package ever turns a request into a principal. `CatalogPrincipalResolver` is
|
|
232
|
+
// an interface with one bundled development implementation, no guard ships here,
|
|
233
|
+
// and the `request.principal` that `publish.controller.ts` reads is put there by
|
|
234
|
+
// a guard the host wrote — the error it throws even names a class this repo does
|
|
235
|
+
// not contain.
|
|
236
|
+
//
|
|
237
|
+
// The asymmetry between write and read is real and is not the bug it looks like.
|
|
238
|
+
// `mayWrite` is called inside the library, in `publish.service.ts`, for exactly
|
|
239
|
+
// one reason: the publish API takes a `CatalogPrincipal` as an argument, so it
|
|
240
|
+
// has one in hand. The read path does not. `CatalogService.readObjects` has no
|
|
241
|
+
// principal parameter, `GET /catalog/objects/:name` has none to give it, and so
|
|
242
|
+
// `mayRead` and `maySeeClassification` are called nowhere in this repository at
|
|
243
|
+
// runtime. That is the design: a library that filtered reads would need a
|
|
244
|
+
// principal on every read, which means shipping a guard, which means having an
|
|
245
|
+
// opinion about the host's identity provider.
|
|
246
|
+
//
|
|
247
|
+
// What was wrong was not the mechanism but the prose around it. A host reading
|
|
248
|
+
// "classifications this principal may see" is entitled to assume something drops
|
|
249
|
+
// the columns, and nothing does. So, stated once, here: **the read path applies
|
|
250
|
+
// no grants and no classification filtering, and a host that wants either calls
|
|
251
|
+
// `readableObjectPage` itself.** Anyone about to "wire this up" should change
|
|
252
|
+
// that sentence rather than quietly making it false.
|
|
253
|
+
// -----------------------------------------------------------------------------
|
|
254
|
+
/** Enforced by the library, because the publish API is handed a principal. */
|
|
225
255
|
function mayWrite(principal, typeName) {
|
|
226
256
|
return hasScope(principal, 'catalog:write') && matches(principal.writeTypes, typeName);
|
|
227
257
|
}
|
|
258
|
+
/**
|
|
259
|
+
* Whether this principal may read a type at all.
|
|
260
|
+
*
|
|
261
|
+
* A predicate for a host to apply, not something the catalog's own read routes
|
|
262
|
+
* consult — see the note above. `readableObjectPage` is the ready-made
|
|
263
|
+
* application of it.
|
|
264
|
+
*/
|
|
228
265
|
function mayRead(principal, typeName) {
|
|
229
266
|
if (!hasScope(principal, 'catalog:read'))
|
|
230
267
|
return false;
|
|
@@ -237,9 +274,64 @@ function mayRead(principal, typeName) {
|
|
|
237
274
|
*
|
|
238
275
|
* Unclassified columns are visible to everyone; a classified one requires the
|
|
239
276
|
* principal to name that classification. Absence is denial.
|
|
277
|
+
*
|
|
278
|
+
* The same caveat as {@link mayRead}: answering the question is all this does.
|
|
279
|
+
* No read path in this library asks it.
|
|
240
280
|
*/
|
|
241
281
|
function maySeeClassification(principal, classification) {
|
|
242
282
|
if (!classification)
|
|
243
283
|
return true;
|
|
244
284
|
return principal.classifications?.includes(classification) ?? false;
|
|
245
285
|
}
|
|
286
|
+
/**
|
|
287
|
+
* A page of objects as this principal is allowed to see it: the page, minus
|
|
288
|
+
* every column carrying a classification they do not hold — and `null` when
|
|
289
|
+
* they may not read the type at all.
|
|
290
|
+
*
|
|
291
|
+
* This exists so that "who applies `mayRead` and `maySeeClassification`?" has an
|
|
292
|
+
* answer a host can call rather than a rule a host has to re-derive. Wrap the
|
|
293
|
+
* service:
|
|
294
|
+
*
|
|
295
|
+
* ```ts
|
|
296
|
+
* const page = await this.catalog.readObjects(name, query);
|
|
297
|
+
* const visible = readableObjectPage(principal, page);
|
|
298
|
+
* if (!visible) throw new ForbiddenException(`${principal.id} may not read ${name}.`);
|
|
299
|
+
* return visible;
|
|
300
|
+
* ```
|
|
301
|
+
*
|
|
302
|
+
* `null` rather than a thrown exception because the status code is the host's
|
|
303
|
+
* decision: a 403 confirms to the caller that the type exists, and a deployment
|
|
304
|
+
* that considers the type list itself sensitive would rather answer 404. `null`
|
|
305
|
+
* rather than an empty page because a page with no columns is what a type whose
|
|
306
|
+
* every column is hidden legitimately returns, and a denial must not be
|
|
307
|
+
* mistakable for one.
|
|
308
|
+
*
|
|
309
|
+
* Hidden values are **deleted** from each row rather than blanked. A key present
|
|
310
|
+
* with `null` asserts that the column exists and happens to be empty, which for
|
|
311
|
+
* a classified column is itself a disclosure.
|
|
312
|
+
*
|
|
313
|
+
* What it does not do: rows can carry keys no column declares, because
|
|
314
|
+
* `readObjects` always fetches the primary key so a UI has a row identity even
|
|
315
|
+
* when that key is hidden. Those survive. This removes what a classification
|
|
316
|
+
* hides; it is not a whitelist, and a classified column that is also part of a
|
|
317
|
+
* hidden primary key is outside what a page alone can express.
|
|
318
|
+
*/
|
|
319
|
+
function readableObjectPage(principal, page) {
|
|
320
|
+
if (!mayRead(principal, page.type))
|
|
321
|
+
return null;
|
|
322
|
+
const hidden = page.columns
|
|
323
|
+
.filter((column) => !maySeeClassification(principal, column.classification))
|
|
324
|
+
.map((column) => column.name);
|
|
325
|
+
if (hidden.length === 0)
|
|
326
|
+
return page;
|
|
327
|
+
return {
|
|
328
|
+
...page,
|
|
329
|
+
columns: page.columns.filter((column) => !hidden.includes(column.name)),
|
|
330
|
+
rows: page.rows.map((row) => {
|
|
331
|
+
const visible = { ...row };
|
|
332
|
+
for (const name of hidden)
|
|
333
|
+
delete visible[name];
|
|
334
|
+
return visible;
|
|
335
|
+
}),
|
|
336
|
+
};
|
|
337
|
+
}
|
|
@@ -3,7 +3,7 @@ import { type CatalogQueryRelation, type CatalogQueryResult } from './catalog.qu
|
|
|
3
3
|
import { CatalogRegistry } from './catalog.registry.base';
|
|
4
4
|
import { type CatalogReadStore, type SnapshotRef } from './catalog.store';
|
|
5
5
|
import type { CatalogGraph, CatalogObjectPage, CatalogObjectQuery, CatalogObjectTypeDef, CatalogOverlay, CatalogSnapshot } from './catalog.types';
|
|
6
|
-
import { type AuditQuery, type CatalogAuditEvent, type CatalogWorkspaceStore, type Dashboard, type DashboardCard, type EmbeddedChart, type EmbeddedDashboard, type SaveQueryInput, type SavedQuery } from './catalog.workspace';
|
|
6
|
+
import { type AuditQuery, type CatalogAuditEvent, type CatalogWorkspaceStore, type Dashboard, type DashboardCard, type EmbeddedChart, type EmbeddedChartPlacement, type EmbeddedDashboard, type SaveQueryInput, type SavedQuery } from './catalog.workspace';
|
|
7
7
|
/**
|
|
8
8
|
* Reads objects of any catalogued type through one endpoint.
|
|
9
9
|
*
|
|
@@ -32,6 +32,17 @@ export declare class CatalogService {
|
|
|
32
32
|
resetOverlay(): Promise<void>;
|
|
33
33
|
/** Columns a generic UI may render: visible, and not a blob. */
|
|
34
34
|
visibleColumns(type: CatalogObjectTypeDef): import("./catalog.types").CatalogPropertyDef[];
|
|
35
|
+
/**
|
|
36
|
+
* Rows of one type, paged.
|
|
37
|
+
*
|
|
38
|
+
* **No principal, and so no access control.** This applies the guardrails that
|
|
39
|
+
* hold for every caller — the type exists, the page is bounded, the sort names
|
|
40
|
+
* a real column — and none that depend on who is asking: a classified column
|
|
41
|
+
* comes back to whoever the host's guard let through the door. That is the
|
|
42
|
+
* library's declare-and-enforce split, written out at length above `mayWrite`
|
|
43
|
+
* in `catalog.principal.ts`. A host that wants per-principal reads passes this
|
|
44
|
+
* page through `readableObjectPage`.
|
|
45
|
+
*/
|
|
35
46
|
readObjects(typeName: string, query: CatalogObjectQuery & {
|
|
36
47
|
snapshot?: string;
|
|
37
48
|
}): Promise<CatalogObjectPage>;
|
|
@@ -68,9 +79,22 @@ export declare class CatalogService {
|
|
|
68
79
|
private requireWorkspace;
|
|
69
80
|
listSavedQueries(): Promise<SavedQuery[]>;
|
|
70
81
|
getSavedQuery(id: string): Promise<SavedQuery>;
|
|
82
|
+
/**
|
|
83
|
+
* @param createdBy who saved it — the row's author and the audit entry's
|
|
84
|
+
* actor. The host's resolved principal id where the host resolves one; see
|
|
85
|
+
* the enforcement note in `catalog.principal.ts` for why this library cannot
|
|
86
|
+
* work it out itself.
|
|
87
|
+
*/
|
|
71
88
|
saveQuery(input: SaveQueryInput, createdBy: string): Promise<SavedQuery>;
|
|
72
|
-
|
|
73
|
-
|
|
89
|
+
/** @param changedBy who made the change, for the audit trail. */
|
|
90
|
+
updateSavedQuery(id: string, input: Partial<SaveQueryInput>, changedBy: string): Promise<SavedQuery>;
|
|
91
|
+
/**
|
|
92
|
+
* @param deletedBy who deleted it, for the audit trail. Required rather than
|
|
93
|
+
* defaulted, matching `saveQuery` and `updateSavedQuery`: a default would
|
|
94
|
+
* quietly attribute revocations to nobody in every caller that was not
|
|
95
|
+
* updated, and the trail's whole value here is that it names somebody.
|
|
96
|
+
*/
|
|
97
|
+
deleteSavedQuery(id: string, deletedBy: string): Promise<boolean>;
|
|
74
98
|
/** Runs a saved query, honouring the TTL it was saved with. */
|
|
75
99
|
runSavedQuery(id: string, maxRows?: number): Promise<{
|
|
76
100
|
savedQuery: SavedQuery;
|
|
@@ -78,17 +102,31 @@ export declare class CatalogService {
|
|
|
78
102
|
}>;
|
|
79
103
|
listDashboards(): Promise<Dashboard[]>;
|
|
80
104
|
getDashboard(id: string): Promise<Dashboard>;
|
|
105
|
+
/**
|
|
106
|
+
* `shared` is declared here, and that is not cosmetic.
|
|
107
|
+
*
|
|
108
|
+
* The store has always accepted it, so it worked as long as the body reached
|
|
109
|
+
* the store untouched. A host with a whitelisting `ValidationPipe` — the
|
|
110
|
+
* normal, recommended configuration — strips a property no type declares, and
|
|
111
|
+
* the symptom is a dashboard that cannot be shared with no error anywhere:
|
|
112
|
+
* the toggle saves, the response says `shared: false`, and the embed API
|
|
113
|
+
* keeps answering 403 for a board somebody just shared.
|
|
114
|
+
*/
|
|
81
115
|
saveDashboard(input: {
|
|
82
116
|
name: string;
|
|
83
117
|
description?: string;
|
|
84
118
|
cards?: DashboardCard[];
|
|
119
|
+
shared?: boolean;
|
|
85
120
|
}, createdBy: string): Promise<Dashboard>;
|
|
121
|
+
/** @param changedBy who made the change, for the audit trail. */
|
|
86
122
|
updateDashboard(id: string, input: Partial<{
|
|
87
123
|
name: string;
|
|
88
124
|
description: string;
|
|
89
125
|
cards: DashboardCard[];
|
|
90
|
-
|
|
91
|
-
|
|
126
|
+
shared: boolean;
|
|
127
|
+
}>, changedBy: string): Promise<Dashboard>;
|
|
128
|
+
/** @param deletedBy who deleted it. See {@link deleteSavedQuery}. */
|
|
129
|
+
deleteDashboard(id: string, deletedBy: string): Promise<boolean>;
|
|
92
130
|
/** Everything shared, so a consumer can discover what it may render. */
|
|
93
131
|
listEmbeddable(): Promise<{
|
|
94
132
|
dashboards: Array<{
|
|
@@ -104,11 +142,16 @@ export declare class CatalogService {
|
|
|
104
142
|
kind: string;
|
|
105
143
|
}>;
|
|
106
144
|
}>;
|
|
107
|
-
/**
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
145
|
+
/**
|
|
146
|
+
* One chart, rendered.
|
|
147
|
+
*
|
|
148
|
+
* `placement` is what the dashboard card said, and it is honoured rather than
|
|
149
|
+
* merely carried: a card's `title` and `library` exist to override the saved
|
|
150
|
+
* query on THIS board, so an embed that ignored them would show a different
|
|
151
|
+
* heading and a different chart from the console for the same dashboard —
|
|
152
|
+
* silently, with nothing thrown and nothing logged.
|
|
153
|
+
*/
|
|
154
|
+
embedChart(savedQueryId: string, placement?: EmbeddedChartPlacement): Promise<EmbeddedChart>;
|
|
112
155
|
/** A whole dashboard, every chart resolved. */
|
|
113
156
|
embedDashboard(dashboardId: string): Promise<EmbeddedDashboard>;
|
|
114
157
|
listEvents(query: AuditQuery): Promise<CatalogAuditEvent[]>;
|