@redocly/openapi-core 1.0.0-beta.67 → 1.0.0-beta.71
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/__tests__/__snapshots__/bundle.test.ts.snap +126 -0
- package/__tests__/bundle.test.ts +53 -1
- package/__tests__/fixtures/refs/definitions.yaml +3 -0
- package/__tests__/fixtures/refs/external-request-body.yaml +13 -0
- package/__tests__/fixtures/refs/externalref.yaml +35 -0
- package/__tests__/fixtures/refs/hosted.yaml +35 -0
- package/__tests__/fixtures/refs/rename.yaml +1 -0
- package/__tests__/fixtures/refs/requestBody.yaml +9 -0
- package/__tests__/fixtures/refs/simple.yaml +1 -0
- package/__tests__/fixtures/refs/vendor.schema.yaml +20 -0
- package/__tests__/lint.test.ts +1 -1
- package/__tests__/login.test.ts +17 -0
- package/lib/bundle.d.ts +4 -0
- package/lib/bundle.js +25 -7
- package/lib/config/all.js +2 -0
- package/lib/config/config.d.ts +10 -0
- package/lib/config/config.js +7 -1
- package/lib/config/load.js +17 -8
- package/lib/index.d.ts +2 -2
- package/lib/lint.js +2 -0
- package/lib/redocly/index.d.ts +26 -20
- package/lib/redocly/index.js +83 -214
- package/lib/redocly/registry-api-types.d.ts +28 -0
- package/lib/redocly/registry-api-types.js +2 -0
- package/lib/redocly/registry-api.d.ts +14 -0
- package/lib/redocly/registry-api.js +105 -0
- package/lib/ref-utils.js +1 -2
- package/lib/rules/common/no-invalid-parameter-examples.d.ts +1 -0
- package/lib/rules/common/no-invalid-parameter-examples.js +25 -0
- package/lib/rules/common/no-invalid-schema-examples.d.ts +1 -0
- package/lib/rules/common/no-invalid-schema-examples.js +23 -0
- package/lib/rules/common/paths-kebab-case.js +1 -1
- package/lib/rules/common/registry-dependencies.js +4 -7
- package/lib/rules/oas2/index.d.ts +2 -0
- package/lib/rules/oas2/index.js +4 -0
- package/lib/rules/oas3/index.js +4 -0
- package/lib/rules/oas3/no-invalid-media-type-examples.js +5 -26
- package/lib/rules/oas3/no-server-trailing-slash.js +1 -1
- package/lib/rules/utils.d.ts +3 -0
- package/lib/rules/utils.js +26 -1
- package/lib/typings/openapi.d.ts +3 -0
- package/lib/utils.d.ts +1 -0
- package/lib/utils.js +5 -1
- package/lib/walk.d.ts +2 -0
- package/lib/walk.js +7 -0
- package/package.json +1 -1
- package/src/bundle.ts +51 -9
- package/src/config/__tests__/load.test.ts +35 -0
- package/src/config/all.ts +2 -0
- package/src/config/config.ts +11 -0
- package/src/config/load.ts +20 -9
- package/src/index.ts +2 -8
- package/src/lint.ts +2 -0
- package/src/redocly/__tests__/redocly-client.test.ts +120 -0
- package/src/redocly/index.ts +100 -227
- package/src/redocly/registry-api-types.ts +31 -0
- package/src/redocly/registry-api.ts +110 -0
- package/src/ref-utils.ts +1 -3
- package/src/rules/common/__tests__/paths-kebab-case.test.ts +23 -0
- package/src/rules/common/no-invalid-parameter-examples.ts +36 -0
- package/src/rules/common/no-invalid-schema-examples.ts +27 -0
- package/src/rules/common/paths-kebab-case.ts +1 -1
- package/src/rules/common/registry-dependencies.ts +6 -8
- package/src/rules/oas2/index.ts +4 -0
- package/src/rules/oas3/__tests__/no-server-trailing-slash.test.ts +19 -0
- package/src/rules/oas3/index.ts +4 -0
- package/src/rules/oas3/no-invalid-media-type-examples.ts +16 -36
- package/src/rules/oas3/no-server-trailing-slash.ts +1 -1
- package/src/rules/utils.ts +43 -2
- package/src/typings/openapi.ts +4 -0
- package/src/utils.ts +5 -1
- package/src/walk.ts +10 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/lib/redocly/query.d.ts +0 -4
- package/lib/redocly/query.js +0 -44
- package/src/redocly/query.ts +0 -38
package/src/redocly/index.ts
CHANGED
|
@@ -1,65 +1,115 @@
|
|
|
1
1
|
import { existsSync, readFileSync, writeFileSync, unlinkSync } from 'fs';
|
|
2
2
|
import { resolve } from 'path';
|
|
3
3
|
import { homedir } from 'os';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
4
|
+
import { red, green, gray } from 'colorette';
|
|
5
|
+
import { RegistryApi } from './registry-api';
|
|
6
|
+
import { AccessTokens, DEFAULT_REGION, DOMAINS, Region } from '../config/config';
|
|
7
|
+
import { isNotEmptyObject } from '../utils';
|
|
6
8
|
|
|
7
9
|
const TOKEN_FILENAME = '.redocly-config.json';
|
|
8
10
|
|
|
9
11
|
export class RedoclyClient {
|
|
10
|
-
private
|
|
12
|
+
private accessTokens: AccessTokens = {};
|
|
13
|
+
private region: Region;
|
|
14
|
+
domain: string;
|
|
15
|
+
registryApi: RegistryApi;
|
|
16
|
+
|
|
17
|
+
constructor(region?: Region) {
|
|
18
|
+
this.region = this.loadRegion(region);
|
|
19
|
+
this.loadTokens();
|
|
20
|
+
this.domain = region
|
|
21
|
+
? DOMAINS[region]
|
|
22
|
+
: process.env.REDOCLY_DOMAIN || DOMAINS[DEFAULT_REGION];
|
|
23
|
+
this.registryApi = new RegistryApi(this.accessTokens, this.region);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
loadRegion(region?: Region) {
|
|
27
|
+
if (region && !DOMAINS[region]) {
|
|
28
|
+
process.stdout.write(
|
|
29
|
+
red(`Invalid argument: region in config file.\nGiven: ${green(region)}, choices: "us", "eu".\n`),
|
|
30
|
+
);
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
11
33
|
|
|
12
|
-
|
|
13
|
-
|
|
34
|
+
if (process.env.REDOCLY_DOMAIN) {
|
|
35
|
+
return (Object.keys(DOMAINS).find(
|
|
36
|
+
(region) => DOMAINS[region as Region] === process.env.REDOCLY_DOMAIN,
|
|
37
|
+
) || DEFAULT_REGION) as Region;
|
|
38
|
+
}
|
|
39
|
+
return region || DEFAULT_REGION;
|
|
14
40
|
}
|
|
15
41
|
|
|
16
|
-
|
|
17
|
-
return
|
|
42
|
+
getRegion(): Region {
|
|
43
|
+
return this.region;
|
|
18
44
|
}
|
|
19
45
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
46
|
+
hasTokens(): boolean {
|
|
47
|
+
return isNotEmptyObject(this.accessTokens);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
setAccessTokens(accessTokens: AccessTokens) {
|
|
51
|
+
this.accessTokens = accessTokens;
|
|
52
|
+
}
|
|
25
53
|
|
|
54
|
+
loadTokens(): void {
|
|
26
55
|
const credentialsPath = resolve(homedir(), TOKEN_FILENAME);
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
this.
|
|
56
|
+
const credentials = this.readCredentialsFile(credentialsPath);
|
|
57
|
+
if (isNotEmptyObject(credentials)) {
|
|
58
|
+
this.setAccessTokens({
|
|
59
|
+
...credentials,
|
|
60
|
+
...(credentials.token && !credentials[this.region] && {
|
|
61
|
+
[this.region]: credentials.token
|
|
62
|
+
})
|
|
63
|
+
})
|
|
64
|
+
}
|
|
65
|
+
if (process.env.REDOCLY_AUTHORIZATION) {
|
|
66
|
+
this.setAccessTokens({
|
|
67
|
+
...this.accessTokens,
|
|
68
|
+
[this.region]: process.env.REDOCLY_AUTHORIZATION
|
|
69
|
+
})
|
|
30
70
|
}
|
|
31
71
|
}
|
|
32
72
|
|
|
73
|
+
async getValidTokens(): Promise<{
|
|
74
|
+
region: string;
|
|
75
|
+
token: string;
|
|
76
|
+
valid: boolean;
|
|
77
|
+
}[]> {
|
|
78
|
+
return (await Promise.all(
|
|
79
|
+
Object.entries(this.accessTokens).map(async ([key, value]) => {
|
|
80
|
+
return { region: key, token: value, valid: await this.verifyToken(value, key as Region) }
|
|
81
|
+
})
|
|
82
|
+
)).filter(item => Boolean(item.valid));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async getTokens() {
|
|
86
|
+
return this.hasTokens() ? await this.getValidTokens() : [];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async isAuthorizedWithRedoclyByRegion(): Promise<boolean> {
|
|
90
|
+
if (!this.hasTokens()) return false;
|
|
91
|
+
const accessToken = this.accessTokens[this.region];
|
|
92
|
+
return !!accessToken && await this.verifyToken(accessToken, this.region);
|
|
93
|
+
}
|
|
94
|
+
|
|
33
95
|
async isAuthorizedWithRedocly(): Promise<boolean> {
|
|
34
|
-
return this.
|
|
96
|
+
return this.hasTokens() && isNotEmptyObject(await this.getValidTokens());
|
|
35
97
|
}
|
|
36
98
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
const authDetails = await RedoclyClient.authorize(accessToken, { verbose });
|
|
40
|
-
if (!authDetails) return false;
|
|
41
|
-
return true;
|
|
99
|
+
readCredentialsFile(credentialsPath: string) {
|
|
100
|
+
return existsSync(credentialsPath) ? JSON.parse(readFileSync(credentialsPath, 'utf-8')) : {};
|
|
42
101
|
}
|
|
43
102
|
|
|
44
|
-
async
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
process.stderr.write(
|
|
48
|
-
`${yellow(
|
|
49
|
-
'Warning:',
|
|
50
|
-
)} invalid Redocly API key. Use "npx @redocly/openapi-cli login" to provide your API key\n`,
|
|
51
|
-
);
|
|
52
|
-
return undefined;
|
|
53
|
-
}
|
|
54
|
-
return this.accessToken;
|
|
103
|
+
async verifyToken(accessToken: string, region: Region, verbose: boolean = false): Promise<boolean> {
|
|
104
|
+
if (!accessToken) return false;
|
|
105
|
+
return this.registryApi.authStatus(accessToken, region, verbose);
|
|
55
106
|
}
|
|
56
107
|
|
|
57
108
|
async login(accessToken: string, verbose: boolean = false) {
|
|
58
109
|
const credentialsPath = resolve(homedir(), TOKEN_FILENAME);
|
|
59
110
|
process.stdout.write(gray('\n Logging in...\n'));
|
|
60
111
|
|
|
61
|
-
const authorized = await this.verifyToken(accessToken, verbose);
|
|
62
|
-
|
|
112
|
+
const authorized = await this.verifyToken(accessToken, this.region, verbose);
|
|
63
113
|
if (!authorized) {
|
|
64
114
|
process.stdout.write(
|
|
65
115
|
red('Authorization failed. Please check if you entered a valid API key.\n'),
|
|
@@ -67,11 +117,12 @@ export class RedoclyClient {
|
|
|
67
117
|
process.exit(1);
|
|
68
118
|
}
|
|
69
119
|
|
|
70
|
-
this.accessToken = accessToken;
|
|
71
120
|
const credentials = {
|
|
72
|
-
|
|
121
|
+
...this.readCredentialsFile(credentialsPath),
|
|
122
|
+
[this.region!]: accessToken,
|
|
73
123
|
};
|
|
74
|
-
|
|
124
|
+
this.accessTokens = credentials;
|
|
125
|
+
this.registryApi.setAccessTokens(credentials);
|
|
75
126
|
writeFileSync(credentialsPath, JSON.stringify(credentials, null, 2));
|
|
76
127
|
process.stdout.write(green(' Authorization confirmed. ✅\n\n'));
|
|
77
128
|
}
|
|
@@ -83,198 +134,20 @@ export class RedoclyClient {
|
|
|
83
134
|
}
|
|
84
135
|
process.stdout.write('Logged out from the Redocly account. ✋\n');
|
|
85
136
|
}
|
|
137
|
+
}
|
|
86
138
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
});
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
static async authorize(accessToken: string, options: { queryName?: string; verbose?: boolean }) {
|
|
95
|
-
const { queryName = '', verbose = false } = options;
|
|
96
|
-
try {
|
|
97
|
-
const queryStr = `query ${queryName}{ viewer { id } }`;
|
|
98
|
-
|
|
99
|
-
return await query(queryStr, {}, { Authorization: accessToken });
|
|
100
|
-
} catch (e) {
|
|
101
|
-
if (verbose) console.log(e);
|
|
102
|
-
return null;
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
async updateDependencies(dependencies: string[] | undefined): Promise<void> {
|
|
107
|
-
const definitionId = process.env.DEFINITION;
|
|
108
|
-
const versionId = process.env.DEFINITION;
|
|
109
|
-
const branchId = process.env.BRANCH;
|
|
110
|
-
|
|
111
|
-
if (!definitionId || !versionId || !branchId) return;
|
|
112
|
-
|
|
113
|
-
await this.query(
|
|
114
|
-
`
|
|
115
|
-
mutation UpdateBranchDependenciesFromURLs(
|
|
116
|
-
$urls: [String!]!
|
|
117
|
-
$definitionId: Int!
|
|
118
|
-
$versionId: Int!
|
|
119
|
-
$branchId: Int!
|
|
120
|
-
) {
|
|
121
|
-
updateBranchDependenciesFromURLs(
|
|
122
|
-
definitionId: $definitionId
|
|
123
|
-
versionId: $versionId
|
|
124
|
-
branchId: $branchId
|
|
125
|
-
urls: $urls
|
|
126
|
-
) {
|
|
127
|
-
branchName
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
`,
|
|
131
|
-
{
|
|
132
|
-
urls: dependencies || [],
|
|
133
|
-
definitionId: parseInt(definitionId, 10),
|
|
134
|
-
versionId: parseInt(versionId, 10),
|
|
135
|
-
branchId: parseInt(branchId, 10),
|
|
136
|
-
},
|
|
137
|
-
);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
updateDefinitionVersion(definitionId: number, versionId: number, updatePatch: object): Promise<void> {
|
|
141
|
-
return this.query(`
|
|
142
|
-
mutation UpdateDefinitionVersion($definitionId: Int!, $versionId: Int!, $updatePatch: DefinitionVersionPatch!) {
|
|
143
|
-
updateDefinitionVersionByDefinitionIdAndId(input: {definitionId: $definitionId, id: $versionId, patch: $updatePatch}) {
|
|
144
|
-
definitionVersion {
|
|
145
|
-
...VersionDetails
|
|
146
|
-
__typename
|
|
147
|
-
}
|
|
148
|
-
__typename
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
fragment VersionDetails on DefinitionVersion {
|
|
153
|
-
id
|
|
154
|
-
nodeId
|
|
155
|
-
uuid
|
|
156
|
-
definitionId
|
|
157
|
-
name
|
|
158
|
-
description
|
|
159
|
-
sourceType
|
|
160
|
-
source
|
|
161
|
-
registryAccess
|
|
162
|
-
__typename
|
|
163
|
-
}
|
|
164
|
-
`,
|
|
165
|
-
{
|
|
166
|
-
definitionId,
|
|
167
|
-
versionId,
|
|
168
|
-
updatePatch,
|
|
169
|
-
},
|
|
170
|
-
);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
getOrganizationId(organizationId: string) {
|
|
174
|
-
return this.query(`
|
|
175
|
-
query ($organizationId: String!) {
|
|
176
|
-
organizationById(id: $organizationId) {
|
|
177
|
-
id
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
`, {
|
|
181
|
-
organizationId
|
|
182
|
-
});
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
getDefinitionByName(name: string, organizationId: string) {
|
|
186
|
-
return this.query(`
|
|
187
|
-
query ($name: String!, $organizationId: String!) {
|
|
188
|
-
definition: definitionByOrganizationIdAndName(name: $name, organizationId: $organizationId) {
|
|
189
|
-
id
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
`, {
|
|
193
|
-
name,
|
|
194
|
-
organizationId
|
|
195
|
-
});
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
createDefinition(organizationId: string, name: string) {
|
|
199
|
-
return this.query(`
|
|
200
|
-
mutation CreateDefinition($organizationId: String!, $name: String!) {
|
|
201
|
-
def: createDefinition(input: {organizationId: $organizationId, name: $name }) {
|
|
202
|
-
definition {
|
|
203
|
-
id
|
|
204
|
-
nodeId
|
|
205
|
-
name
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
`, {
|
|
210
|
-
organizationId,
|
|
211
|
-
name
|
|
212
|
-
})
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
createDefinitionVersion(definitionId: string, name: string, sourceType: string, source: any) {
|
|
216
|
-
return this.query(`
|
|
217
|
-
mutation CreateVersion($definitionId: Int!, $name: String!, $sourceType: DvSourceType!, $source: JSON) {
|
|
218
|
-
createDefinitionVersion(input: {definitionId: $definitionId, name: $name, sourceType: $sourceType, source: $source }) {
|
|
219
|
-
definitionVersion {
|
|
220
|
-
id
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
`, {
|
|
225
|
-
definitionId,
|
|
226
|
-
name,
|
|
227
|
-
sourceType,
|
|
228
|
-
source
|
|
229
|
-
});
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
getSignedUrl(organizationId: string, filesHash: string, fileName: string) {
|
|
233
|
-
return this.query(`
|
|
234
|
-
query ($organizationId: String!, $filesHash: String!, $fileName: String!) {
|
|
235
|
-
signFileUploadCLI(organizationId: $organizationId, filesHash: $filesHash, fileName: $fileName) {
|
|
236
|
-
signedFileUrl
|
|
237
|
-
uploadedFilePath
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
`, {
|
|
241
|
-
organizationId,
|
|
242
|
-
filesHash,
|
|
243
|
-
fileName
|
|
244
|
-
})
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
getDefinitionVersion(organizationId: string, definitionName: string, versionName: string) {
|
|
248
|
-
return this.query(`
|
|
249
|
-
query ($organizationId: String!, $definitionName: String!, $versionName: String!) {
|
|
250
|
-
version: definitionVersionByOrganizationDefinitionAndName(organizationId: $organizationId, definitionName: $definitionName, versionName: $versionName) {
|
|
251
|
-
id
|
|
252
|
-
definitionId
|
|
253
|
-
defaultBranch {
|
|
254
|
-
name
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
`, {
|
|
259
|
-
organizationId,
|
|
260
|
-
definitionName,
|
|
261
|
-
versionName
|
|
262
|
-
});
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
static isRegistryURL(link: string): boolean {
|
|
266
|
-
const domain = process.env.REDOCLY_DOMAIN || 'redoc.ly';
|
|
267
|
-
if (!link.startsWith(`https://api.${domain}/registry/`)) return false;
|
|
268
|
-
const registryPath = link.replace(`https://api.${domain}/registry/`, '');
|
|
139
|
+
export function isRedoclyRegistryURL(link: string): boolean {
|
|
140
|
+
const domain = process.env.REDOCLY_DOMAIN || DOMAINS[DEFAULT_REGION];
|
|
141
|
+
if (!link.startsWith(`https://api.${domain}/registry/`)) return false;
|
|
142
|
+
const registryPath = link.replace(`https://api.${domain}/registry/`, '');
|
|
269
143
|
|
|
270
|
-
|
|
144
|
+
const pathParts = registryPath.split('/');
|
|
271
145
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
146
|
+
// we can be sure, that there is job UUID present
|
|
147
|
+
// (org, definition, version, bundle, branch, job, "openapi.yaml" 🤦♂️)
|
|
148
|
+
// so skip this link.
|
|
149
|
+
// FIXME
|
|
150
|
+
if (pathParts.length === 7) return false;
|
|
277
151
|
|
|
278
|
-
|
|
279
|
-
}
|
|
152
|
+
return true;
|
|
280
153
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export namespace RegistryApiTypes {
|
|
2
|
+
interface VersionParams {
|
|
3
|
+
organizationId: string;
|
|
4
|
+
name: string;
|
|
5
|
+
version: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface PrepareFileuploadParams extends VersionParams {
|
|
9
|
+
filesHash: string;
|
|
10
|
+
filename: string;
|
|
11
|
+
isUpsert?: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface PushApiParams extends VersionParams {
|
|
15
|
+
rootFilePath: string;
|
|
16
|
+
filePaths: string[];
|
|
17
|
+
branch?: string;
|
|
18
|
+
isUpsert?: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface PrepareFileuploadOKResponse {
|
|
22
|
+
filePath: string;
|
|
23
|
+
signedUploadUrl: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface NotFoundProblemResponse {
|
|
27
|
+
status: 404;
|
|
28
|
+
title: 'Not Found';
|
|
29
|
+
code: 'ORGANIZATION_NOT_FOUND' | 'API_VERSION_NOT_FOUND';
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import fetch, { RequestInit, HeadersInit } from 'node-fetch';
|
|
2
|
+
import { RegistryApiTypes } from './registry-api-types';
|
|
3
|
+
import { AccessTokens, Region, DEFAULT_REGION, DOMAINS } from '../config/config';
|
|
4
|
+
import { isNotEmptyObject } from '../utils';
|
|
5
|
+
const version = require('../../package.json').version;
|
|
6
|
+
|
|
7
|
+
export class RegistryApi {
|
|
8
|
+
constructor(private accessTokens: AccessTokens, private region: Region) {}
|
|
9
|
+
|
|
10
|
+
get accessToken() {
|
|
11
|
+
return isNotEmptyObject(this.accessTokens) && this.accessTokens[this.region];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
getBaseUrl(region: Region = DEFAULT_REGION) {
|
|
15
|
+
return `https://api.${DOMAINS[region]}/registry`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
setAccessTokens(accessTokens: AccessTokens) {
|
|
19
|
+
this.accessTokens = accessTokens;
|
|
20
|
+
return this;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
private async request(path = '', options: RequestInit = {}, region?: Region) {
|
|
24
|
+
const headers = Object.assign({}, options.headers || {}, { 'x-redocly-cli-version': version });
|
|
25
|
+
if (!headers.hasOwnProperty('authorization')) { throw new Error('Unauthorized'); }
|
|
26
|
+
const response = await fetch(`${this.getBaseUrl(region)}${path}`, Object.assign({}, options, { headers }));
|
|
27
|
+
if (response.status === 401) { throw new Error('Unauthorized'); }
|
|
28
|
+
if (response.status === 404) {
|
|
29
|
+
const body: RegistryApiTypes.NotFoundProblemResponse = await response.json();
|
|
30
|
+
throw new Error(body.code);
|
|
31
|
+
}
|
|
32
|
+
return response;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async authStatus(accessToken: string, region: Region, verbose = false) {
|
|
36
|
+
try {
|
|
37
|
+
const response = await this.request('', { headers: { authorization: accessToken }}, region);
|
|
38
|
+
return response.ok;
|
|
39
|
+
} catch (error) {
|
|
40
|
+
if (verbose) {
|
|
41
|
+
console.log(error);
|
|
42
|
+
}
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async prepareFileUpload({
|
|
48
|
+
organizationId,
|
|
49
|
+
name,
|
|
50
|
+
version,
|
|
51
|
+
filesHash,
|
|
52
|
+
filename,
|
|
53
|
+
isUpsert,
|
|
54
|
+
}: RegistryApiTypes.PrepareFileuploadParams): Promise<RegistryApiTypes.PrepareFileuploadOKResponse> {
|
|
55
|
+
const response = await this.request(
|
|
56
|
+
`/${organizationId}/${name}/${version}/prepare-file-upload`,
|
|
57
|
+
{
|
|
58
|
+
method: 'POST',
|
|
59
|
+
headers: {
|
|
60
|
+
'content-type': 'application/json',
|
|
61
|
+
authorization: this.accessToken,
|
|
62
|
+
} as HeadersInit,
|
|
63
|
+
body: JSON.stringify({
|
|
64
|
+
filesHash,
|
|
65
|
+
filename,
|
|
66
|
+
isUpsert,
|
|
67
|
+
}),
|
|
68
|
+
},
|
|
69
|
+
this.region
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
if (response.ok) {
|
|
73
|
+
return response.json();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
throw new Error('Could not prepare file upload');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async pushApi({
|
|
80
|
+
organizationId,
|
|
81
|
+
name,
|
|
82
|
+
version,
|
|
83
|
+
rootFilePath,
|
|
84
|
+
filePaths,
|
|
85
|
+
branch,
|
|
86
|
+
isUpsert,
|
|
87
|
+
}: RegistryApiTypes.PushApiParams) {
|
|
88
|
+
const response = await this.request(`/${organizationId}/${name}/${version}`, {
|
|
89
|
+
method: 'PUT',
|
|
90
|
+
headers: {
|
|
91
|
+
'content-type': 'application/json',
|
|
92
|
+
authorization: this.accessToken
|
|
93
|
+
} as HeadersInit,
|
|
94
|
+
body: JSON.stringify({
|
|
95
|
+
rootFilePath,
|
|
96
|
+
filePaths,
|
|
97
|
+
branch,
|
|
98
|
+
isUpsert,
|
|
99
|
+
}),
|
|
100
|
+
},
|
|
101
|
+
this.region
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
if (response.ok) {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
throw new Error('Could not push api');
|
|
109
|
+
}
|
|
110
|
+
}
|
package/src/ref-utils.ts
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { sep as platformDependentSeparator } from 'path';
|
|
2
|
-
|
|
3
1
|
import { Source } from './resolve';
|
|
4
2
|
import { OasRef } from './typings/openapi';
|
|
5
3
|
|
|
@@ -61,7 +59,7 @@ export function pointerBaseName(pointer: string) {
|
|
|
61
59
|
}
|
|
62
60
|
|
|
63
61
|
export function refBaseName(ref: string) {
|
|
64
|
-
const parts = ref.split(
|
|
62
|
+
const parts = ref.split(/[\/\\]/); // split by '\' and '/'
|
|
65
63
|
return parts[parts.length - 1].split('.')[0];
|
|
66
64
|
}
|
|
67
65
|
|
|
@@ -83,4 +83,27 @@ describe('Oas3 paths-kebab-case', () => {
|
|
|
83
83
|
]
|
|
84
84
|
`);
|
|
85
85
|
});
|
|
86
|
+
|
|
87
|
+
it('should allow trailing slash in path with "paths-kebab-case" rule', async () => {
|
|
88
|
+
const document = parseYamlToDocument(
|
|
89
|
+
outdent`
|
|
90
|
+
openapi: 3.0.0
|
|
91
|
+
paths:
|
|
92
|
+
/some/:
|
|
93
|
+
get:
|
|
94
|
+
summary: List all pets
|
|
95
|
+
`,
|
|
96
|
+
'foobar.yaml',
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
const results = await lintDocument({
|
|
100
|
+
externalRefResolver: new BaseResolver(),
|
|
101
|
+
document,
|
|
102
|
+
config: makeConfig({
|
|
103
|
+
'paths-kebab-case': 'error',
|
|
104
|
+
'no-path-trailing-slash': 'off',
|
|
105
|
+
}),
|
|
106
|
+
});
|
|
107
|
+
expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`Array []`);
|
|
108
|
+
});
|
|
86
109
|
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { UserContext } from '../../walk';
|
|
2
|
+
import { Oas3Parameter } from '../../typings/openapi';
|
|
3
|
+
import { validateExample } from '../utils';
|
|
4
|
+
|
|
5
|
+
export const NoInvalidParameterExamples: any = (opts: any) => {
|
|
6
|
+
const disallowAdditionalProperties = opts.disallowAdditionalProperties ?? true;
|
|
7
|
+
return {
|
|
8
|
+
Parameter: {
|
|
9
|
+
leave(parameter: Oas3Parameter, ctx: UserContext) {
|
|
10
|
+
if (parameter.example) {
|
|
11
|
+
validateExample(
|
|
12
|
+
parameter.example,
|
|
13
|
+
parameter.schema!,
|
|
14
|
+
ctx.location.child('example'),
|
|
15
|
+
ctx,
|
|
16
|
+
disallowAdditionalProperties,
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (parameter.examples) {
|
|
21
|
+
for (const [key, example] of Object.entries(parameter.examples)) {
|
|
22
|
+
if ('value' in example) {
|
|
23
|
+
validateExample(
|
|
24
|
+
example.value,
|
|
25
|
+
parameter.schema!,
|
|
26
|
+
ctx.location.child(['examples', key]),
|
|
27
|
+
ctx,
|
|
28
|
+
false,
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { UserContext } from '../../walk';
|
|
2
|
+
import { Oas3_1Schema } from '../../typings/openapi';
|
|
3
|
+
import { validateExample } from '../utils';
|
|
4
|
+
|
|
5
|
+
export const NoInvalidSchemaExamples: any = (opts: any) => {
|
|
6
|
+
const disallowAdditionalProperties = opts.disallowAdditionalProperties ?? true;
|
|
7
|
+
return {
|
|
8
|
+
Schema: {
|
|
9
|
+
leave(schema: Oas3_1Schema, ctx: UserContext) {
|
|
10
|
+
if (schema.examples) {
|
|
11
|
+
for (const example of schema.examples) {
|
|
12
|
+
validateExample(
|
|
13
|
+
example,
|
|
14
|
+
schema,
|
|
15
|
+
ctx.location.child(['examples', schema.examples.indexOf(example)]),
|
|
16
|
+
ctx,
|
|
17
|
+
disallowAdditionalProperties,
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
if (schema.example) {
|
|
22
|
+
validateExample(schema.example, schema, ctx.location.child('example'), ctx, false);
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
};
|
|
@@ -4,7 +4,7 @@ import { UserContext } from '../../walk';
|
|
|
4
4
|
export const PathsKebabCase: Oas3Rule | Oas2Rule = () => {
|
|
5
5
|
return {
|
|
6
6
|
PathItem(_path: object, { report, key }: UserContext) {
|
|
7
|
-
const segments = (key as string).substr(1).split('/');
|
|
7
|
+
const segments = (key as string).substr(1).split('/').filter(s => s !== ''); // filter out empty segments
|
|
8
8
|
if (!segments.every((segment) => /^{.+}$/.test(segment) || /^[a-z0-9-.]+$/.test(segment))) {
|
|
9
9
|
report({
|
|
10
10
|
message: `\`${key}\` does not use kebab-case.`,
|
|
@@ -1,24 +1,22 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { UserContext } from '../../walk';
|
|
2
|
+
import { isRedoclyRegistryURL } from '../../redocly';
|
|
2
3
|
|
|
3
4
|
import { Oas3Decorator, Oas2Decorator } from '../../visitors';
|
|
4
5
|
|
|
5
6
|
export const RegistryDependencies: Oas3Decorator | Oas2Decorator = () => {
|
|
6
|
-
let redoclyClient: RedoclyClient;
|
|
7
7
|
let registryDependencies = new Set<string>();
|
|
8
8
|
|
|
9
9
|
return {
|
|
10
10
|
DefinitionRoot: {
|
|
11
|
-
leave() {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
redoclyClient.updateDependencies(Array.from(registryDependencies.keys()));
|
|
15
|
-
}
|
|
11
|
+
leave(_: any, ctx: UserContext) {
|
|
12
|
+
const data = ctx.getVisitorData();
|
|
13
|
+
data.links = Array.from(registryDependencies);
|
|
16
14
|
},
|
|
17
15
|
},
|
|
18
16
|
ref(node) {
|
|
19
17
|
if (node.$ref) {
|
|
20
18
|
const link = node.$ref.split('#/')[0];
|
|
21
|
-
if (
|
|
19
|
+
if (isRedoclyRegistryURL(link)) {
|
|
22
20
|
registryDependencies.add(link);
|
|
23
21
|
}
|
|
24
22
|
}
|
package/src/rules/oas2/index.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { OasSpec } from '../common/spec';
|
|
2
|
+
import { NoInvalidSchemaExamples } from '../common/no-invalid-schema-examples';
|
|
3
|
+
import { NoInvalidParameterExamples } from '../common/no-invalid-parameter-examples';
|
|
2
4
|
import { InfoDescription } from '../common/info-description';
|
|
3
5
|
import { InfoContact } from '../common/info-contact';
|
|
4
6
|
import { InfoLicense } from '../common/info-license-url';
|
|
@@ -41,6 +43,8 @@ import { InfoDescriptionOverride } from '../common/info-description-override';
|
|
|
41
43
|
|
|
42
44
|
export const rules = {
|
|
43
45
|
spec: OasSpec as Oas2Rule,
|
|
46
|
+
'no-invalid-schema-examples': NoInvalidSchemaExamples,
|
|
47
|
+
'no-invalid-parameter-examples': NoInvalidParameterExamples,
|
|
44
48
|
'info-description': InfoDescription as Oas2Rule,
|
|
45
49
|
'info-contact': InfoContact as Oas2Rule,
|
|
46
50
|
'info-license': InfoLicense as Oas2Rule,
|