@redocly/openapi-core 1.0.0-beta.87 → 1.0.0-beta.90
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__/codeframes.test.ts +22 -0
- package/lib/config/config.d.ts +46 -4
- package/lib/config/config.js +66 -6
- package/lib/config/load.d.ts +4 -3
- package/lib/config/load.js +22 -16
- package/lib/format/codeframes.js +5 -2
- package/lib/index.d.ts +3 -3
- package/lib/index.js +4 -1
- package/lib/redocly/index.d.ts +4 -1
- package/lib/redocly/index.js +28 -19
- package/lib/redocly/registry-api.d.ts +4 -1
- package/lib/redocly/registry-api.js +3 -3
- package/lib/rules/oas3/no-empty-servers.js +2 -1
- package/lib/types/redocly-yaml.js +31 -10
- package/package.json +1 -1
- package/src/__tests__/lint.test.ts +40 -6
- package/src/config/__tests__/load.test.ts +7 -0
- package/src/config/config.ts +104 -16
- package/src/config/load.ts +18 -20
- package/src/format/codeframes.ts +4 -2
- package/src/index.ts +11 -3
- package/src/redocly/index.ts +49 -30
- package/src/redocly/registry-api.ts +38 -21
- package/src/rules/oas3/__tests__/spec/spec.test.ts +4 -4
- package/src/rules/oas3/no-empty-servers.ts +2 -1
- package/src/types/redocly-yaml.ts +38 -10
- package/src/utils.ts +0 -1
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -46,7 +46,7 @@ describe('lint', () => {
|
|
|
46
46
|
it('lintConfig should work', async () => {
|
|
47
47
|
const document = parseYamlToDocument(
|
|
48
48
|
outdent`
|
|
49
|
-
|
|
49
|
+
apis: error string
|
|
50
50
|
lint:
|
|
51
51
|
plugins:
|
|
52
52
|
- './local-plugin.js'
|
|
@@ -58,7 +58,7 @@ describe('lint', () => {
|
|
|
58
58
|
no-invalid-media-type-examples: error
|
|
59
59
|
path-http-verbs-order: error
|
|
60
60
|
boolean-parameter-prefixes: off
|
|
61
|
-
|
|
61
|
+
features.openapi:
|
|
62
62
|
showConsole: true
|
|
63
63
|
layout:
|
|
64
64
|
scope: section
|
|
@@ -78,12 +78,12 @@ describe('lint', () => {
|
|
|
78
78
|
Object {
|
|
79
79
|
"location": Array [
|
|
80
80
|
Object {
|
|
81
|
-
"pointer": "#/
|
|
81
|
+
"pointer": "#/apis",
|
|
82
82
|
"reportOnKey": false,
|
|
83
83
|
"source": "",
|
|
84
84
|
},
|
|
85
85
|
],
|
|
86
|
-
"message": "Expected type \`
|
|
86
|
+
"message": "Expected type \`ConfigApis\` (object) but got \`string\`",
|
|
87
87
|
"ruleId": "spec",
|
|
88
88
|
"severity": "error",
|
|
89
89
|
"suggest": Array [],
|
|
@@ -91,7 +91,7 @@ describe('lint', () => {
|
|
|
91
91
|
Object {
|
|
92
92
|
"location": Array [
|
|
93
93
|
Object {
|
|
94
|
-
"pointer": "#/
|
|
94
|
+
"pointer": "#/features.openapi/layout",
|
|
95
95
|
"reportOnKey": false,
|
|
96
96
|
"source": "",
|
|
97
97
|
},
|
|
@@ -105,6 +105,40 @@ describe('lint', () => {
|
|
|
105
105
|
`);
|
|
106
106
|
});
|
|
107
107
|
|
|
108
|
+
it("'plugins' shouldn't be allowed in 'apis' -> 'lint' field", async () => {
|
|
109
|
+
const document = parseYamlToDocument(
|
|
110
|
+
outdent`
|
|
111
|
+
apis:
|
|
112
|
+
lint:
|
|
113
|
+
plugins:
|
|
114
|
+
- './local-plugin.js'
|
|
115
|
+
lint:
|
|
116
|
+
plugins:
|
|
117
|
+
- './local-plugin.js'
|
|
118
|
+
`,
|
|
119
|
+
'',
|
|
120
|
+
);
|
|
121
|
+
const results = await lintConfig({ document });
|
|
122
|
+
|
|
123
|
+
expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`
|
|
124
|
+
Array [
|
|
125
|
+
Object {
|
|
126
|
+
"location": Array [
|
|
127
|
+
Object {
|
|
128
|
+
"pointer": "#/apis/lint/plugins",
|
|
129
|
+
"reportOnKey": true,
|
|
130
|
+
"source": "",
|
|
131
|
+
},
|
|
132
|
+
],
|
|
133
|
+
"message": "Property \`plugins\` is not expected here.",
|
|
134
|
+
"ruleId": "spec",
|
|
135
|
+
"severity": "error",
|
|
136
|
+
"suggest": Array [],
|
|
137
|
+
},
|
|
138
|
+
]
|
|
139
|
+
`);
|
|
140
|
+
});
|
|
141
|
+
|
|
108
142
|
it("'const' can have any type", async () => {
|
|
109
143
|
const document = parseYamlToDocument(
|
|
110
144
|
outdent`
|
|
@@ -140,7 +174,7 @@ describe('lint', () => {
|
|
|
140
174
|
const results = await lintDocument({
|
|
141
175
|
externalRefResolver: new BaseResolver(),
|
|
142
176
|
document,
|
|
143
|
-
config: makeConfig({ spec: 'error'
|
|
177
|
+
config: makeConfig({ spec: 'error' }),
|
|
144
178
|
});
|
|
145
179
|
|
|
146
180
|
expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`Array []`);
|
|
@@ -2,6 +2,7 @@ import { loadConfig, findConfig } from '../load';
|
|
|
2
2
|
import { RedoclyClient } from '../../redocly';
|
|
3
3
|
|
|
4
4
|
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
5
6
|
|
|
6
7
|
describe('loadConfig', () => {
|
|
7
8
|
it('should resolve config http header by US region', async () => {
|
|
@@ -66,4 +67,10 @@ describe('findConfig', () => {
|
|
|
66
67
|
Please use 'redocly.yaml' instead.
|
|
67
68
|
`);
|
|
68
69
|
});
|
|
70
|
+
it('should find a nested config ', async () => {
|
|
71
|
+
jest.spyOn(fs, 'existsSync').mockImplementation((name) => name === 'dir/redocly.yaml');
|
|
72
|
+
jest.spyOn(path, 'resolve').mockImplementationOnce((dir, name) => `${dir}/${name}`);
|
|
73
|
+
const configName = findConfig('dir');
|
|
74
|
+
expect(configName).toStrictEqual('dir/redocly.yaml');
|
|
75
|
+
});
|
|
69
76
|
});
|
package/src/config/config.ts
CHANGED
|
@@ -2,10 +2,8 @@ import * as fs from 'fs';
|
|
|
2
2
|
import * as path from 'path';
|
|
3
3
|
import { dirname } from 'path';
|
|
4
4
|
import { red, blue } from 'colorette';
|
|
5
|
-
|
|
6
5
|
import { parseYaml, stringifyYaml } from '../js-yaml';
|
|
7
6
|
import { notUndefined, slash } from '../utils';
|
|
8
|
-
|
|
9
7
|
import {
|
|
10
8
|
OasVersion,
|
|
11
9
|
Oas3PreprocessorsSet,
|
|
@@ -14,11 +12,9 @@ import {
|
|
|
14
12
|
Oas2RuleSet,
|
|
15
13
|
Oas2PreprocessorsSet,
|
|
16
14
|
Oas2DecoratorsSet,
|
|
17
|
-
Oas3RuleSet
|
|
15
|
+
Oas3RuleSet,
|
|
18
16
|
} from '../oas-types';
|
|
19
|
-
|
|
20
17
|
import { ProblemSeverity, NormalizedProblem } from '../walk';
|
|
21
|
-
|
|
22
18
|
import recommended from './recommended';
|
|
23
19
|
import { NodeType } from '../types';
|
|
24
20
|
|
|
@@ -124,13 +120,12 @@ export type ResolveConfig = {
|
|
|
124
120
|
|
|
125
121
|
export const DEFAULT_REGION = 'us';
|
|
126
122
|
export type Region = 'us' | 'eu';
|
|
127
|
-
export type AccessTokens = {[region in Region]?: string };
|
|
123
|
+
export type AccessTokens = { [region in Region]?: string };
|
|
128
124
|
const REDOCLY_DOMAIN = process.env.REDOCLY_DOMAIN;
|
|
129
125
|
export const DOMAINS: { [region in Region]: string } = {
|
|
130
126
|
us: 'redocly.com',
|
|
131
127
|
eu: 'eu.redocly.com',
|
|
132
128
|
};
|
|
133
|
-
export const AVAILABLE_REGIONS = Object.keys(DOMAINS) as Region[];
|
|
134
129
|
|
|
135
130
|
// FIXME: temporary fix for our lab environments
|
|
136
131
|
if (REDOCLY_DOMAIN?.endsWith('.redocly.host')) {
|
|
@@ -139,13 +134,31 @@ if (REDOCLY_DOMAIN?.endsWith('.redocly.host')) {
|
|
|
139
134
|
if (REDOCLY_DOMAIN === 'redoc.online') {
|
|
140
135
|
DOMAINS[REDOCLY_DOMAIN as Region] = REDOCLY_DOMAIN;
|
|
141
136
|
}
|
|
137
|
+
export const AVAILABLE_REGIONS = Object.keys(DOMAINS) as Region[];
|
|
142
138
|
|
|
143
|
-
export type
|
|
144
|
-
referenceDocs?: any;
|
|
139
|
+
export type DeprecatedRawConfig = {
|
|
145
140
|
apiDefinitions?: Record<string, string>;
|
|
146
141
|
lint?: LintRawConfig;
|
|
147
142
|
resolve?: RawResolveConfig;
|
|
148
143
|
region?: Region;
|
|
144
|
+
referenceDocs?: Record<string, any>;
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
export type Api = {
|
|
148
|
+
root: string;
|
|
149
|
+
lint?: Omit<LintRawConfig, 'plugins'>;
|
|
150
|
+
'features.openapi'?: Record<string, any>;
|
|
151
|
+
'features.mockServer'?: Record<string, any>;
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
export type RawConfig = {
|
|
155
|
+
apis?: Record<string, Api>;
|
|
156
|
+
lint?: LintRawConfig;
|
|
157
|
+
resolve?: RawResolveConfig;
|
|
158
|
+
region?: Region;
|
|
159
|
+
'features.openapi'?: Record<string, any>;
|
|
160
|
+
'features.mockServer'?: Record<string, any>;
|
|
161
|
+
organization?: string;
|
|
149
162
|
};
|
|
150
163
|
|
|
151
164
|
export class LintConfig {
|
|
@@ -201,7 +214,9 @@ export class LintConfig {
|
|
|
201
214
|
[OasVersion.Version3_1]: { ...merged.decorators, ...merged.oas3_1Decorators },
|
|
202
215
|
};
|
|
203
216
|
|
|
204
|
-
const dir = this.configFile
|
|
217
|
+
const dir = this.configFile
|
|
218
|
+
? path.dirname(this.configFile)
|
|
219
|
+
: (typeof process !== 'undefined' && process.cwd()) || '';
|
|
205
220
|
const ignoreFile = path.join(dir, IGNORE_FILE);
|
|
206
221
|
|
|
207
222
|
/* no crash when using it on the client */
|
|
@@ -229,7 +244,8 @@ export class LintConfig {
|
|
|
229
244
|
const ignoreFile = path.join(dir, IGNORE_FILE);
|
|
230
245
|
const mapped: Record<string, any> = {};
|
|
231
246
|
for (const absFileName of Object.keys(this.ignore)) {
|
|
232
|
-
const ignoredRules = (mapped[slash(path.relative(dir, absFileName))] =
|
|
247
|
+
const ignoredRules = (mapped[slash(path.relative(dir, absFileName))] =
|
|
248
|
+
this.ignore[absFileName]);
|
|
233
249
|
for (const ruleId of Object.keys(ignoredRules)) {
|
|
234
250
|
ignoredRules[ruleId] = Array.from(ignoredRules[ruleId]) as any;
|
|
235
251
|
}
|
|
@@ -398,16 +414,19 @@ export class LintConfig {
|
|
|
398
414
|
}
|
|
399
415
|
|
|
400
416
|
export class Config {
|
|
401
|
-
|
|
402
|
-
apiDefinitions: Record<string, string>;
|
|
417
|
+
apis: Record<string, Api>;
|
|
403
418
|
lint: LintConfig;
|
|
404
419
|
resolve: ResolveConfig;
|
|
405
420
|
licenseKey?: string;
|
|
406
421
|
region?: Region;
|
|
422
|
+
'features.openapi': Record<string, any>;
|
|
423
|
+
'features.mockServer'?: Record<string, any>;
|
|
424
|
+
organization?: string;
|
|
407
425
|
constructor(public rawConfig: RawConfig, public configFile?: string) {
|
|
408
|
-
this.
|
|
426
|
+
this.apis = rawConfig.apis || {};
|
|
409
427
|
this.lint = new LintConfig(rawConfig.lint || {}, configFile);
|
|
410
|
-
this.
|
|
428
|
+
this['features.openapi'] = rawConfig['features.openapi'] || {};
|
|
429
|
+
this['features.mockServer'] = rawConfig['features.mockServer'] || {};
|
|
411
430
|
this.resolve = {
|
|
412
431
|
http: {
|
|
413
432
|
headers: rawConfig?.resolve?.http?.headers ?? [],
|
|
@@ -415,13 +434,13 @@ export class Config {
|
|
|
415
434
|
},
|
|
416
435
|
};
|
|
417
436
|
this.region = rawConfig.region;
|
|
437
|
+
this.organization = rawConfig.organization;
|
|
418
438
|
}
|
|
419
439
|
}
|
|
420
440
|
|
|
421
441
|
function resolvePresets(presets: string[], plugins: Plugin[]) {
|
|
422
442
|
return presets.map((presetName) => {
|
|
423
443
|
const { pluginId, configName } = parsePresetName(presetName);
|
|
424
|
-
|
|
425
444
|
const plugin = plugins.find((p) => p.id === pluginId);
|
|
426
445
|
if (!plugin) {
|
|
427
446
|
throw new Error(`Invalid config ${red(presetName)}: plugin ${pluginId} is not included.`);
|
|
@@ -624,3 +643,72 @@ function assignExisting<T>(target: Record<string, T>, obj: Record<string, T>) {
|
|
|
624
643
|
}
|
|
625
644
|
}
|
|
626
645
|
}
|
|
646
|
+
|
|
647
|
+
export function getMergedConfig(config: Config, entrypointAlias?: string): Config {
|
|
648
|
+
return entrypointAlias
|
|
649
|
+
? new Config({
|
|
650
|
+
...config.rawConfig,
|
|
651
|
+
lint: getMergedLintConfig(config, entrypointAlias),
|
|
652
|
+
'features.openapi': {
|
|
653
|
+
...config['features.openapi'],
|
|
654
|
+
...config.apis[entrypointAlias]?.['features.openapi'],
|
|
655
|
+
},
|
|
656
|
+
'features.mockServer': {
|
|
657
|
+
...config['features.mockServer'],
|
|
658
|
+
...config.apis[entrypointAlias]?.['features.mockServer'],
|
|
659
|
+
},
|
|
660
|
+
// TODO: merge everything else here
|
|
661
|
+
})
|
|
662
|
+
: config;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
export function getMergedLintConfig(config: Config, entrypointAlias?: string) {
|
|
666
|
+
const apiLint = entrypointAlias ? config.apis[entrypointAlias]?.lint : {};
|
|
667
|
+
const mergedLint = {
|
|
668
|
+
...config.rawConfig.lint,
|
|
669
|
+
...apiLint,
|
|
670
|
+
rules: { ...config.rawConfig.lint?.rules, ...apiLint?.rules },
|
|
671
|
+
preprocessors: { ...config.rawConfig.lint?.preprocessors, ...apiLint?.preprocessors },
|
|
672
|
+
decorators: { ...config.rawConfig.lint?.decorators, ...apiLint?.decorators },
|
|
673
|
+
};
|
|
674
|
+
return mergedLint;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
function transformApiDefinitionsToApis(
|
|
678
|
+
apiDefinitions: Record<string, string> = {},
|
|
679
|
+
): Record<string, Api> {
|
|
680
|
+
let apis: Record<string, Api> = {};
|
|
681
|
+
for (const [apiName, apiPath] of Object.entries(apiDefinitions)) {
|
|
682
|
+
apis[apiName] = { root: apiPath };
|
|
683
|
+
}
|
|
684
|
+
return apis;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
export function transformConfig(rawConfig: DeprecatedRawConfig | RawConfig): RawConfig {
|
|
688
|
+
if ((rawConfig as RawConfig).apis && (rawConfig as DeprecatedRawConfig).apiDefinitions) {
|
|
689
|
+
throw new Error("Do not use 'apiDefinitions' field. Use 'apis' instead.\n");
|
|
690
|
+
}
|
|
691
|
+
if (
|
|
692
|
+
(rawConfig as RawConfig)['features.openapi'] &&
|
|
693
|
+
(rawConfig as DeprecatedRawConfig).referenceDocs
|
|
694
|
+
) {
|
|
695
|
+
throw new Error("Do not use 'referenceDocs' field. Use 'features.openapi' instead.\n");
|
|
696
|
+
}
|
|
697
|
+
const { apiDefinitions, referenceDocs, ...rest } = rawConfig as DeprecatedRawConfig & RawConfig;
|
|
698
|
+
// TODO: put links to the changelog and uncomment this after successful release of ReferenceDocs/Redoc, Portal and Workflows
|
|
699
|
+
// if (apiDefinitions) {
|
|
700
|
+
// process.stderr.write(
|
|
701
|
+
// `The ${yellow('apiDefinitions')} field is deprecated. Use ${green('apis')} instead, see changelog: <link>\n`
|
|
702
|
+
// );
|
|
703
|
+
// }
|
|
704
|
+
// if (referenceDocs) {
|
|
705
|
+
// process.stderr.write(
|
|
706
|
+
// `The ${yellow('referenceDocs')} field is deprecated. Use ${green('features.openapi')} instead, see changelog: <link>\n`
|
|
707
|
+
// );
|
|
708
|
+
// }
|
|
709
|
+
return {
|
|
710
|
+
'features.openapi': referenceDocs,
|
|
711
|
+
apis: transformApiDefinitionsToApis(apiDefinitions),
|
|
712
|
+
...rest,
|
|
713
|
+
};
|
|
714
|
+
}
|
package/src/config/load.ts
CHANGED
|
@@ -1,23 +1,13 @@
|
|
|
1
1
|
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
2
3
|
import { RedoclyClient } from '../redocly';
|
|
3
4
|
import { loadYaml } from '../utils';
|
|
4
|
-
import { Config, DOMAINS, RawConfig, Region } from './config';
|
|
5
|
-
|
|
5
|
+
import { Config, DOMAINS, RawConfig, Region, transformConfig } from './config';
|
|
6
6
|
import { defaultPlugin } from './builtIn';
|
|
7
7
|
|
|
8
|
-
export async function loadConfig(configPath
|
|
9
|
-
|
|
10
|
-
configPath = findConfig();
|
|
11
|
-
}
|
|
12
|
-
let rawConfig: RawConfig = {};
|
|
8
|
+
export async function loadConfig(configPath: string | undefined = findConfig(), customExtends?: string[]): Promise<Config> {
|
|
9
|
+
const rawConfig = await getConfig(configPath);
|
|
13
10
|
|
|
14
|
-
if (configPath !== undefined) {
|
|
15
|
-
try {
|
|
16
|
-
rawConfig = (await loadYaml(configPath)) as RawConfig;
|
|
17
|
-
} catch (e) {
|
|
18
|
-
throw new Error(`Error parsing config file at \`${configPath}\`: ${e.message}`);
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
11
|
if (customExtends !== undefined) {
|
|
22
12
|
rawConfig.lint = rawConfig.lint || {};
|
|
23
13
|
rawConfig.lint.extends = customExtends;
|
|
@@ -48,7 +38,6 @@ export async function loadConfig(configPath?: string, customExtends?: string[]):
|
|
|
48
38
|
}] : []));
|
|
49
39
|
}
|
|
50
40
|
}
|
|
51
|
-
|
|
52
41
|
return new Config(
|
|
53
42
|
{
|
|
54
43
|
...rawConfig,
|
|
@@ -63,12 +52,11 @@ export async function loadConfig(configPath?: string, customExtends?: string[]):
|
|
|
63
52
|
|
|
64
53
|
export const CONFIG_FILE_NAMES = ['redocly.yaml', 'redocly.yml', '.redocly.yaml', '.redocly.yml'];
|
|
65
54
|
|
|
66
|
-
export function findConfig(): string | undefined {
|
|
55
|
+
export function findConfig(dir?: string): string | undefined {
|
|
67
56
|
if (!fs.hasOwnProperty('existsSync')) return;
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
) as Array<string | never>;
|
|
57
|
+
const existingConfigFiles = CONFIG_FILE_NAMES
|
|
58
|
+
.map(name => dir ? path.resolve(dir, name) : name)
|
|
59
|
+
.filter(fs.existsSync);
|
|
72
60
|
if (existingConfigFiles.length > 1) {
|
|
73
61
|
throw new Error(`
|
|
74
62
|
Multiple configuration files are not allowed.
|
|
@@ -78,3 +66,13 @@ export function findConfig(): string | undefined {
|
|
|
78
66
|
}
|
|
79
67
|
return existingConfigFiles[0];
|
|
80
68
|
}
|
|
69
|
+
|
|
70
|
+
export async function getConfig(configPath: string | undefined = findConfig()) {
|
|
71
|
+
if (!configPath) return {};
|
|
72
|
+
try {
|
|
73
|
+
const rawConfig = ((await loadYaml(configPath)) || {}) as RawConfig;
|
|
74
|
+
return transformConfig(rawConfig);
|
|
75
|
+
} catch (e) {
|
|
76
|
+
throw new Error(`Error parsing config file at '${configPath}': ${e.message}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
package/src/format/codeframes.ts
CHANGED
|
@@ -183,8 +183,10 @@ export function getAstNodeByPointer(root: YAMLNode, pointer: string, reportOnKey
|
|
|
183
183
|
for (const key of pointerSegments) {
|
|
184
184
|
if (currentNode.kind === yamlAst.Kind.MAP) {
|
|
185
185
|
const mapping = currentNode.mappings.find((m) => m.key.value === key);
|
|
186
|
-
if (!mapping
|
|
187
|
-
currentNode = mapping
|
|
186
|
+
if (!mapping) break;
|
|
187
|
+
currentNode = mapping as YAMLNode;
|
|
188
|
+
if (!mapping?.value) break; // If node has value - return value, if not - return node itself
|
|
189
|
+
currentNode = mapping.value as YAMLNode;
|
|
188
190
|
} else if (currentNode.kind === yamlAst.Kind.SEQ) {
|
|
189
191
|
const elem = currentNode.items[parseInt(key, 10)] as YAMLNode;
|
|
190
192
|
if (!elem) break;
|
package/src/index.ts
CHANGED
|
@@ -14,16 +14,24 @@ export {
|
|
|
14
14
|
Oas3_1Schema,
|
|
15
15
|
Oas3Tag,
|
|
16
16
|
Oas3_1Webhooks,
|
|
17
|
-
Referenced
|
|
17
|
+
Referenced,
|
|
18
18
|
} from './typings/openapi';
|
|
19
19
|
export { Oas2Definition } from './typings/swagger';
|
|
20
20
|
export { StatsAccumulator, StatsName } from './typings/common';
|
|
21
21
|
export { normalizeTypes } from './types';
|
|
22
22
|
export { Stats } from './rules/other/stats';
|
|
23
23
|
|
|
24
|
-
export {
|
|
24
|
+
export {
|
|
25
|
+
Config,
|
|
26
|
+
LintConfig,
|
|
27
|
+
RawConfig,
|
|
28
|
+
IGNORE_FILE,
|
|
29
|
+
Region,
|
|
30
|
+
getMergedConfig,
|
|
31
|
+
transformConfig,
|
|
32
|
+
} from './config/config';
|
|
25
33
|
|
|
26
|
-
export { loadConfig, findConfig, CONFIG_FILE_NAMES } from './config/load';
|
|
34
|
+
export { loadConfig, getConfig, findConfig, CONFIG_FILE_NAMES } from './config/load';
|
|
27
35
|
export { RedoclyClient, isRedoclyRegistryURL } from './redocly';
|
|
28
36
|
|
|
29
37
|
export {
|
package/src/redocly/index.ts
CHANGED
|
@@ -19,9 +19,7 @@ export class RedoclyClient {
|
|
|
19
19
|
constructor(region?: Region) {
|
|
20
20
|
this.region = this.loadRegion(region);
|
|
21
21
|
this.loadTokens();
|
|
22
|
-
this.domain = region
|
|
23
|
-
? DOMAINS[region]
|
|
24
|
-
: process.env.REDOCLY_DOMAIN || DOMAINS[DEFAULT_REGION];
|
|
22
|
+
this.domain = region ? DOMAINS[region] : process.env.REDOCLY_DOMAIN || DOMAINS[DEFAULT_REGION];
|
|
25
23
|
|
|
26
24
|
/*
|
|
27
25
|
* We can't use process.env here because it is replaced by a const in some client-side bundles,
|
|
@@ -34,7 +32,11 @@ export class RedoclyClient {
|
|
|
34
32
|
loadRegion(region?: Region) {
|
|
35
33
|
if (region && !DOMAINS[region]) {
|
|
36
34
|
process.stdout.write(
|
|
37
|
-
red(
|
|
35
|
+
red(
|
|
36
|
+
`Invalid argument: region in config file.\nGiven: ${green(
|
|
37
|
+
region,
|
|
38
|
+
)}, choices: "us", "eu".\n`,
|
|
39
|
+
),
|
|
38
40
|
);
|
|
39
41
|
process.exit(1);
|
|
40
42
|
}
|
|
@@ -86,37 +88,36 @@ export class RedoclyClient {
|
|
|
86
88
|
if (isNotEmptyObject(credentials)) {
|
|
87
89
|
this.setAccessTokens({
|
|
88
90
|
...credentials,
|
|
89
|
-
...(credentials.token &&
|
|
90
|
-
[this.region]
|
|
91
|
-
|
|
92
|
-
|
|
91
|
+
...(credentials.token &&
|
|
92
|
+
!credentials[this.region] && {
|
|
93
|
+
[this.region]: credentials.token,
|
|
94
|
+
}),
|
|
95
|
+
});
|
|
93
96
|
}
|
|
94
97
|
if (process.env.REDOCLY_AUTHORIZATION) {
|
|
95
98
|
this.setAccessTokens({
|
|
96
99
|
...this.accessTokens,
|
|
97
|
-
[this.region]: process.env.REDOCLY_AUTHORIZATION
|
|
98
|
-
})
|
|
100
|
+
[this.region]: process.env.REDOCLY_AUTHORIZATION,
|
|
101
|
+
});
|
|
99
102
|
}
|
|
100
103
|
}
|
|
101
104
|
|
|
102
|
-
getAllTokens
|
|
103
|
-
return (<[Region, string][]>Object.entries(this.accessTokens))
|
|
104
|
-
([region]) => AVAILABLE_REGIONS.includes(region)
|
|
105
|
-
|
|
106
|
-
([region, token]) => ({ region, token })
|
|
107
|
-
);
|
|
105
|
+
getAllTokens(): RegionalToken[] {
|
|
106
|
+
return (<[Region, string][]>Object.entries(this.accessTokens))
|
|
107
|
+
.filter(([region]) => AVAILABLE_REGIONS.includes(region))
|
|
108
|
+
.map(([region, token]) => ({ region, token }));
|
|
108
109
|
}
|
|
109
110
|
|
|
110
111
|
async getValidTokens(): Promise<RegionalTokenWithValidity[]> {
|
|
111
|
-
const
|
|
112
|
+
const allTokens = this.getAllTokens();
|
|
112
113
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
114
|
+
const verifiedTokens = await Promise.allSettled(
|
|
115
|
+
allTokens.map(({ token, region }) => this.verifyToken(token, region)),
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
return allTokens
|
|
119
|
+
.filter((_, index) => verifiedTokens[index].status === 'fulfilled')
|
|
120
|
+
.map(({ token, region }) => ({ token, region, valid: true }));
|
|
120
121
|
}
|
|
121
122
|
|
|
122
123
|
async getTokens() {
|
|
@@ -124,9 +125,23 @@ export class RedoclyClient {
|
|
|
124
125
|
}
|
|
125
126
|
|
|
126
127
|
async isAuthorizedWithRedoclyByRegion(): Promise<boolean> {
|
|
127
|
-
if (!this.hasTokens())
|
|
128
|
+
if (!this.hasTokens()) {
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
|
|
128
132
|
const accessToken = this.accessTokens[this.region];
|
|
129
|
-
|
|
133
|
+
|
|
134
|
+
if (!accessToken) {
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
try {
|
|
139
|
+
await this.verifyToken(accessToken, this.region);
|
|
140
|
+
|
|
141
|
+
return true;
|
|
142
|
+
} catch (err) {
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
130
145
|
}
|
|
131
146
|
|
|
132
147
|
async isAuthorizedWithRedocly(): Promise<boolean> {
|
|
@@ -137,8 +152,11 @@ export class RedoclyClient {
|
|
|
137
152
|
return existsSync(credentialsPath) ? JSON.parse(readFileSync(credentialsPath, 'utf-8')) : {};
|
|
138
153
|
}
|
|
139
154
|
|
|
140
|
-
async verifyToken(
|
|
141
|
-
|
|
155
|
+
async verifyToken(
|
|
156
|
+
accessToken: string,
|
|
157
|
+
region: Region,
|
|
158
|
+
verbose: boolean = false,
|
|
159
|
+
): Promise<{ viewerId: string; organizations: string[] }> {
|
|
142
160
|
return this.registryApi.authStatus(accessToken, region, verbose);
|
|
143
161
|
}
|
|
144
162
|
|
|
@@ -146,8 +164,9 @@ export class RedoclyClient {
|
|
|
146
164
|
const credentialsPath = resolve(homedir(), TOKEN_FILENAME);
|
|
147
165
|
process.stdout.write(gray('\n Logging in...\n'));
|
|
148
166
|
|
|
149
|
-
|
|
150
|
-
|
|
167
|
+
try {
|
|
168
|
+
await this.verifyToken(accessToken, this.region, verbose);
|
|
169
|
+
} catch (err) {
|
|
151
170
|
process.stdout.write(
|
|
152
171
|
red('Authorization failed. Please check if you entered a valid API key.\n'),
|
|
153
172
|
);
|
|
@@ -22,28 +22,43 @@ export class RegistryApi {
|
|
|
22
22
|
|
|
23
23
|
private async request(path = '', options: RequestInit = {}, region?: Region) {
|
|
24
24
|
const headers = Object.assign({}, options.headers || {}, { 'x-redocly-cli-version': version });
|
|
25
|
-
|
|
25
|
+
|
|
26
|
+
if (!headers.hasOwnProperty('authorization')) {
|
|
27
|
+
throw new Error('Unauthorized');
|
|
28
|
+
}
|
|
29
|
+
|
|
26
30
|
const response = await fetch(
|
|
27
31
|
`${this.getBaseUrl(region)}${path}`,
|
|
28
32
|
Object.assign({}, options, { headers }),
|
|
29
33
|
);
|
|
30
|
-
|
|
34
|
+
|
|
35
|
+
if (response.status === 401) {
|
|
36
|
+
throw new Error('Unauthorized');
|
|
37
|
+
}
|
|
38
|
+
|
|
31
39
|
if (response.status === 404) {
|
|
32
40
|
const body: RegistryApiTypes.NotFoundProblemResponse = await response.json();
|
|
33
41
|
throw new Error(body.code);
|
|
34
42
|
}
|
|
43
|
+
|
|
35
44
|
return response;
|
|
36
45
|
}
|
|
37
46
|
|
|
38
|
-
async authStatus(
|
|
47
|
+
async authStatus(
|
|
48
|
+
accessToken: string,
|
|
49
|
+
region: Region,
|
|
50
|
+
verbose = false,
|
|
51
|
+
): Promise<{ viewerId: string; organizations: string[] }> {
|
|
39
52
|
try {
|
|
40
|
-
const response = await this.request('', { headers: { authorization: accessToken }}, region);
|
|
41
|
-
|
|
53
|
+
const response = await this.request('', { headers: { authorization: accessToken } }, region);
|
|
54
|
+
|
|
55
|
+
return await response.json();
|
|
42
56
|
} catch (error) {
|
|
43
57
|
if (verbose) {
|
|
44
58
|
console.log(error);
|
|
45
59
|
}
|
|
46
|
-
|
|
60
|
+
|
|
61
|
+
throw error;
|
|
47
62
|
}
|
|
48
63
|
}
|
|
49
64
|
|
|
@@ -69,7 +84,7 @@ export class RegistryApi {
|
|
|
69
84
|
isUpsert,
|
|
70
85
|
}),
|
|
71
86
|
},
|
|
72
|
-
this.region
|
|
87
|
+
this.region,
|
|
73
88
|
);
|
|
74
89
|
|
|
75
90
|
if (response.ok) {
|
|
@@ -88,20 +103,22 @@ export class RegistryApi {
|
|
|
88
103
|
branch,
|
|
89
104
|
isUpsert,
|
|
90
105
|
}: RegistryApiTypes.PushApiParams) {
|
|
91
|
-
const response = await this.request(
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
106
|
+
const response = await this.request(
|
|
107
|
+
`/${organizationId}/${name}/${version}`,
|
|
108
|
+
{
|
|
109
|
+
method: 'PUT',
|
|
110
|
+
headers: {
|
|
111
|
+
'content-type': 'application/json',
|
|
112
|
+
authorization: this.accessToken,
|
|
113
|
+
} as HeadersInit,
|
|
114
|
+
body: JSON.stringify({
|
|
115
|
+
rootFilePath,
|
|
116
|
+
filePaths,
|
|
117
|
+
branch,
|
|
118
|
+
isUpsert,
|
|
119
|
+
}),
|
|
120
|
+
},
|
|
121
|
+
this.region,
|
|
105
122
|
);
|
|
106
123
|
|
|
107
124
|
if (response.ok) {
|
|
@@ -192,8 +192,8 @@ describe('Oas3 Structural visitor basic', () => {
|
|
|
192
192
|
Object {
|
|
193
193
|
"location": Array [
|
|
194
194
|
Object {
|
|
195
|
-
"pointer": "#/",
|
|
196
|
-
"reportOnKey":
|
|
195
|
+
"pointer": "#/openapi",
|
|
196
|
+
"reportOnKey": true,
|
|
197
197
|
"source": "foobar.yaml",
|
|
198
198
|
},
|
|
199
199
|
],
|
|
@@ -259,8 +259,8 @@ describe('Oas3 Structural visitor basic', () => {
|
|
|
259
259
|
Object {
|
|
260
260
|
"location": Array [
|
|
261
261
|
Object {
|
|
262
|
-
"pointer": "#/",
|
|
263
|
-
"reportOnKey":
|
|
262
|
+
"pointer": "#/openapi",
|
|
263
|
+
"reportOnKey": true,
|
|
264
264
|
"source": "foobar.yaml",
|
|
265
265
|
},
|
|
266
266
|
],
|
|
@@ -3,9 +3,10 @@ import { Oas3Rule } from '../../visitors';
|
|
|
3
3
|
export const NoEmptyServers: Oas3Rule = () => {
|
|
4
4
|
return {
|
|
5
5
|
DefinitionRoot(root, { report, location }) {
|
|
6
|
-
if (!root.servers) {
|
|
6
|
+
if (!root.hasOwnProperty('servers')) {
|
|
7
7
|
report({
|
|
8
8
|
message: 'Servers must be present.',
|
|
9
|
+
location: location.child(['openapi']).key()
|
|
9
10
|
});
|
|
10
11
|
return;
|
|
11
12
|
}
|