@attlaz/client 1.95.0 → 1.97.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/dist/Catalog/Service/CatalogEndpoint.js +2 -1
- package/dist/Catalog/Service/CatalogProductEndpoint.js +3 -2
- package/dist/Client.d.ts +12 -12
- package/dist/Client.js +12 -12
- package/dist/{EnhanceData → DataEnrichment}/Service/GenerateContentEndpoint.js +3 -2
- package/dist/DataQuality/Model/QualityDataset.d.ts +1 -1
- package/dist/DataQuality/Model/QualityDataset.js +1 -1
- package/dist/DataQuality/Model/QualityEntity.d.ts +1 -1
- package/dist/DataQuality/Model/QualityEntity.js +1 -1
- package/dist/DataQuality/Model/{QualitySuppression.d.ts → QualityEntityProblemSuppression.d.ts} +3 -3
- package/dist/DataQuality/Model/{QualitySuppression.js → QualityEntityProblemSuppression.js} +3 -3
- package/dist/DataQuality/Model/QualityEntityProblemSuppressionResult.d.ts +6 -0
- package/dist/DataQuality/Model/QualityProblem.d.ts +1 -1
- package/dist/DataQuality/Model/QualityProblem.js +1 -1
- package/dist/DataQuality/Model/QualityProblemDefinition.d.ts +1 -1
- package/dist/DataQuality/Model/QualityProblemDefinition.js +1 -1
- package/dist/DataQuality/Model/QualityProblemEvent.d.ts +1 -1
- package/dist/DataQuality/Model/QualityProblemEvent.js +1 -1
- package/dist/DataQuality/Model/QualityReportEntity.d.ts +31 -0
- package/dist/DataQuality/Model/QualityReportEntity.js +44 -0
- package/dist/DataQuality/Model/QualityReportProblem.d.ts +22 -0
- package/dist/DataQuality/Model/QualityReportProblem.js +35 -0
- package/dist/DataQuality/Model/QualityReportResult.d.ts +9 -0
- package/dist/DataQuality/Model/QualityReportResult.js +14 -0
- package/dist/DataQuality/Model/QualityScan.d.ts +19 -0
- package/dist/DataQuality/Model/QualityScan.js +28 -0
- package/dist/DataQuality/Service/QualityEndpoint.d.ts +64 -7
- package/dist/DataQuality/Service/QualityEndpoint.js +116 -30
- package/dist/FeedManagement/Service/CatalogFeedEndpoint.js +2 -1
- package/dist/Flow/Service/FlowEndpoint.js +6 -5
- package/dist/Flow/Service/FlowRunEndpoint.js +8 -7
- package/dist/Flow/Service/FlowRunRequestEndpoint.js +2 -1
- package/dist/Flow/Service/FlowRunStatsEndpoint.js +2 -1
- package/dist/Helper/LoadAllHelper.js +7 -1
- package/dist/Http/Data/Path.d.ts +11 -0
- package/dist/Http/Data/Path.js +31 -0
- package/dist/Http/HttpClient.d.ts +8 -1
- package/dist/Http/HttpClient.js +21 -1
- package/dist/Http/OAuthClientOptions.d.ts +2 -0
- package/dist/Http/OAuthClientOptions.js +3 -0
- package/dist/Http/Transport/OAuthClient.d.ts +7 -0
- package/dist/Http/Transport/OAuthClient.js +31 -4
- package/dist/Logging/Service/LogEndpoint.d.ts +5 -1
- package/dist/Logging/Service/LogEndpoint.js +14 -8
- package/dist/{Pulse → MarketPulse}/Service/CompetitorEndpoint.js +4 -3
- package/dist/{Pulse → MarketPulse}/Service/CrawlJobEndpoint.d.ts +1 -0
- package/dist/{Pulse → MarketPulse}/Service/CrawlJobEndpoint.js +8 -6
- package/dist/{Pulse → MarketPulse}/Service/IndexJobEndpoint.js +5 -4
- package/dist/{Pulse → MarketPulse}/Service/PriceAlertEndpoint.js +8 -7
- package/dist/{Pulse → MarketPulse}/Service/PricingEndpoint.js +9 -8
- package/dist/{Pulse → MarketPulse}/Service/ProductGroupEndpoint.js +4 -3
- package/dist/{Pulse → MarketPulse}/Service/ProductMatchEndpoint.js +6 -5
- package/dist/{Pulse → MarketPulse}/Service/ProductMatchJobEndpoint.js +5 -4
- package/dist/{Pulse → MarketPulse}/Service/PulseCatalogProductEndpoint.js +4 -3
- package/dist/{Pulse → MarketPulse}/Service/PulseStatisticsEndpoint.js +5 -4
- package/dist/{Pulse → MarketPulse}/Service/VendorProductEndpoint.js +11 -7
- package/dist/Service/AccessTokenEndpoint.js +3 -2
- package/dist/Service/AdapterConnectionEndpoint.js +9 -8
- package/dist/Service/AdapterEndpoint.js +3 -2
- package/dist/Service/ChannelEndpoint.js +5 -4
- package/dist/Service/CodeDeployEndpoint.js +5 -4
- package/dist/Service/CodeSourceStrategiesEndpoint.js +3 -2
- package/dist/Service/CollectionsEndpoint.js +5 -4
- package/dist/Service/CommandExecutionEndpoint.js +2 -1
- package/dist/Service/ConfigEndpoint.d.ts +6 -1
- package/dist/Service/ConfigEndpoint.js +9 -3
- package/dist/Service/ConfigurationEndpoint.js +5 -4
- package/dist/Service/HealthAlertEndpoint.js +3 -2
- package/dist/Service/PlatformEndpoint.js +3 -2
- package/dist/Service/PlatformLanguageEndpoint.js +2 -1
- package/dist/Service/ProjectEndpoint.js +3 -2
- package/dist/Service/ProjectEnvironmentEndpoint.js +3 -2
- package/dist/Service/ProviderTokenEndpoint.js +4 -3
- package/dist/Service/RunnerPoolEndpoint.js +5 -4
- package/dist/Service/SourcesAccountEndpoint.js +6 -5
- package/dist/Service/StorageEndpoint.js +8 -7
- package/dist/Service/SubscriberEndpoint.js +4 -3
- package/dist/Service/TriggerEndpoint.js +5 -4
- package/dist/Service/UserActionEndpoint.js +4 -3
- package/dist/Service/UserEndpoint.js +3 -2
- package/dist/Service/WorkspaceEndpoint.js +2 -1
- package/dist/Service/WorkspaceMemberEndpoint.js +5 -4
- package/dist/index.d.ts +52 -48
- package/dist/index.js +38 -34
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +4 -2
- package/dist/DataQuality/Model/QualitySuppressionResult.d.ts +0 -6
- /package/dist/{EnhanceData → DataEnrichment}/Model/GenerateContentBatch.d.ts +0 -0
- /package/dist/{EnhanceData → DataEnrichment}/Model/GenerateContentBatch.js +0 -0
- /package/dist/{EnhanceData → DataEnrichment}/Model/GenerateContentBatchData.d.ts +0 -0
- /package/dist/{EnhanceData → DataEnrichment}/Model/GenerateContentBatchData.js +0 -0
- /package/dist/{EnhanceData → DataEnrichment}/Service/GenerateContentEndpoint.d.ts +0 -0
- /package/dist/DataQuality/Model/{QualitySuppressionResult.js → QualityEntityProblemSuppressionResult.js} +0 -0
- /package/dist/{Pulse → MarketPulse}/Model/CatalogProductWithMatches.d.ts +0 -0
- /package/dist/{Pulse → MarketPulse}/Model/CatalogProductWithMatches.js +0 -0
- /package/dist/{Pulse → MarketPulse}/Model/CompetingVendorProduct.d.ts +0 -0
- /package/dist/{Pulse → MarketPulse}/Model/CompetingVendorProduct.js +0 -0
- /package/dist/{Pulse → MarketPulse}/Model/Competitor.d.ts +0 -0
- /package/dist/{Pulse → MarketPulse}/Model/Competitor.js +0 -0
- /package/dist/{Pulse → MarketPulse}/Model/CrawlJob.d.ts +0 -0
- /package/dist/{Pulse → MarketPulse}/Model/CrawlJob.js +0 -0
- /package/dist/{Pulse → MarketPulse}/Model/IndexJob.d.ts +0 -0
- /package/dist/{Pulse → MarketPulse}/Model/IndexJob.js +0 -0
- /package/dist/{Pulse → MarketPulse}/Model/MarketSummaryEntry.d.ts +0 -0
- /package/dist/{Pulse → MarketPulse}/Model/MarketSummaryEntry.js +0 -0
- /package/dist/{Pulse → MarketPulse}/Model/MatchHistory.d.ts +0 -0
- /package/dist/{Pulse → MarketPulse}/Model/MatchHistory.js +0 -0
- /package/dist/{Pulse → MarketPulse}/Model/MatchHistoryBucket.d.ts +0 -0
- /package/dist/{Pulse → MarketPulse}/Model/MatchHistoryBucket.js +0 -0
- /package/dist/{Pulse → MarketPulse}/Model/MatchHistoryInterval.d.ts +0 -0
- /package/dist/{Pulse → MarketPulse}/Model/MatchHistoryInterval.js +0 -0
- /package/dist/{Pulse → MarketPulse}/Model/MatchedVendorProduct.d.ts +0 -0
- /package/dist/{Pulse → MarketPulse}/Model/MatchedVendorProduct.js +0 -0
- /package/dist/{Pulse → MarketPulse}/Model/PriceAlertConfig.d.ts +0 -0
- /package/dist/{Pulse → MarketPulse}/Model/PriceAlertConfig.js +0 -0
- /package/dist/{Pulse → MarketPulse}/Model/PriceAlertConfigData.d.ts +0 -0
- /package/dist/{Pulse → MarketPulse}/Model/PriceAlertConfigData.js +0 -0
- /package/dist/{Pulse → MarketPulse}/Model/PriceAlertEvent.d.ts +0 -0
- /package/dist/{Pulse → MarketPulse}/Model/PriceAlertEvent.js +0 -0
- /package/dist/{Pulse → MarketPulse}/Model/PriceIndexSnapshotEntry.d.ts +0 -0
- /package/dist/{Pulse → MarketPulse}/Model/PriceIndexSnapshotEntry.js +0 -0
- /package/dist/{Pulse → MarketPulse}/Model/PriceSuggestion.d.ts +0 -0
- /package/dist/{Pulse → MarketPulse}/Model/PriceSuggestion.js +0 -0
- /package/dist/{Pulse → MarketPulse}/Model/PricingRule.d.ts +0 -0
- /package/dist/{Pulse → MarketPulse}/Model/PricingRule.js +0 -0
- /package/dist/{Pulse → MarketPulse}/Model/PricingRuleData.d.ts +0 -0
- /package/dist/{Pulse → MarketPulse}/Model/PricingRuleData.js +0 -0
- /package/dist/{Pulse → MarketPulse}/Model/ProductGroup.d.ts +0 -0
- /package/dist/{Pulse → MarketPulse}/Model/ProductGroup.js +0 -0
- /package/dist/{Pulse → MarketPulse}/Model/ProductGroupData.d.ts +0 -0
- /package/dist/{Pulse → MarketPulse}/Model/ProductGroupData.js +0 -0
- /package/dist/{Pulse → MarketPulse}/Model/ProductMatch.d.ts +0 -0
- /package/dist/{Pulse → MarketPulse}/Model/ProductMatch.js +0 -0
- /package/dist/{Pulse → MarketPulse}/Model/ProductMatchJob.d.ts +0 -0
- /package/dist/{Pulse → MarketPulse}/Model/ProductMatchJob.js +0 -0
- /package/dist/{Pulse → MarketPulse}/Model/ProductSegmentType.d.ts +0 -0
- /package/dist/{Pulse → MarketPulse}/Model/ProductSegmentType.js +0 -0
- /package/dist/{Pulse → MarketPulse}/Model/ProductsSegment.d.ts +0 -0
- /package/dist/{Pulse → MarketPulse}/Model/ProductsSegment.js +0 -0
- /package/dist/{Pulse → MarketPulse}/Model/PulseCatalogProductFilterOptions.d.ts +0 -0
- /package/dist/{Pulse → MarketPulse}/Model/PulseCatalogProductFilterOptions.js +0 -0
- /package/dist/{Pulse → MarketPulse}/Model/PulseCatalogProductFilters.d.ts +0 -0
- /package/dist/{Pulse → MarketPulse}/Model/PulseCatalogProductFilters.js +0 -0
- /package/dist/{Pulse → MarketPulse}/Model/ReviewMatch.d.ts +0 -0
- /package/dist/{Pulse → MarketPulse}/Model/ReviewMatch.js +0 -0
- /package/dist/{Pulse → MarketPulse}/Model/ReviewMatchFilters.d.ts +0 -0
- /package/dist/{Pulse → MarketPulse}/Model/ReviewMatchFilters.js +0 -0
- /package/dist/{Pulse → MarketPulse}/Model/TopCompetitor.d.ts +0 -0
- /package/dist/{Pulse → MarketPulse}/Model/TopCompetitor.js +0 -0
- /package/dist/{Pulse → MarketPulse}/Model/Vendor.d.ts +0 -0
- /package/dist/{Pulse → MarketPulse}/Model/Vendor.js +0 -0
- /package/dist/{Pulse → MarketPulse}/Model/VendorProduct.d.ts +0 -0
- /package/dist/{Pulse → MarketPulse}/Model/VendorProduct.js +0 -0
- /package/dist/{Pulse → MarketPulse}/Model/VendorProductPriceHistoryEntry.d.ts +0 -0
- /package/dist/{Pulse → MarketPulse}/Model/VendorProductPriceHistoryEntry.js +0 -0
- /package/dist/{Pulse → MarketPulse}/Model/VendorStatus.d.ts +0 -0
- /package/dist/{Pulse → MarketPulse}/Model/VendorStatus.js +0 -0
- /package/dist/{Pulse → MarketPulse}/Service/CompetitorEndpoint.d.ts +0 -0
- /package/dist/{Pulse → MarketPulse}/Service/IndexJobEndpoint.d.ts +0 -0
- /package/dist/{Pulse → MarketPulse}/Service/PriceAlertEndpoint.d.ts +0 -0
- /package/dist/{Pulse → MarketPulse}/Service/PricingEndpoint.d.ts +0 -0
- /package/dist/{Pulse → MarketPulse}/Service/ProductGroupEndpoint.d.ts +0 -0
- /package/dist/{Pulse → MarketPulse}/Service/ProductMatchEndpoint.d.ts +0 -0
- /package/dist/{Pulse → MarketPulse}/Service/ProductMatchJobEndpoint.d.ts +0 -0
- /package/dist/{Pulse → MarketPulse}/Service/PulseCatalogProductEndpoint.d.ts +0 -0
- /package/dist/{Pulse → MarketPulse}/Service/PulseStatisticsEndpoint.d.ts +0 -0
- /package/dist/{Pulse → MarketPulse}/Service/VendorProductEndpoint.d.ts +0 -0
|
@@ -1,24 +1,29 @@
|
|
|
1
|
+
import { path } from '../../Http/Data/Path.js';
|
|
2
|
+
import { HttpStatus } from '../../Http/HttpStatus.js';
|
|
1
3
|
import { QueryString } from '../../Http/Data/QueryString.js';
|
|
4
|
+
import { ApiError } from '../../Model/Error/ApiError.js';
|
|
2
5
|
import { QualityDataset } from '../Model/QualityDataset.js';
|
|
3
6
|
import { QualityEntity } from '../Model/QualityEntity.js';
|
|
4
7
|
import { QualityProblem } from '../Model/QualityProblem.js';
|
|
5
8
|
import { QualityProblemDefinition } from '../Model/QualityProblemDefinition.js';
|
|
6
9
|
import { QualityProblemEvent } from '../Model/QualityProblemEvent.js';
|
|
7
|
-
import {
|
|
10
|
+
import { QualityReportResult } from '../Model/QualityReportResult.js';
|
|
11
|
+
import { QualityScan } from '../Model/QualityScan.js';
|
|
12
|
+
import { QualityEntityProblemSuppression } from '../Model/QualityEntityProblemSuppression.js';
|
|
8
13
|
import { Endpoint } from '../../Service/Endpoint.js';
|
|
9
14
|
/**
|
|
10
|
-
* Data Quality — `/
|
|
15
|
+
* Data Quality — `/data-quality/datasets`. A dataset holds the entities being checked and the
|
|
11
16
|
* problems open on them.
|
|
12
17
|
*/
|
|
13
18
|
export class QualityEndpoint extends Endpoint {
|
|
14
19
|
async getDatasets(projectId, pagination = null) {
|
|
15
|
-
const queryString = new QueryString('/
|
|
20
|
+
const queryString = new QueryString('/data-quality/datasets');
|
|
16
21
|
queryString.set('project', projectId);
|
|
17
22
|
queryString.addPagination(pagination);
|
|
18
23
|
return await this.requestCollection(queryString, QualityDataset.parse);
|
|
19
24
|
}
|
|
20
25
|
async getDataset(datasetId) {
|
|
21
|
-
const result = await this.requestObject('/
|
|
26
|
+
const result = await this.requestObject(path('/data-quality/datasets/:datasetId', { datasetId }), null, QualityDataset.parse, 'GET');
|
|
22
27
|
return result.getData();
|
|
23
28
|
}
|
|
24
29
|
async createDataset(projectId, name, autoCloseMode = null) {
|
|
@@ -26,19 +31,19 @@ export class QualityEndpoint extends Endpoint {
|
|
|
26
31
|
if (autoCloseMode !== null) {
|
|
27
32
|
body['auto_close_mode'] = autoCloseMode;
|
|
28
33
|
}
|
|
29
|
-
const result = await this.requestObject('/
|
|
34
|
+
const result = await this.requestObject('/data-quality/datasets', body, QualityDataset.parse, 'POST');
|
|
30
35
|
return result.getData();
|
|
31
36
|
}
|
|
32
37
|
async updateDataset(datasetId, changes) {
|
|
33
38
|
// camelCase keys are snake_cased by the base Endpoint (autoCloseMode -> auto_close_mode).
|
|
34
|
-
const result = await this.requestObject('/
|
|
39
|
+
const result = await this.requestObject(path('/data-quality/datasets/:datasetId', { datasetId }), changes, QualityDataset.parse, 'PATCH');
|
|
35
40
|
return result.getData();
|
|
36
41
|
}
|
|
37
42
|
async deleteDataset(datasetId) {
|
|
38
|
-
await this.requestObject('/
|
|
43
|
+
await this.requestObject(path('/data-quality/datasets/:datasetId', { datasetId }), null, (raw) => raw, 'DELETE');
|
|
39
44
|
}
|
|
40
45
|
async getEntities(datasetId, pagination = null, filters = {}) {
|
|
41
|
-
const queryString = new QueryString('/
|
|
46
|
+
const queryString = new QueryString(path('/data-quality/datasets/:datasetId/entities', { datasetId }));
|
|
42
47
|
if (filters.entityType) {
|
|
43
48
|
queryString.set('entity_type', filters.entityType);
|
|
44
49
|
}
|
|
@@ -62,9 +67,7 @@ export class QualityEndpoint extends Endpoint {
|
|
|
62
67
|
}
|
|
63
68
|
/** Suppressed problems are hidden unless `includeSuppressed` is set. */
|
|
64
69
|
async getEntityProblems(datasetId, entityType, entityId, includeSuppressed = false) {
|
|
65
|
-
const queryString = new QueryString('/
|
|
66
|
-
+ '/entities/' + encodeURIComponent(entityType)
|
|
67
|
-
+ '/' + encodeURIComponent(entityId) + '/problems');
|
|
70
|
+
const queryString = new QueryString(path('/data-quality/datasets/:datasetId/entities/:entityType/:entityId/problems', { datasetId, entityType, entityId }));
|
|
68
71
|
if (includeSuppressed) {
|
|
69
72
|
queryString.set('include_suppressed', 'true');
|
|
70
73
|
}
|
|
@@ -72,22 +75,105 @@ export class QualityEndpoint extends Endpoint {
|
|
|
72
75
|
}
|
|
73
76
|
/** The transition timeline for one problem, oldest first. */
|
|
74
77
|
async getProblemEvents(datasetId, entityType, entityId, code) {
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
78
|
+
const requestPath = path('/data-quality/datasets/:datasetId/entities/:entityType/:entityId/problems/:code/events', { datasetId, entityType, entityId, code });
|
|
79
|
+
return await this.requestCollection(requestPath, QualityProblemEvent.parse);
|
|
80
|
+
}
|
|
81
|
+
// ---- Ingest ----
|
|
82
|
+
/**
|
|
83
|
+
* Open a scan: the bracket a reporter puts around one run of checks.
|
|
84
|
+
*
|
|
85
|
+
* `pack` names the group of checks this run covers. It scopes what completing the scan is allowed
|
|
86
|
+
* to close, so a partial run cannot resolve problems it never looked at.
|
|
87
|
+
*/
|
|
88
|
+
async openScan(datasetId, pack = null) {
|
|
89
|
+
const requestPath = path('/data-quality/datasets/:datasetId/scans', { datasetId });
|
|
90
|
+
const result = await this.requestObject(requestPath, { pack: pack }, QualityScan.parse, 'POST');
|
|
91
|
+
const scan = result.getData();
|
|
92
|
+
if (scan === null) {
|
|
93
|
+
throw QualityEndpoint.writeFailed(requestPath);
|
|
94
|
+
}
|
|
95
|
+
return scan;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Report what an open scan found. Safe to call repeatedly to send entities in batches.
|
|
99
|
+
*
|
|
100
|
+
* Entities are created on first report — there is nothing to register up front.
|
|
101
|
+
*
|
|
102
|
+
* **Batch size**: the API caps a request body at 512 KB (the Gateway rejects over 1 MB before it
|
|
103
|
+
* even arrives), and an entity carrying one problem is roughly 250-300 bytes of JSON — so keep
|
|
104
|
+
* batches to a low four figures of entities. An oversize request comes back as `413` with error
|
|
105
|
+
* code `payload_too_large`. Send several batches into the same scan rather than one large one.
|
|
106
|
+
*/
|
|
107
|
+
async reportToScan(datasetId, scanId, entities) {
|
|
108
|
+
return await this.postReport(path('/data-quality/datasets/:datasetId/scans/:scanId/report', { datasetId, scanId }), { entities: entities.map((entity) => entity.toPayload()) });
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Close the scan, resolving problems the run covered but did not re-report. Returns how many were
|
|
112
|
+
* resolved.
|
|
113
|
+
*
|
|
114
|
+
* Skipping this leaves problems open that the reporter has already stopped finding.
|
|
115
|
+
*/
|
|
116
|
+
async completeScan(datasetId, scanId) {
|
|
117
|
+
const requestPath = path('/data-quality/datasets/:datasetId/scans/:scanId/complete', { datasetId, scanId });
|
|
118
|
+
const result = await this.requestObject(requestPath, {}, (raw) => ({ resolved: Number(raw.resolved ?? 0) }), 'POST');
|
|
119
|
+
const data = result.getData();
|
|
120
|
+
if (data === null) {
|
|
121
|
+
throw QualityEndpoint.writeFailed(requestPath);
|
|
122
|
+
}
|
|
123
|
+
return data.resolved;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Open a scan, report everything and complete it in one call — for a reporter that already has
|
|
127
|
+
* the full picture in memory and does not need to stream it in batches.
|
|
128
|
+
*
|
|
129
|
+
* Everything goes in one request, so this is bounded by the API's 512 KB body cap — a few
|
|
130
|
+
* thousand entities at most. Beyond that use `openScan` / `reportToScan` / `completeScan`, which
|
|
131
|
+
* can send as many batches as you like into one scan.
|
|
132
|
+
*/
|
|
133
|
+
async reportProblems(datasetId, entities, pack = null) {
|
|
134
|
+
return await this.postReport(path('/data-quality/datasets/:datasetId/problems', { datasetId }), {
|
|
135
|
+
pack: pack,
|
|
136
|
+
entities: entities.map((entity) => entity.toPayload()),
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Both report routes take the same already-snake_cased body and return the same counts.
|
|
141
|
+
*
|
|
142
|
+
* `_formatted: true` sends the body verbatim. Without it the client's key formatter would rewrite
|
|
143
|
+
* every key and strip the trailing `_id` from `entity_id`, which the endpoint requires.
|
|
144
|
+
*/
|
|
145
|
+
async postReport(requestPath, body) {
|
|
146
|
+
const result = await this.requestObject(requestPath, { ...body, _formatted: true }, QualityReportResult.parse, 'POST');
|
|
147
|
+
const reportResult = result.getData();
|
|
148
|
+
if (reportResult === null) {
|
|
149
|
+
throw QualityEndpoint.writeFailed(requestPath);
|
|
150
|
+
}
|
|
151
|
+
return reportResult;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* A write that came back with nothing — the base Endpoint maps a 404 to null data, and also
|
|
155
|
+
* returns null when the response cannot be parsed.
|
|
156
|
+
*
|
|
157
|
+
* Reads may legitimately answer "no such thing" with null, but a write cannot: returning a zero
|
|
158
|
+
* count here would be indistinguishable from a report that genuinely changed nothing, and a
|
|
159
|
+
* reporter would log success while its problems went nowhere.
|
|
160
|
+
*
|
|
161
|
+
* The message names both causes because the API cannot yet tell them apart — a missing dataset
|
|
162
|
+
* and a path that no longer exists (a reporter still on `/data_quality/`) both come back as a
|
|
163
|
+
* bare 404. Once the API sets an error `code`, branch on that instead.
|
|
164
|
+
*/
|
|
165
|
+
static writeFailed(requestPath) {
|
|
166
|
+
return new ApiError('Data Quality write to `' + requestPath + '` returned nothing. Either the dataset or scan does not exist, '
|
|
167
|
+
+ 'or the endpoint does not — check the id, and that the client version matches the API.', HttpStatus.HTTP_NOTFOUND);
|
|
80
168
|
}
|
|
81
169
|
/** Clear an entity's problems so the next scan starts from scratch; returns how many were removed. */
|
|
82
170
|
async resetEntity(datasetId, entityType, entityId) {
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
+ '/' + encodeURIComponent(entityId) + '/reset';
|
|
86
|
-
const result = await this.requestObject(path, {}, (raw) => ({ deleted: raw.deleted ?? 0 }), 'POST');
|
|
171
|
+
const requestPath = path('/data-quality/datasets/:datasetId/entities/:entityType/:entityId/reset', { datasetId, entityType, entityId });
|
|
172
|
+
const result = await this.requestObject(requestPath, {}, (raw) => ({ deleted: raw.deleted ?? 0 }), 'POST');
|
|
87
173
|
return result.getData()?.deleted ?? 0;
|
|
88
174
|
}
|
|
89
175
|
async getFilterOptions(datasetId) {
|
|
90
|
-
const result = await this.requestObject('/
|
|
176
|
+
const result = await this.requestObject(path('/data-quality/datasets/:datasetId/filter-options', { datasetId }), null, (raw) => ({
|
|
91
177
|
entityTypes: raw.entity_types ?? [],
|
|
92
178
|
properties: raw.properties ?? [],
|
|
93
179
|
codes: (raw.codes ?? []).map((row) => ({
|
|
@@ -100,7 +186,7 @@ export class QualityEndpoint extends Endpoint {
|
|
|
100
186
|
}
|
|
101
187
|
/** Both groupings in one call: `counts` by (property, severity) and `codes` for the fix cards. */
|
|
102
188
|
async getSummary(datasetId) {
|
|
103
|
-
const result = await this.requestObject('/
|
|
189
|
+
const result = await this.requestObject(path('/data-quality/datasets/:datasetId/summary', { datasetId }), null, (raw) => ({
|
|
104
190
|
counts: (raw.counts ?? []).map((row) => ({
|
|
105
191
|
property: row.property,
|
|
106
192
|
severity: row.severity,
|
|
@@ -125,7 +211,7 @@ export class QualityEndpoint extends Endpoint {
|
|
|
125
211
|
* have open problems, so a fully suppressed or resolved code has no label there.
|
|
126
212
|
*/
|
|
127
213
|
async getDefinitions(projectId, pack = null) {
|
|
128
|
-
const queryString = new QueryString('/
|
|
214
|
+
const queryString = new QueryString('/data-quality/definitions');
|
|
129
215
|
queryString.set('project', projectId);
|
|
130
216
|
if (pack !== null) {
|
|
131
217
|
queryString.set('pack', pack);
|
|
@@ -133,15 +219,15 @@ export class QualityEndpoint extends Endpoint {
|
|
|
133
219
|
return await this.requestCollection(queryString, QualityProblemDefinition.parse);
|
|
134
220
|
}
|
|
135
221
|
// ---- Suppressions ----
|
|
136
|
-
async
|
|
137
|
-
return await this.requestCollection('/
|
|
222
|
+
async getEntityProblemSuppressions(datasetId) {
|
|
223
|
+
return await this.requestCollection(path('/data-quality/datasets/:datasetId/entity-problem-suppressions', { datasetId }), QualityEntityProblemSuppression.parse);
|
|
138
224
|
}
|
|
139
225
|
/**
|
|
140
226
|
* Suppress a code, either on one entity or — with both entity fields omitted — across the whole
|
|
141
227
|
* dataset. Also mutes the matching problems that are already open, so suppressing acts on what
|
|
142
228
|
* is there rather than only on future scans.
|
|
143
229
|
*/
|
|
144
|
-
async
|
|
230
|
+
async createEntityProblemSuppression(datasetId, suppression) {
|
|
145
231
|
// Both entity keys are always sent, explicitly null for a dataset-wide suppression: the
|
|
146
232
|
// endpoint requires them to be present so that a dropped or renamed key is an error rather
|
|
147
233
|
// than a silent widening to the whole dataset.
|
|
@@ -154,15 +240,15 @@ export class QualityEndpoint extends Endpoint {
|
|
|
154
240
|
entity: suppression.entityId ?? null,
|
|
155
241
|
reason: suppression.reason ?? null,
|
|
156
242
|
};
|
|
157
|
-
const result = await this.requestObject('/
|
|
158
|
-
suppression: raw.id ?
|
|
243
|
+
const result = await this.requestObject(path('/data-quality/datasets/:datasetId/entity-problem-suppressions', { datasetId }), body, (raw) => ({
|
|
244
|
+
suppression: raw.id ? QualityEntityProblemSuppression.parse(raw) : null,
|
|
159
245
|
muted: Number(raw.muted ?? 0),
|
|
160
246
|
}), 'POST');
|
|
161
247
|
return result.getData() ?? { suppression: null, muted: 0 };
|
|
162
248
|
}
|
|
163
249
|
/** Lift a suppression; returns how many muted problems were returned to the open list. */
|
|
164
|
-
async
|
|
165
|
-
const result = await this.requestObject('/
|
|
250
|
+
async deleteEntityProblemSuppression(datasetId, entityProblemSuppressionId) {
|
|
251
|
+
const result = await this.requestObject(path('/data-quality/datasets/:datasetId/entity-problem-suppressions/:entityProblemSuppressionId', { datasetId, entityProblemSuppressionId }), null, (raw) => ({ reopened: Number(raw.reopened ?? 0) }), 'DELETE');
|
|
166
252
|
return result.getData()?.reopened ?? 0;
|
|
167
253
|
}
|
|
168
254
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { path } from '../../Http/Data/Path.js';
|
|
1
2
|
import { QueryString } from '../../Http/Data/QueryString.js';
|
|
2
3
|
import { Endpoint } from '../../Service/Endpoint.js';
|
|
3
4
|
import { CatalogFeed } from '../Model/CatalogFeed.js';
|
|
@@ -9,7 +10,7 @@ import { CatalogFeed } from '../Model/CatalogFeed.js';
|
|
|
9
10
|
*/
|
|
10
11
|
export class CatalogFeedEndpoint extends Endpoint {
|
|
11
12
|
async getByCatalog(catalogId, pagination = null, expand = []) {
|
|
12
|
-
const queryString = new QueryString('/catalogs/'
|
|
13
|
+
const queryString = new QueryString(path('/catalogs/:catalogId/feeds', { catalogId: catalogId.toString() }));
|
|
13
14
|
if (expand.length > 0) {
|
|
14
15
|
queryString.set('expand', expand);
|
|
15
16
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { path } from '../../Http/Data/Path.js';
|
|
1
2
|
import { QueryString } from '../../Http/Data/QueryString.js';
|
|
2
3
|
import { HttpStatus } from '../../Http/HttpStatus.js';
|
|
3
4
|
import { Flow } from '../Model/Flow.js';
|
|
@@ -7,7 +8,7 @@ import { Endpoint } from '../../Service/Endpoint.js';
|
|
|
7
8
|
export class FlowEndpoint extends Endpoint {
|
|
8
9
|
async getByProject(projectId, pagination) {
|
|
9
10
|
try {
|
|
10
|
-
const queryString = new QueryString('/projects/'
|
|
11
|
+
const queryString = new QueryString(path('/projects/:projectId/flows', { projectId }));
|
|
11
12
|
queryString.addPagination(pagination);
|
|
12
13
|
const result = await this.requestCollection(queryString, Flow.parse);
|
|
13
14
|
return result;
|
|
@@ -21,7 +22,7 @@ export class FlowEndpoint extends Endpoint {
|
|
|
21
22
|
}
|
|
22
23
|
async getById(flowId) {
|
|
23
24
|
try {
|
|
24
|
-
const result = await this.requestObject('/flows
|
|
25
|
+
const result = await this.requestObject(path('/flows/:flowId', { flowId }), null, Flow.parse);
|
|
25
26
|
return result.getData();
|
|
26
27
|
}
|
|
27
28
|
catch (error) {
|
|
@@ -39,7 +40,7 @@ export class FlowEndpoint extends Endpoint {
|
|
|
39
40
|
if (!Utils.isNullOrUndefined(projectEnvironmentId)) {
|
|
40
41
|
parameters = { environment: projectEnvironmentId };
|
|
41
42
|
}
|
|
42
|
-
const queryString = new QueryString('projects/'
|
|
43
|
+
const queryString = new QueryString(path('projects/:projectId/flowsummaries', { projectId }));
|
|
43
44
|
if (pagination !== null) {
|
|
44
45
|
queryString.addPagination(pagination);
|
|
45
46
|
}
|
|
@@ -51,7 +52,7 @@ export class FlowEndpoint extends Endpoint {
|
|
|
51
52
|
parameters = { environment: projectEnvironmentId };
|
|
52
53
|
}
|
|
53
54
|
try {
|
|
54
|
-
const rawFlowSummaries = await this.requestObject('flows/'
|
|
55
|
+
const rawFlowSummaries = await this.requestObject(path('flows/:flowId/summaries', { flowId }), parameters, FlowSummary.parse);
|
|
55
56
|
if (rawFlowSummaries.getData() === null) {
|
|
56
57
|
return null;
|
|
57
58
|
}
|
|
@@ -66,7 +67,7 @@ export class FlowEndpoint extends Endpoint {
|
|
|
66
67
|
}
|
|
67
68
|
async save(flow) {
|
|
68
69
|
try {
|
|
69
|
-
const url = '/projects/'
|
|
70
|
+
const url = path('/projects/:projectId/flows', { projectId: flow.projectId });
|
|
70
71
|
const result = await this.requestObject(url, flow, Flow.parse, 'POST');
|
|
71
72
|
const updatedFlow = result.getData();
|
|
72
73
|
if (updatedFlow === null) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { path } from '../../Http/Data/Path.js';
|
|
1
2
|
import { FlowRun } from '../Model/FlowRun.js';
|
|
2
3
|
import { FlowRunSummary } from '../Model/FlowRunSummary.js';
|
|
3
4
|
import { FlowRunHistory } from '../Model/FlowRunHistory.js';
|
|
@@ -6,12 +7,12 @@ import { QueryString } from '../../Http/Data/QueryString.js';
|
|
|
6
7
|
import { Endpoint } from '../../Service/Endpoint.js';
|
|
7
8
|
export class FlowRunEndpoint extends Endpoint {
|
|
8
9
|
async getFlowRuns(flowId) {
|
|
9
|
-
const result = await this.requestCollection('/flows/'
|
|
10
|
+
const result = await this.requestCollection(path('/flows/:flowId/runs', { flowId }), FlowRun.parse);
|
|
10
11
|
return result;
|
|
11
12
|
}
|
|
12
13
|
async getFlowRunSummaries(flowId, flowRunStatuses = null, pagination = null, projectEnvironmentId = null) {
|
|
13
14
|
try {
|
|
14
|
-
const queryString = new QueryString('/flows/'
|
|
15
|
+
const queryString = new QueryString(path('/flows/:flowId/runsummaries', { flowId }));
|
|
15
16
|
queryString.addPagination(pagination);
|
|
16
17
|
// if (from !== null && from !== undefined) {
|
|
17
18
|
//
|
|
@@ -49,7 +50,7 @@ export class FlowRunEndpoint extends Endpoint {
|
|
|
49
50
|
}
|
|
50
51
|
async getFlowRunSummary(flowRunId) {
|
|
51
52
|
try {
|
|
52
|
-
const result = await this.requestObject('/flowruns/'
|
|
53
|
+
const result = await this.requestObject(path('/flowruns/:flowRunId/summaries', { flowRunId }), null, FlowRunSummary.parse);
|
|
53
54
|
return result.getData();
|
|
54
55
|
}
|
|
55
56
|
catch (error) {
|
|
@@ -67,7 +68,7 @@ export class FlowRunEndpoint extends Endpoint {
|
|
|
67
68
|
*/
|
|
68
69
|
async getFlowRun(flowRunId) {
|
|
69
70
|
try {
|
|
70
|
-
const result = await this.requestObject('/flowruns/'
|
|
71
|
+
const result = await this.requestObject(path('/flowruns/:flowRunId/summaries', { flowRunId }), null, FlowRun.parse);
|
|
71
72
|
return result.getData();
|
|
72
73
|
}
|
|
73
74
|
catch (error) {
|
|
@@ -78,7 +79,7 @@ export class FlowRunEndpoint extends Endpoint {
|
|
|
78
79
|
}
|
|
79
80
|
}
|
|
80
81
|
async getFlowRunHistory(flowRunId) {
|
|
81
|
-
const result = await this.requestCollection('/flowruns/'
|
|
82
|
+
const result = await this.requestCollection(path('/flowruns/:flowRunId/history', { flowRunId }), FlowRunHistory.parse);
|
|
82
83
|
return result;
|
|
83
84
|
}
|
|
84
85
|
/**
|
|
@@ -87,7 +88,7 @@ export class FlowRunEndpoint extends Endpoint {
|
|
|
87
88
|
* reflects the eventually-consistent index projection which can lag the history.
|
|
88
89
|
*/
|
|
89
90
|
async getFlowRunLifecycle(flowRunId) {
|
|
90
|
-
const result = await this.requestObject('/flowruns/'
|
|
91
|
+
const result = await this.requestObject(path('/flowruns/:flowRunId/lifecycle', { flowRunId }), null, FlowRunLifecycle.parse);
|
|
91
92
|
return result.getData();
|
|
92
93
|
}
|
|
93
94
|
async updateFlowRun(flowRunId, status, time = null) {
|
|
@@ -101,7 +102,7 @@ export class FlowRunEndpoint extends Endpoint {
|
|
|
101
102
|
if (time !== null && time !== undefined) {
|
|
102
103
|
params.time = time;
|
|
103
104
|
}
|
|
104
|
-
const result = await this.requestObject('/flowruns
|
|
105
|
+
const result = await this.requestObject(path('/flowruns/:flowRunId', { flowRunId }), params, FlowRun.parse, 'POST');
|
|
105
106
|
const updatedFlowRun = result.getData();
|
|
106
107
|
if (updatedFlowRun === null) {
|
|
107
108
|
throw new Error('FlowRun not updated');
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import { path } from '../../Http/Data/Path.js';
|
|
1
2
|
import { FlowRunResponse } from '../Model/FlowRunResponse.js';
|
|
2
3
|
import { Endpoint } from '../../Service/Endpoint.js';
|
|
3
4
|
export class FlowRunRequestEndpoint extends Endpoint {
|
|
4
5
|
async requestFlowRun(flowId, params, wait = false) {
|
|
5
|
-
let url = '/flows/'
|
|
6
|
+
let url = path('/flows/:flowId/flowrunrequests', { flowId });
|
|
6
7
|
if (wait) {
|
|
7
8
|
url += '?wait=true';
|
|
8
9
|
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import { path } from '../../Http/Data/Path.js';
|
|
1
2
|
import { FlowRunStats } from '../Model/FlowRunStats.js';
|
|
2
3
|
import { Endpoint } from '../../Service/Endpoint.js';
|
|
3
4
|
export class FlowRunStatsEndpoint extends Endpoint {
|
|
4
5
|
async create(flowRunStats) {
|
|
5
|
-
const url = '/flowruns/'
|
|
6
|
+
const url = path('/flowruns/:flowRunId/stats', { flowRunId: flowRunStats.flowRunId });
|
|
6
7
|
try {
|
|
7
8
|
const result = await this.requestObject(url, flowRunStats, FlowRunStats.parse, 'POST');
|
|
8
9
|
const createdFlowRunStat = result.getData();
|
|
@@ -10,7 +10,13 @@ export class LoadAllHelper {
|
|
|
10
10
|
firstPagination.startingAfter = lastRef;
|
|
11
11
|
const data = await call(firstPagination);
|
|
12
12
|
const records = data.getData();
|
|
13
|
-
//
|
|
13
|
+
// An empty page that still claims hasMore cannot be paged past: the cursor advances from
|
|
14
|
+
// the last record, so the next call would repeat this one forever. The server contradicted
|
|
15
|
+
// itself — say so rather than spin.
|
|
16
|
+
if (records.length === 0 && data.hasMore) {
|
|
17
|
+
throw new Error('loadAll stopped after ' + totalResult.length + ' records: the API reported more results but returned an empty page, so paging cannot continue.');
|
|
18
|
+
}
|
|
19
|
+
// Spread one page at a time (bounded by `limit`), so the argument count stays small.
|
|
14
20
|
if (totalResult.length + records.length > 50_000) {
|
|
15
21
|
throw new Error('loadAll exceeded 50,000 records (' + (totalResult.length + records.length) + '). This method is not intended for large datasets.');
|
|
16
22
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build a request path from a template, encoding every value that goes into it.
|
|
3
|
+
*
|
|
4
|
+
* path('/data-quality/datasets/:datasetId/scans', {datasetId})
|
|
5
|
+
*
|
|
6
|
+
* Ids are opaque strings supplied by the caller. Concatenating them straight into a URL means a
|
|
7
|
+
* value containing `/`, `?` or `#` silently changes which endpoint is called; an empty one produces
|
|
8
|
+
* a path that quietly addresses the collection instead of the item. Encoding here makes the request
|
|
9
|
+
* mean what it says, and an empty value an error rather than a surprise.
|
|
10
|
+
*/
|
|
11
|
+
export declare function path(template: string, parameters?: Record<string, string | number>): string;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build a request path from a template, encoding every value that goes into it.
|
|
3
|
+
*
|
|
4
|
+
* path('/data-quality/datasets/:datasetId/scans', {datasetId})
|
|
5
|
+
*
|
|
6
|
+
* Ids are opaque strings supplied by the caller. Concatenating them straight into a URL means a
|
|
7
|
+
* value containing `/`, `?` or `#` silently changes which endpoint is called; an empty one produces
|
|
8
|
+
* a path that quietly addresses the collection instead of the item. Encoding here makes the request
|
|
9
|
+
* mean what it says, and an empty value an error rather than a surprise.
|
|
10
|
+
*/
|
|
11
|
+
export function path(template, parameters = {}) {
|
|
12
|
+
const used = new Set();
|
|
13
|
+
const built = template.replace(/:([A-Za-z][A-Za-z0-9_]*)/g, (_match, name) => {
|
|
14
|
+
if (!Object.prototype.hasOwnProperty.call(parameters, name)) {
|
|
15
|
+
throw new Error('Unable to build path "' + template + '": no value for ":' + name + '"');
|
|
16
|
+
}
|
|
17
|
+
used.add(name);
|
|
18
|
+
const value = String(parameters[name]);
|
|
19
|
+
if (value === '') {
|
|
20
|
+
throw new Error('Unable to build path "' + template + '": ":' + name + '" is empty');
|
|
21
|
+
}
|
|
22
|
+
return encodeURIComponent(value);
|
|
23
|
+
});
|
|
24
|
+
// A value with no matching placeholder is a typo that would otherwise vanish silently.
|
|
25
|
+
for (const name of Object.keys(parameters)) {
|
|
26
|
+
if (!used.has(name)) {
|
|
27
|
+
throw new Error('Unable to build path "' + template + '": no placeholder for "' + name + '"');
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return built;
|
|
31
|
+
}
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import { HttpClientRequest } from './HttpClientRequest.js';
|
|
2
2
|
import { HttpClientResponse } from './HttpClientResponse.js';
|
|
3
3
|
export declare class HttpClient {
|
|
4
|
-
|
|
4
|
+
/**
|
|
5
|
+
* Overall budget for one request, in milliseconds. Matches the Stripe SDKs (80s), which is
|
|
6
|
+
* generous enough for a large payload and bounded enough that an unresponsive API surfaces as an
|
|
7
|
+
* error instead of hanging the caller. `fetch` applies no timeout of its own in the browser, and
|
|
8
|
+
* only bounds response headers in Node.
|
|
9
|
+
*/
|
|
10
|
+
static readonly DEFAULT_TIMEOUT_MS: number;
|
|
11
|
+
static request(request: HttpClientRequest, timeoutMs?: number): Promise<HttpClientResponse>;
|
|
5
12
|
}
|
package/dist/Http/HttpClient.js
CHANGED
|
@@ -1,11 +1,21 @@
|
|
|
1
1
|
import { HttpClientResponse } from './HttpClientResponse.js';
|
|
2
2
|
import { ClientError } from './ClientError.js';
|
|
3
3
|
export class HttpClient {
|
|
4
|
-
|
|
4
|
+
/**
|
|
5
|
+
* Overall budget for one request, in milliseconds. Matches the Stripe SDKs (80s), which is
|
|
6
|
+
* generous enough for a large payload and bounded enough that an unresponsive API surfaces as an
|
|
7
|
+
* error instead of hanging the caller. `fetch` applies no timeout of its own in the browser, and
|
|
8
|
+
* only bounds response headers in Node.
|
|
9
|
+
*/
|
|
10
|
+
static DEFAULT_TIMEOUT_MS = 80_000;
|
|
11
|
+
static async request(request, timeoutMs = HttpClient.DEFAULT_TIMEOUT_MS) {
|
|
5
12
|
const init = {
|
|
6
13
|
method: request.method,
|
|
7
14
|
headers: request.headers,
|
|
8
15
|
};
|
|
16
|
+
if (timeoutMs > 0) {
|
|
17
|
+
init.signal = AbortSignal.timeout(timeoutMs);
|
|
18
|
+
}
|
|
9
19
|
// Node's fetch (undici) throws TypeError when GET/HEAD carry a body. Axios used to silently drop it,
|
|
10
20
|
// so callers may still set body on GET — skip it here instead of propagating a confusing network error.
|
|
11
21
|
const method = request.method.toUpperCase();
|
|
@@ -48,6 +58,16 @@ export class HttpClient {
|
|
|
48
58
|
if (error instanceof ClientError) {
|
|
49
59
|
throw error;
|
|
50
60
|
}
|
|
61
|
+
// AbortSignal.timeout rejects with a TimeoutError DOMException, whose message ("The
|
|
62
|
+
// operation was aborted due to timeout") says nothing about what was being called. Name
|
|
63
|
+
// the url and the budget instead.
|
|
64
|
+
//
|
|
65
|
+
// Matched on `name` rather than `instanceof`: a DOMException raised in another realm — a
|
|
66
|
+
// test runner's VM context, an iframe — is not an instance of this realm's Error, so an
|
|
67
|
+
// instanceof check passes locally and quietly fails everywhere else.
|
|
68
|
+
if (typeof error === 'object' && error !== null && error.name === 'TimeoutError') {
|
|
69
|
+
throw new ClientError('Request to `' + request.getFullUrl() + '` timed out after ' + timeoutMs + 'ms');
|
|
70
|
+
}
|
|
51
71
|
throw ClientError.fromError(error);
|
|
52
72
|
}
|
|
53
73
|
}
|
|
@@ -7,5 +7,7 @@ export declare class OAuthClientOptions {
|
|
|
7
7
|
redirectUri: string;
|
|
8
8
|
scopes: string[];
|
|
9
9
|
state: string;
|
|
10
|
+
/** Overall budget for one request, in milliseconds. Set 0 to disable (not recommended). */
|
|
11
|
+
timeoutMs: number;
|
|
10
12
|
constructor(apiEndpoint: string, clientId?: string | null, clientSecret?: string | null);
|
|
11
13
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { HttpClient } from './HttpClient.js';
|
|
1
2
|
export class OAuthClientOptions {
|
|
2
3
|
apiEndpoint;
|
|
3
4
|
clientId;
|
|
@@ -7,6 +8,8 @@ export class OAuthClientOptions {
|
|
|
7
8
|
redirectUri = 'https://example.com/auth/github/callback';
|
|
8
9
|
scopes = [];
|
|
9
10
|
state = '';
|
|
11
|
+
/** Overall budget for one request, in milliseconds. Set 0 to disable (not recommended). */
|
|
12
|
+
timeoutMs = HttpClient.DEFAULT_TIMEOUT_MS;
|
|
10
13
|
constructor(apiEndpoint,
|
|
11
14
|
// Mutable so client credentials can be updated in place (see OAuthClient.setClientCredentials)
|
|
12
15
|
// without recreating the client and losing its token.
|
|
@@ -49,6 +49,13 @@ export declare class OAuthClient implements ITransport {
|
|
|
49
49
|
setParseErrorHandler(handler: ParseErrorHandler | null): void;
|
|
50
50
|
reportParseError(message: string, context: Record<string, unknown>): void;
|
|
51
51
|
private defaultHeaders;
|
|
52
|
+
/**
|
|
53
|
+
* A copy, not the live object. Returning the internal map let callers mutate it by accident: a
|
|
54
|
+
* request assigned it and then set its own headers, so `Authorization` (and `Attlaz-API-Version`)
|
|
55
|
+
* were written into the shared defaults and sent on every later request — including the ones that
|
|
56
|
+
* deliberately pass `signWithOauthToken: false` (password reset, email confirmation, invite
|
|
57
|
+
* lookup, health). {@link addDefaultHeader} is the only way to add a real default.
|
|
58
|
+
*/
|
|
52
59
|
getDefaultHeaders(): Headers;
|
|
53
60
|
private initDefaultHeaders;
|
|
54
61
|
addDefaultHeader(header: string, value: string): void;
|
|
@@ -156,11 +156,31 @@ export class OAuthClient {
|
|
|
156
156
|
* server's HTTP status and, when the body is JSON, its structured error message.
|
|
157
157
|
*/
|
|
158
158
|
async requestToken(params) {
|
|
159
|
-
|
|
159
|
+
// The token endpoint bypasses HttpClient, so it does not inherit its timeout. Without one an
|
|
160
|
+
// unresponsive token endpoint hangs authenticate()/refreshToken() forever — and that is the
|
|
161
|
+
// one call every client makes before it can do anything else.
|
|
162
|
+
const timeoutMs = this.options.timeoutMs;
|
|
163
|
+
const init = {
|
|
160
164
|
method: 'POST',
|
|
161
165
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
162
166
|
body: new URLSearchParams(params).toString(),
|
|
163
|
-
}
|
|
167
|
+
};
|
|
168
|
+
if (timeoutMs > 0) {
|
|
169
|
+
init.signal = AbortSignal.timeout(timeoutMs);
|
|
170
|
+
}
|
|
171
|
+
const tokenUrl = this.getApiEndpointUrl(this.options.accessTokenUri);
|
|
172
|
+
let response;
|
|
173
|
+
try {
|
|
174
|
+
response = await fetch(tokenUrl, init);
|
|
175
|
+
}
|
|
176
|
+
catch (error) {
|
|
177
|
+
// Matched on `name`, not instanceof: a DOMException from another realm (a test runner's VM
|
|
178
|
+
// context, an iframe) is not an instance of this realm's Error. Same reasoning as HttpClient.
|
|
179
|
+
if (typeof error === 'object' && error !== null && error.name === 'TimeoutError') {
|
|
180
|
+
throw new ClientError('Token request to `' + tokenUrl + '` timed out after ' + timeoutMs + 'ms');
|
|
181
|
+
}
|
|
182
|
+
throw ClientError.fromError(error);
|
|
183
|
+
}
|
|
164
184
|
if (!response.ok) {
|
|
165
185
|
const clientError = new ClientError(response.statusText || 'Token request failed', response.status);
|
|
166
186
|
// The token endpoint bypasses HttpClient (and authenticate() bypasses Endpoint.toApiError),
|
|
@@ -208,7 +228,7 @@ export class OAuthClient {
|
|
|
208
228
|
console.debug('[OAuthClient] Request: ' + requestData.method.toUpperCase() + ' ' + requestData.getFullUrl(), { headers: requestData.headers });
|
|
209
229
|
}
|
|
210
230
|
try {
|
|
211
|
-
const response = await HttpClient.request(requestData);
|
|
231
|
+
const response = await HttpClient.request(requestData, this.options.timeoutMs);
|
|
212
232
|
return response.body;
|
|
213
233
|
}
|
|
214
234
|
catch (error) {
|
|
@@ -258,8 +278,15 @@ export class OAuthClient {
|
|
|
258
278
|
console.error(message, context);
|
|
259
279
|
}
|
|
260
280
|
defaultHeaders = {};
|
|
281
|
+
/**
|
|
282
|
+
* A copy, not the live object. Returning the internal map let callers mutate it by accident: a
|
|
283
|
+
* request assigned it and then set its own headers, so `Authorization` (and `Attlaz-API-Version`)
|
|
284
|
+
* were written into the shared defaults and sent on every later request — including the ones that
|
|
285
|
+
* deliberately pass `signWithOauthToken: false` (password reset, email confirmation, invite
|
|
286
|
+
* lookup, health). {@link addDefaultHeader} is the only way to add a real default.
|
|
287
|
+
*/
|
|
261
288
|
getDefaultHeaders() {
|
|
262
|
-
return this.defaultHeaders;
|
|
289
|
+
return { ...this.defaultHeaders };
|
|
263
290
|
}
|
|
264
291
|
initDefaultHeaders() {
|
|
265
292
|
const userAgentInfo = {
|
|
@@ -11,7 +11,11 @@ export declare class LogEndpoint extends Endpoint {
|
|
|
11
11
|
getLogs(logQuery: LogQuery): Promise<CollectionResult<Log>>;
|
|
12
12
|
getCursoredLogEntries(logStreamId: LogStreamId, pagination: CursorPagination | null, filter: FilterNode | null): Promise<CollectionResult<Log>>;
|
|
13
13
|
save(log: Log): Promise<Log>;
|
|
14
|
-
|
|
14
|
+
/**
|
|
15
|
+
* The stream id was missing here: the path was built with the literal text `:streamId`, so every
|
|
16
|
+
* call requested `/logstreams/:streamId/logs/<id>` and could never have matched the route.
|
|
17
|
+
*/
|
|
18
|
+
delete(logStreamId: string, logId: string): Promise<boolean>;
|
|
15
19
|
clear(logStreamId: LogStreamId): Promise<boolean>;
|
|
16
20
|
getInformation(logStreamId: LogStreamId): Promise<LogStreamInformation | null>;
|
|
17
21
|
getByProject(projectId: string, pagination?: CursorPagination | null): Promise<CollectionResult<LogStream>>;
|