Skip to content

Commit

Permalink
feat: add OpenAPI validation to a few endpoints (#1409)
Browse files Browse the repository at this point in the history
* feat: add OpenAPI validation to a few endpoints (2)

* refactor: use package version as the OpenAPI version

* refactor: keep the existing OpenAPI page for now

* refactor: add snapshots tests for the OpenAPI output

* refactor: validate Content-Type by default

* refactor: update vulnerable deps

* refactor: fix documentation URL to match schema

* refactor: improve external type declaration

* refactor: remove unused package resolutions

* refactor: try express-openapi fork

* Update package.json

* Update src/lib/services/openapi-service.ts

* Update src/lib/types/openapi.d.ts

* Update src/lib/types/openapi.d.ts

Co-authored-by: Ivar Conradi Østhus <ivarconr@gmail.com>
  • Loading branch information
olav and ivarconr committed Apr 25, 2022
1 parent f52f1ca commit fdebeef
Show file tree
Hide file tree
Showing 31 changed files with 2,572 additions and 2,595 deletions.
3 changes: 3 additions & 0 deletions package.json
Expand Up @@ -98,13 +98,15 @@
"joi": "^17.3.0",
"js-yaml": "^4.1.0",
"knex": "1.0.4",
"json-schema-to-ts": "^1.6.5",
"log4js": "^6.0.0",
"memoizee": "^0.4.15",
"mime": "^2.4.2",
"multer": "^1.4.1",
"mustache": "^4.1.0",
"node-fetch": "^2.6.7",
"nodemailer": "^6.5.0",
"openapi-types": "^10.0.0",
"owasp-password-strength-test": "^1.3.0",
"parse-database-url": "^0.3.0",
"pg": "^8.7.3",
Expand All @@ -115,6 +117,7 @@
"serve-favicon": "^2.5.0",
"stoppable": "^1.1.0",
"type-is": "^1.6.18",
"@unleash/express-openapi": "^0.2.0",
"unleash-frontend": "4.10.0-beta.6",
"uuid": "^8.3.2",
"semver": "^7.3.5"
Expand Down
10 changes: 10 additions & 0 deletions src/lib/app.ts
Expand Up @@ -69,6 +69,11 @@ export default async function getApp(
if (config.enableOAS) {
app.use(`${baseUriPath}/oas`, express.static('docs/api/oas'));
}

if (config.enableOAS && services.openApiService) {
services.openApiService.useDocs(app);
}

switch (config.authentication.type) {
case IAuthType.OPEN_SOURCE: {
app.use(baseUriPath, apiTokenMiddleware(config, services));
Expand Down Expand Up @@ -128,6 +133,10 @@ export default async function getApp(
// Setup API routes
app.use(`${baseUriPath}/`, new IndexRouter(config, services).router);

if (services.openApiService) {
services.openApiService.useErrorHandler(app);
}

if (process.env.NODE_ENV !== 'production') {
app.use(errorHandler());
}
Expand All @@ -144,5 +153,6 @@ export default async function getApp(

res.send(indexHTML);
});

return app;
}
45 changes: 45 additions & 0 deletions src/lib/openapi/index.ts
@@ -0,0 +1,45 @@
import { OpenAPIV3 } from 'openapi-types';
import { featuresSchema } from './spec/features-schema';
import { featureSchema } from './spec/feature-schema';
import { strategySchema } from './spec/strategy-schema';
import { variantSchema } from './spec/variant-schema';
import { overrideSchema } from './spec/override-schema';
import { createFeatureSchema } from './spec/create-feature-schema';
import { constraintSchema } from './spec/constraint-schema';

// Create the base OpenAPI schema, with everything except paths.
export const createOpenApiSchema = (
serverUrl?: string,
): Omit<OpenAPIV3.Document, 'paths'> => {
return {
openapi: '3.0.3',
servers: serverUrl ? [{ url: serverUrl }] : [],
info: {
title: 'Unleash API',
version: process.env.npm_package_version,
},
security: [
{
apiKey: [],
},
],
components: {
securitySchemes: {
apiKey: {
type: 'apiKey',
in: 'header',
name: 'Authorization',
},
},
schemas: {
createFeatureSchema,
featuresSchema,
featureSchema,
strategySchema,
variantSchema,
overrideSchema,
constraintSchema,
},
},
};
};
24 changes: 24 additions & 0 deletions src/lib/openapi/spec/constraint-schema.ts
@@ -0,0 +1,24 @@
import { createSchemaObject, CreateSchemaType } from '../types';

export const schema = {
type: 'object',
required: ['contextName', 'operator'],
properties: {
contextName: {
type: 'string',
},
operator: {
type: 'string',
},
values: {
type: 'array',
items: {
type: 'string',
},
},
},
} as const;

export type ConstraintSchema = CreateSchemaType<typeof schema>;

export const constraintSchema = createSchemaObject(schema);
12 changes: 12 additions & 0 deletions src/lib/openapi/spec/create-feature-request.ts
@@ -0,0 +1,12 @@
import { OpenAPIV3 } from 'openapi-types';

export const createFeatureRequest: OpenAPIV3.RequestBodyObject = {
required: true,
content: {
'application/json': {
schema: {
$ref: '#/components/schemas/createFeatureSchema',
},
},
},
};
24 changes: 24 additions & 0 deletions src/lib/openapi/spec/create-feature-schema.ts
@@ -0,0 +1,24 @@
import { createSchemaObject, CreateSchemaType } from '../types';

const schema = {
type: 'object',
required: ['name'],
properties: {
name: {
type: 'string',
},
type: {
type: 'string',
},
description: {
type: 'string',
},
impressionData: {
type: 'boolean',
},
},
} as const;

export type CreateFeatureSchema = CreateSchemaType<typeof schema>;

export const createFeatureSchema = createSchemaObject(schema);
12 changes: 12 additions & 0 deletions src/lib/openapi/spec/feature-response.ts
@@ -0,0 +1,12 @@
import { OpenAPIV3 } from 'openapi-types';

export const featureResponse: OpenAPIV3.ResponseObject = {
description: 'featureResponse',
content: {
'application/json': {
schema: {
$ref: '#/components/schemas/featureSchema',
},
},
},
};
55 changes: 55 additions & 0 deletions src/lib/openapi/spec/feature-schema.ts
@@ -0,0 +1,55 @@
import { createSchemaObject, CreateSchemaType } from '../types';

const schema = {
type: 'object',
required: ['name', 'project'],
properties: {
name: {
type: 'string',
},
type: {
type: 'string',
},
description: {
type: 'string',
},
project: {
type: 'string',
},
enabled: {
type: 'boolean',
},
stale: {
type: 'boolean',
},
impressionData: {
type: 'boolean',
},
createdAt: {
type: 'string',
format: 'date',
nullable: true,
},
lastSeenAt: {
type: 'string',
format: 'date',
nullable: true,
},
strategies: {
type: 'array',
items: {
$ref: '#/components/schemas/strategySchema',
},
},
variants: {
items: {
$ref: '#/components/schemas/variantSchema',
},
type: 'array',
},
},
} as const;

export type FeatureSchema = CreateSchemaType<typeof schema>;

export const featureSchema = createSchemaObject(schema);
12 changes: 12 additions & 0 deletions src/lib/openapi/spec/features-response.ts
@@ -0,0 +1,12 @@
import { OpenAPIV3 } from 'openapi-types';

export const featuresResponse: OpenAPIV3.ResponseObject = {
description: 'featuresResponse',
content: {
'application/json': {
schema: {
$ref: '#/components/schemas/featuresSchema',
},
},
},
};
21 changes: 21 additions & 0 deletions src/lib/openapi/spec/features-schema.ts
@@ -0,0 +1,21 @@
import { createSchemaObject, CreateSchemaType } from '../types';

export const schema = {
type: 'object',
required: ['version', 'features'],
properties: {
version: {
type: 'integer',
},
features: {
type: 'array',
items: {
$ref: '#/components/schemas/featureSchema',
},
},
},
} as const;

export type FeaturesSchema = CreateSchemaType<typeof schema>;

export const featuresSchema = createSchemaObject(schema);
21 changes: 21 additions & 0 deletions src/lib/openapi/spec/override-schema.ts
@@ -0,0 +1,21 @@
import { createSchemaObject, CreateSchemaType } from '../types';

export const schema = {
type: 'object',
required: ['contextName', 'values'],
properties: {
contextName: {
type: 'string',
},
values: {
type: 'array',
items: {
type: 'string',
},
},
},
} as const;

export type OverrideSchema = CreateSchemaType<typeof schema>;

export const overrideSchema = createSchemaObject(schema);
27 changes: 27 additions & 0 deletions src/lib/openapi/spec/strategy-schema.ts
@@ -0,0 +1,27 @@
import { createSchemaObject, CreateSchemaType } from '../types';

export const schema = {
type: 'object',
required: ['id', 'name', 'constraints', 'parameters'],
properties: {
id: {
type: 'string',
},
name: {
type: 'string',
},
constraints: {
type: 'array',
items: {
$ref: '#/components/schemas/constraintSchema',
},
},
parameters: {
type: 'object',
},
},
} as const;

export type StrategySchema = CreateSchemaType<typeof schema>;

export const strategySchema = createSchemaObject(schema);
33 changes: 33 additions & 0 deletions src/lib/openapi/spec/variant-schema.ts
@@ -0,0 +1,33 @@
import { createSchemaObject, CreateSchemaType } from '../types';

export const schema = {
type: 'object',
required: ['name', 'weight', 'weightType', 'stickiness', 'overrides'],
properties: {
name: {
type: 'string',
},
weight: {
type: 'number',
},
weightType: {
type: 'string',
},
stickiness: {
type: 'string',
},
payload: {
type: 'object',
},
overrides: {
type: 'array',
items: {
$ref: '#/components/schemas/overrideSchema',
},
},
},
} as const;

export type VariantSchema = CreateSchemaType<typeof schema>;

export const variantSchema = createSchemaObject(schema);
21 changes: 21 additions & 0 deletions src/lib/openapi/types.ts
@@ -0,0 +1,21 @@
import { OpenAPIV3 } from 'openapi-types';
import { FromSchema } from 'json-schema-to-ts';
import { DeepMutable } from '../types/mutable';

// Admin paths must have the "admin" tag.
export interface AdminApiOperation
extends Omit<OpenAPIV3.OperationObject, 'tags'> {
tags: ['admin'];
}

// Client paths must have the "client" tag.
export interface ClientApiOperation
extends Omit<OpenAPIV3.OperationObject, 'tags'> {
tags: ['client'];
}

// Create a type from a const schema object.
export type CreateSchemaType<T> = FromSchema<T>;

// Create an OpenAPIV3.SchemaObject from a const schema object.
export const createSchemaObject = <T>(schema: T): DeepMutable<T> => schema;

0 comments on commit fdebeef

Please sign in to comment.