Skip to content

Commit 9275222

Browse files
authored
Validate OpenAPI slug with pattern (#1029)
* Validate OpenAPI slug with pattern * review * remove logger * Fix build
1 parent 29faf81 commit 9275222

File tree

7 files changed

+122
-19
lines changed

7 files changed

+122
-19
lines changed

.changeset/funny-geese-mix.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@gitbook/cli': patch
3+
---
4+
5+
Validate OpenAPI slug with pattern

bun.lock

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -726,7 +726,7 @@
726726
},
727727
"packages/api": {
728728
"name": "@gitbook/api",
729-
"version": "0.143.2",
729+
"version": "0.145.0",
730730
"dependencies": {
731731
"event-iterator": "^2.0.0",
732732
"eventsource-parser": "^3.0.0",

packages/api/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
dist/
22
spec/
33
src/client.ts
4+
src/constants.ts

packages/api/build.sh

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ swagger-typescript-api --path ./spec/openapi.yaml --output ./src/ --name client.
2828
# Then we bundle into an importable JSON module
2929
swagger-cli bundle ./spec/openapi.yaml --outfile ./spec/openapi.json --type json
3030

31+
# Then we extract the API constants
32+
echo "Extracting API constants..."
33+
bun ./scripts/extract-constants.ts
34+
3135
# Then we build the JS files
3236
echo "Bundling CJS format from code..."
3337
esbuild ./src/index.ts --bundle --platform=node --format=cjs --outfile=./dist/index.cjs --log-level=warning
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import fs from 'fs';
2+
import path from 'path';
3+
4+
const fileContentPrefix = `/* tslint:disable */
5+
/*
6+
* ---------------------------------------------------------------
7+
* ## THIS FILE WAS GENERATED ##
8+
* ## ##
9+
* ## See extract-constants.ts for more details ##
10+
* ---------------------------------------------------------------
11+
*/
12+
`;
13+
14+
const inputPath = path.resolve(process.cwd(), 'spec/openapi.json');
15+
const outputPath = path.resolve(process.cwd(), 'src/constants.ts');
16+
17+
const file = await fs.promises.readFile(inputPath, 'utf-8');
18+
const api = JSON.parse(file);
19+
const constants: Record<string, number | string | string[] | number[]> = {};
20+
for (const [schemaName, schema] of Object.entries(api.components.schemas)) {
21+
if (
22+
schema &&
23+
typeof schema === 'object' &&
24+
'maxLength' in schema &&
25+
typeof schema.maxLength === 'number'
26+
) {
27+
constants[formatKey(schemaName, 'maxLength')] = schema.maxLength;
28+
}
29+
30+
if (
31+
schema &&
32+
typeof schema === 'object' &&
33+
'minLength' in schema &&
34+
typeof schema.minLength === 'number'
35+
) {
36+
constants[formatKey(schemaName, 'minLength')] = schema.minLength;
37+
}
38+
39+
if (
40+
schema &&
41+
typeof schema === 'object' &&
42+
'pattern' in schema &&
43+
typeof schema.pattern === 'string'
44+
) {
45+
constants[formatKey(schemaName, 'pattern')] = schema.pattern;
46+
}
47+
48+
if (
49+
schema &&
50+
typeof schema === 'object' &&
51+
'enum' in schema &&
52+
Array.isArray(schema.enum) &&
53+
(schema.enum.every((item) => typeof item === 'string') ||
54+
schema.enum.every((item) => typeof item === 'number'))
55+
) {
56+
constants[formatKey(schemaName, 'enum')] = schema.enum;
57+
}
58+
}
59+
const constantLines = Object.entries(constants)
60+
.map(([key, value]) => {
61+
if (Array.isArray(value)) {
62+
return `export const ${key}:${JSON.stringify(value)} = ${JSON.stringify(value)};`;
63+
}
64+
return `export const ${key} = ${JSON.stringify(value)};`;
65+
})
66+
.join('\n');
67+
68+
await fs.promises.writeFile(outputPath, `${fileContentPrefix}\n${constantLines}`);
69+
70+
function formatKey(schemaName: string, key: string) {
71+
return convertCamelToSnake(`${schemaName}_${key}`);
72+
}
73+
74+
function convertCamelToSnake(str: string) {
75+
return str.replace(/([a-z])([A-Z])/g, '$1_$2').toUpperCase();
76+
}

packages/api/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { Api } from './client';
55
import { GitBookAPIError } from './GitBookAPIError';
66

77
export * from './client';
8+
export * from './constants';
89
export { GitBookAPIError };
910

1011
export const GITBOOK_DEFAULT_ENDPOINT = 'https://api.gitbook.com';

packages/cli/src/openapi/publish.ts

Lines changed: 34 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
11
import * as fs from 'fs';
2-
import * as path from 'path';
3-
42
import * as api from '@gitbook/api';
53
import { getAPIClient } from '../remote';
64

@@ -22,16 +20,13 @@ export async function publishOpenAPISpecificationFromURL(args: {
2220
*/
2321
url: string;
2422
}): Promise<api.OpenAPISpec> {
23+
const slug = validateSlug(args.specSlug);
2524
const api = await getAPIClient(true);
26-
const spec = await api.orgs.createOrUpdateOpenApiSpecBySlug(
27-
args.organizationId,
28-
args.specSlug,
29-
{
30-
source: {
31-
url: args.url,
32-
},
25+
const spec = await api.orgs.createOrUpdateOpenApiSpecBySlug(args.organizationId, slug, {
26+
source: {
27+
url: args.url,
3328
},
34-
);
29+
});
3530
return spec.data;
3631
}
3732

@@ -53,17 +48,14 @@ export async function publishOpenAPISpecificationFromFilepath(args: {
5348
*/
5449
filepath: string;
5550
}): Promise<api.OpenAPISpec> {
51+
const slug = validateSlug(args.specSlug);
5652
const api = await getAPIClient(true);
5753
const fileContent = await readOpenAPIFile(args.filepath);
58-
const spec = await api.orgs.createOrUpdateOpenApiSpecBySlug(
59-
args.organizationId,
60-
args.specSlug,
61-
{
62-
source: {
63-
text: fileContent,
64-
},
54+
const spec = await api.orgs.createOrUpdateOpenApiSpecBySlug(args.organizationId, slug, {
55+
source: {
56+
text: fileContent,
6557
},
66-
);
58+
});
6759
return spec.data;
6860
}
6961

@@ -81,3 +73,27 @@ async function readOpenAPIFile(filePath: string): Promise<string> {
8173
throw error;
8274
}
8375
}
76+
77+
const OPENAPISPEC_SLUG_REGEX = new RegExp(api.OPEN_APISPEC_SLUG_PATTERN);
78+
/**
79+
* Validate the OpenAPI specification slug.
80+
* It should match the pattern and be between the minimum and maximum length.
81+
*/
82+
function validateSlug(specSlug: string) {
83+
if (!OPENAPISPEC_SLUG_REGEX.test(specSlug)) {
84+
throw new Error(
85+
`Invalid OpenAPI specification slug, must match pattern: ${api.OPEN_APISPEC_SLUG_PATTERN}`,
86+
);
87+
}
88+
89+
if (
90+
specSlug.length < api.OPEN_APISPEC_SLUG_MIN_LENGTH ||
91+
specSlug.length > api.OPEN_APISPEC_SLUG_MAX_LENGTH
92+
) {
93+
throw new Error(
94+
`Invalid OpenAPI specification slug, must be between ${api.OPEN_APISPEC_SLUG_MIN_LENGTH} and ${api.OPEN_APISPEC_SLUG_MAX_LENGTH} characters`,
95+
);
96+
}
97+
98+
return specSlug;
99+
}

0 commit comments

Comments
 (0)