Ading server testing and better handlers.

Signed-off-by: Aramis Sennyey <sennyeya@amazon.com>
This commit is contained in:
Aramis Sennyey
2023-02-16 16:13:26 -05:00
committed by Fredrik Adelöw
parent ef4d42ec30
commit e1962a4f0a
13 changed files with 491 additions and 1031 deletions
@@ -54,7 +54,6 @@ import { initialize } from 'express-openapi';
import yaml from 'js-yaml';
import fs from 'fs';
import path from 'path';
import OpenAPIBackend from 'openapi-backend';
class ParsingError extends Error {
toString() {
@@ -105,23 +104,6 @@ export async function createRouter(
logger.info('Catalog is running in readonly mode');
}
// create api with your definition file or object
const api = new OpenAPIBackend({ definition: './petstore.yml' });
// register your framework specific request handlers here
api.register({
getPets: (c, req, res) => {
return res.status(200).json({ result: 'ok' });
},
getPetById: (c, req, res) => res.status(200).json({ result: 'ok' }),
validationFail: (c, req, res) =>
res.status(400).json({ err: c.validation.errors }),
notFound: (c, req, res) => res.status(404).json({ err: 'not found' }),
});
// initalize the backend
api.init();
const validateDependency = (
dependency: any,
next: (req: express.Request, res: express.Response) => any,
@@ -143,6 +125,7 @@ export async function createRouter(
// NOTE: If using yaml you can provide a path relative to process.cwd() e.g.
// apiDoc: './api-v1/api-doc.yml',
apiDoc: yaml.load(
// eslint-disable-next-line no-restricted-syntax
fs.readFileSync(path.resolve(__dirname, '../../openapi.yaml'), 'utf-8'),
) as any,
operations: {
@@ -345,12 +328,18 @@ export async function createRouter(
},
enableObjectCoercion: true,
errorMiddleware: errorHandler(),
errorTransformer: (openapiError, ajvError) => {
switch (openapiError.errorCode) {
errorTransformer: openapiError => {
const error = openapiError as {
errorCode: string;
path: string;
message: string;
};
// eslint-disable-next-line default-case
switch (error.errorCode) {
case 'type.openapi.requestValidation':
throw new InputError(
`Invalid field ${openapiError.path}`,
new ParsingError(openapiError.message),
`Invalid field ${error.path}`,
new ParsingError(error.message),
);
}
return {};
+3 -1
View File
@@ -32,6 +32,8 @@
"dependencies": {
"express": "^4.18.2",
"json-schema-to-ts": "^2.6.2",
"openapi-types": "^12.1.0"
"openapi-types": "^12.1.0",
"openapi3-ts": "^3.1.2",
"ts-node": "^10.9.1"
}
}
+163 -132
View File
@@ -13,26 +13,23 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { OpenAPIV3_1 } from 'openapi-types';
import { FieldValues } from './types/fields';
import { OpenAPIV3, OpenAPIV3_1 } from 'openapi-types';
import { IRouter, Router } from 'express';
import core, { ParamsDictionary } from 'express-serve-static-core';
import { FromSchema, JSONSchema7 } from 'json-schema-to-ts';
import {
AppendNonBlankKey,
FieldPath,
FieldPathValue,
Path,
} from './types/path';
import { IRouter, Router, IRouterMatcher } from 'express';
import core, {
ParamsDictionary,
RequestHandler,
} from 'express-serve-static-core';
import { FromSchema } from 'json-schema-to-ts';
DocPath,
DocPathMethod,
DocPathTemplate,
MethodAwareDocPath,
PathTemplate,
RequestBodySchema,
RequiredDoc,
ValueOf,
} from './types';
import { ResponseSchemas } from './types/response';
type RouterFn<TFieldValues extends FieldValues> = <
TFieldName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
>(
name: TFieldName,
) => null;
type DeepWriteable<T> = { -readonly [P in keyof T]: DeepWriteable<T[P]> };
const doc = {
openapi: '3.1.0',
@@ -201,59 +198,6 @@ const doc = {
},
} as const;
const isUndefined = (a: any): a is undefined => a === undefined;
const isNull = (a: any): a is null => a === null;
const isNullOrUndefined = (a: any): a is undefined | null =>
isUndefined(a) || isNull(a);
const isDateObject = (value: unknown): value is Date => value instanceof Date;
const isObjectType = (value: unknown) => typeof value === 'object';
const isObject = <T extends object>(value: unknown): value is T =>
!isNullOrUndefined(value) &&
!Array.isArray(value) &&
isObjectType(value) &&
!isDateObject(value);
const compact = <TValue>(value: TValue[]) =>
Array.isArray(value) ? value.filter(Boolean) : [];
export const resolve = <T>(
obj: T,
path: string,
defaultValue?: unknown,
): any => {
if (!path || !isObject(obj)) {
return defaultValue;
}
const result = compact(path.split(/[,[\].]+?/)).reduce(
(result, key) =>
isNullOrUndefined(result) ? result : result[key as keyof {}],
obj,
);
if (result === undefined || result === obj) {
return obj[path as keyof T] === undefined
? defaultValue
: obj[path as keyof T];
}
return result;
};
/**
* We want this to input path and have
* @param name
* @returns
*/
const x: RouterFn<typeof doc.paths> = name => {
console.log(resolve(doc, name));
return null;
};
x('/pets/{petId}');
type RemoveTail<
S extends string,
Tail extends string,
@@ -279,79 +223,166 @@ export type RouteParameters<Route extends string> = string extends Route
interface ParsedQs {
[key: string]: undefined | string | string[] | ParsedQs | ParsedQs[];
}
interface ApiRouterMatcher<
TFieldValues extends FieldValues,
// oh boy don't do this
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (
k: infer I,
) => void
? I
: never;
type LastOf<T> = UnionToIntersection<
T extends any ? () => T : never
> extends () => infer R
? R
: never;
// TS4.0+
type Push<T extends any[], V> = [...T, V];
// TS4.1+
type TuplifyUnion<
T,
Method extends
| 'all'
| 'get'
| 'post'
| 'put'
| 'delete'
| 'patch'
| 'options'
| 'head',
> {
<
TFieldName extends Path<TFieldValues> = Path<TFieldValues>,
P = RouteParameters<TFieldName>,
ResBody = any,
ReqBody = any,
ReqQuery = ParsedQs,
Locals extends Record<string, any> = Record<string, any>,
>(
// (it's used as the default type parameter for P)
path: TFieldName,
// (This generic is meant to be passed explicitly.)
...handlers: Array<RequestHandler<P, ResBody, ReqBody, ReqQuery, Locals>>
): T;
}
L = LastOf<T>,
N = [T] extends [never] ? true : false,
> = true extends N ? [] : Push<TuplifyUnion<Exclude<T, L>>, L>;
type path = ApiRouterMatcher<typeof doc.paths, any, 'get'>;
const test: path = a => {
console.log(a);
};
test('/pets/{petId}');
type ConvertAll<T, R extends ReadonlyArray<unknown> = []> = T extends [
infer First extends JSONSchema7,
...infer Rest,
]
? ConvertAll<Rest, [...R, FromSchema<First>]>
: R;
export interface IApiRouter<ApiSpec, PathSpec, T> extends IRouter {
all: ApiRouterMatcher<PathSpec, T, 'all'>;
get: ApiRouterMatcher<PathSpec, T, 'get'>;
}
type ResponseToJsonSchema<
Doc extends RequiredDoc,
Path extends PathTemplate<Extract<keyof Doc['paths'], string>>,
Method extends DocPathMethod<Doc, Path>,
> = ConvertAll<
TuplifyUnion<ValueOf<ResponseSchemas<Doc, Path, Method>>>
>[number];
export default class ApiRouter<
ApiSpec,
PathSpec extends OpenAPIV3_1.Document,
T,
> implements IApiRouter<ApiSpec, PathSpec, T>
{
type DocRequestHandler<
Doc extends RequiredDoc,
Path extends DocPathTemplate<Doc>,
Method extends keyof Doc['paths'][Path],
> = core.RequestHandler<
core.ParamsDictionary,
// From https://stackoverflow.com/questions/71393738/typescript-intersection-not-union-type-from-json-schema.
ResponseToJsonSchema<Doc, Path, Method>,
RequestBodySchema<Doc, Path, Method>,
ParsedQs,
Record<string, string>
>;
export default class ApiRouter<Doc extends RequiredDoc> {
private _router = Router();
constructor(private spec: OpenAPIV3_1.Document) {}
constructor(private spec: OpenAPIV3_1.Document | OpenAPIV3.Document) {}
static fromSpec<ApiSpec, PathSpec, T>(spec: OpenAPIV3_1.Document) {
return new ApiRouter<ApiSpec, PathSpec, T>(spec);
static fromSpec<Doc extends RequiredDoc>(
spec: OpenAPIV3_1.Document | OpenAPIV3.Document,
) {
return new ApiRouter<Doc>(spec);
}
get<
TFieldValues extends FieldValues = PathSpec,
TFieldName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
>(
path: TFieldName,
...handlers: core.RequestHandler<
core.ParamsDictionary,
any,
FieldPathValue<TFieldValues, TFieldName>,
ParsedQs,
Record<string, string>
>[]
get<Path extends MethodAwareDocPath<Doc, DocPathTemplate<Doc>, 'get'>>(
path: Path,
...handlers: DocRequestHandler<Doc, Path, 'get'>[]
) {
console.log(path);
return this._router.get(path, ...handlers);
this._router.get(path, ...handlers);
return this;
}
post<Path extends MethodAwareDocPath<Doc, DocPathTemplate<Doc>, 'post'>>(
path: Path,
...handlers: DocRequestHandler<Doc, Path, 'post'>[]
) {
console.log(path);
this._router.post(path, ...handlers);
return this;
}
all<Path extends MethodAwareDocPath<Doc, DocPathTemplate<Doc>, 'all'>>(
path: Path,
...handlers: DocRequestHandler<Doc, Path, 'all'>[]
) {
console.log(path);
this._router.all(path, ...handlers);
return this;
}
put<Path extends MethodAwareDocPath<Doc, DocPathTemplate<Doc>, 'put'>>(
path: Path,
...handlers: DocRequestHandler<Doc, Path, 'put'>[]
) {
console.log(path);
this._router.put(path, ...handlers);
return this;
}
delete<Path extends MethodAwareDocPath<Doc, DocPathTemplate<Doc>, 'delete'>>(
path: Path,
...handlers: DocRequestHandler<Doc, Path, 'delete'>[]
) {
console.log(path);
this._router.delete(path, ...handlers);
return this;
}
patch<Path extends MethodAwareDocPath<Doc, DocPathTemplate<Doc>, 'patch'>>(
path: Path,
...handlers: DocRequestHandler<Doc, Path, 'patch'>[]
) {
console.log(path);
this._router.patch(path, ...handlers);
return this;
}
options<
Path extends MethodAwareDocPath<Doc, DocPathTemplate<Doc>, 'options'>,
>(path: Path, ...handlers: DocRequestHandler<Doc, Path, 'options'>[]) {
console.log(path);
this._router.options(path, ...handlers);
return this;
}
head<Path extends MethodAwareDocPath<Doc, DocPathTemplate<Doc>, 'head'>>(
path: Path,
...handlers: DocRequestHandler<Doc, Path, 'head'>[]
) {
console.log(path);
this._router.head(path, ...handlers);
return this;
}
use(...handlers: core.RequestHandler[]) {
return this._router.use(handlers);
}
build() {
return this._router;
}
}
const router = ApiRouter.fromSpec<typeof doc, typeof doc.paths, any>(doc);
interface RouterOptions {}
router.get('/pets', (req, res) => {
req.body.tags;
});
export async function createRouter(options: RouterOptions) {
const router = ApiRouter.fromSpec<DeepWriteable<typeof doc>>(
// As const forces the doc to readonly which conflicts with imported types.
doc as DeepWriteable<typeof doc>,
);
router.get('/pets/:uid', (req, res) => {
res.json({
id: 1,
name: req.params['uid'],
});
});
// router.get('/pet') will complain with a TS error
router.post('/pets', (req, res) => {
res.json({
message: req.path,
code: 1,
});
});
return router.build();
}
+33
View File
@@ -0,0 +1,33 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import yn from 'yn';
import { getRootLogger } from '@backstage/backend-common';
import { startStandaloneServer } from './standaloneServer';
const port = process.env.PLUGIN_PORT ? Number(process.env.PLUGIN_PORT) : 3004;
const enableCors = yn(process.env.PLUGIN_CORS, { default: false });
const logger = getRootLogger();
startStandaloneServer({ port, enableCors, logger }).catch(err => {
logger.error(err);
process.exit(1);
});
process.on('SIGINT', () => {
logger.info('CTRL+C pressed; exiting.');
process.exit(0);
});
@@ -0,0 +1,48 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Server } from 'http';
import { Logger } from 'winston';
import { createServiceBuilder } from '@backstage/backend-common';
import { createRouter } from './router';
export interface ServerOptions {
port: number;
enableCors: boolean;
logger: Logger;
}
export async function startStandaloneServer(
options: ServerOptions,
): Promise<Server> {
const logger = options.logger.child({ service: 'app-backend' });
logger.debug('Starting application server...');
const router = await createRouter({
logger,
appPackageName: 'example-app',
});
const service = createServiceBuilder(module)
.setPort(options.port)
.addRouter('', router);
return await service.start().catch(err => {
logger.error(err);
process.exit(1);
});
}
module.hot?.accept();
@@ -0,0 +1,116 @@
/**
* Pulled from https://github.com/varanauskas/oatx.
*/
import type {
ContentObject,
OpenAPIObject,
ReferenceObject,
} from 'openapi3-ts';
export type RequiredDoc = Pick<OpenAPIObject, 'paths' | 'components'>;
export type PathDoc = { paths: Record<string, unknown> };
/**
* Get value types of `T`
*/
export type ValueOf<T> = T[keyof T];
/**
* Validate a string against OpenAPI path template
* ```
* const path = PathTemplate<"/posts/{postId}/comments/{commentId}"> = "/posts/1/comments/2"const pathWithParams: PathTemplate<"/posts/{postId}/comments/{commentId}"> = "/posts/1/comments/2";
* const pathWithoutParams: PathTemplate<"/posts/comments"> = "/posts/comments";```
* https://spec.openapis.org/oas/v3.1.0#path-templating-matching
*/
export type PathTemplate<Path extends string> =
Path extends `${infer Prefix}{${string}}${infer Suffix}`
? `${Prefix}${string}${PathTemplate<Suffix>}`
: Path;
/**
* Extract path as specified in OpenAPI `Doc` based on request path
* ```
* const spec = {
* paths: {
* "/posts/{postId}/comments/{commentId}": {},
* "/posts/comments": {},
* }
* };
* const specPathWithParams: DocPath<typeof spec, "/posts/1/comments/2"> = "/posts/{postId}/comments/{commentId}";
* const specPathWithoutParams: DocPath<typeof spec, "/posts/comments"> = "/posts/comments";
* ```
*/
export type DocPath<
Doc extends PathDoc,
Path extends PathTemplate<Extract<keyof Doc['paths'], string>>,
> = ValueOf<{
[Template in Extract<
keyof Doc['paths'],
string
>]: Path extends PathTemplate<Template> ? Template : never;
}>;
export type DocPathTemplate<Doc extends PathDoc> = PathTemplate<
Extract<keyof Doc['paths'], string>
>;
export type DocPathMethod<
Doc extends Pick<RequiredDoc, 'paths'>,
Path extends DocPathTemplate<Doc>,
> = keyof Doc['paths'][DocPath<Doc, Path>];
export type MethodAwareDocPath<
Doc extends PathDoc,
Path extends PathTemplate<Extract<keyof Doc['paths'], string>>,
Method extends keyof Doc['paths'][Path],
> = ValueOf<{
[Template in Extract<
keyof Doc['paths'],
string
>]: Path extends PathTemplate<Template>
? Method extends DocPathMethod<Doc, Path>
? PathTemplate<Template>
: never
: never;
}>;
export type DocOperation<
Doc extends RequiredDoc,
Path extends keyof Doc['paths'],
Method extends keyof Doc['paths'][Path],
> = Doc['paths'][Path][Method];
export type ComponentTypes<Doc extends RequiredDoc> = Extract<
keyof Doc['components'],
string
>;
export type ComponentRef<
Doc extends RequiredDoc,
Type extends ComponentTypes<Doc>,
Ref extends ReferenceObject,
> = Ref extends { $ref: `#/components/${Type}/${infer Name}` }
? Name extends keyof Doc['components'][Type]
? Doc['components'][Type][Name] extends ReferenceObject
? ComponentRef<Doc, Type, Doc['components'][Type][Name]>
: Doc['components'][Type][Name]
: never
: never;
export type SchemaRef<Doc extends RequiredDoc, Schema> = Schema extends {
$ref: `#/components/schemas/${infer Name}`;
}
? 'schemas' extends keyof Doc['components']
? Name extends keyof Doc['components']['schemas']
? SchemaRef<Doc, Doc['components']['schemas'][Name]>
: never
: never
: { [Key in keyof Schema]: SchemaRef<Doc, Schema[Key]> };
export type ObjectWithContentSchema<
Doc extends RequiredDoc,
Object extends { content?: ContentObject },
> = Object['content'] extends ContentObject
? SchemaRef<Doc, Object['content']['application/json']['schema']>
: never;
@@ -1,66 +0,0 @@
/*
* Copyright 2023 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { IsFlatObject, Noop } from './utils';
export type InternalFieldName = string;
export type FieldName<TFieldValues extends FieldValues> =
IsFlatObject<TFieldValues> extends true
? Extract<keyof TFieldValues, string>
: string;
export type CustomElement<TFieldValues extends FieldValues> = {
name: FieldName<TFieldValues>;
type?: string;
value?: any;
disabled?: boolean;
checked?: boolean;
options?: HTMLOptionsCollection;
files?: FileList | null;
focus?: Noop;
};
export type FieldValue<TFieldValues extends FieldValues> =
TFieldValues[InternalFieldName];
export type FieldValues = Record<string, any>;
export type NativeFieldValue =
| string
| number
| boolean
| null
| undefined
| unknown[];
export type FieldElement<TFieldValues extends FieldValues = FieldValues> =
| HTMLInputElement
| HTMLSelectElement
| HTMLTextAreaElement
| CustomElement<TFieldValues>;
export type Ref = FieldElement;
export type Field = {
_f: {
ref: Ref;
name: InternalFieldName;
refs?: HTMLInputElement[];
mount?: boolean;
};
};
export type FieldRefs = Partial<Record<InternalFieldName, Field>>;
@@ -0,0 +1,2 @@
export * from './common';
export * from './requests';
@@ -1,666 +0,0 @@
/*
* Copyright 2023 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { FieldValues } from './fields';
import {
BrowserNativeObject,
IsAny,
IsNever,
IsEqual,
Primitive,
} from './utils';
/**
* Type alias to `string` which describes a lodash-like path through an object.
* E.g. `'foo.bar.0.baz'`
*/
export type PathString = string;
/**
* Type which can be traversed through with a {@link PathString}.
* I.e. objects, arrays, and tuples
*/
export type Traversable = object;
/**
* Type to query whether an array type T is a tuple type.
* @typeParam T - type which may be an array or tuple
* @example
* ```
* IsTuple<[number]> = true
* IsTuple<number[]> = false
* ```
*/
export type IsTuple<T extends ReadonlyArray<any>> = number extends T['length']
? false
: true;
/**
* Type which can be used to index an array or tuple type.
*/
export type ArrayKey = number;
/**
* Type which can be used to index an object.
*/
export type Key = string;
/**
* Type to assert that a type is a {@link Key}.
* @typeParam T - type which may be a {@link Key}
*/
export type AsKey<T> = Extract<T, Key>;
/**
* Type to convert a type to a {@link Key}.
* @typeParam T - type which may be converted to a {@link Key}
*/
export type ToKey<T> = T extends ArrayKey ? `${T}` : AsKey<T>;
/**
* Type which describes a path through an object
* as a list of individual {@link Key}s.
*/
export type PathTuple = Key[];
/**
* Type to assert that a type is a {@link PathTuple}.
* @typeParam T - type which may be a {@link PathTuple}
*/
export type AsPathTuple<T> = Extract<T, PathTuple>;
/**
* Type to intersect a union type.
* See https://fettblog.eu/typescript-union-to-intersection/
* @typeParam U - union
* @example
* ```
* UnionToIntersection<{ foo: string } | { bar: number }>
* = { foo: string; bar: number }
* ```
*/
export type UnionToIntersection<U> = (
U extends any ? (_: U) => any : never
) extends (_: infer I) => any
? I
: never;
/**
* Type which appends a {@link Key} to the {@link PathTuple} only if it is not
* blank, i.e. not the empty string.
* @typeParam PT - path
* @typeParam K - key
* @example
* ```
* AppendNonBlankKey<['foo'], 'bar'> = ['foo', 'bar']
* AppendNonBlankKey<['foo'], ''> = ['foo']
* ```
*/
export type AppendNonBlankKey<
PT extends PathTuple,
K extends Key,
> = K extends '' ? PT : [...PT, K];
/**
* Type to implement {@link SplitPathString} tail recursively.
* @typeParam PS - remaining {@link PathString} which should be split into its
* individual {@link Key}s
* @typeParam PT - accumulator of the {@link Key}s which have been split from
* the original {@link PathString} already
*/
type SplitPathStringImpl<
PS extends PathString,
PT extends PathTuple,
> = PS extends `${infer K}.${infer R}`
? SplitPathStringImpl<R, AppendNonBlankKey<PT, K>>
: AppendNonBlankKey<PT, PS>;
/**
* Type to split a {@link PathString} into a {@link PathTuple}.
* The individual {@link Key}s may be empty strings.
* @typeParam PS - {@link PathString} which should be split into its
* individual {@link Key}s
* @example
* ```
* SplitPathString<'foo'> = ['foo']
* SplitPathString<'foo.bar.0.baz'> = ['foo', 'bar', '0', 'baz']
* SplitPathString<'.'> = []
* ```
*/
export type SplitPathString<PS extends PathString> = SplitPathStringImpl<
PS,
[]
>;
/**
* Type to implement {@link JoinPathTuple} tail-recursively.
* @typeParam PT - remaining {@link Key}s which needs to be joined
* @typeParam PS - accumulator of the already joined {@link Key}s
*/
type JoinPathTupleImpl<
PT extends PathTuple,
PS extends PathString,
> = PT extends [infer K, ...infer R]
? JoinPathTupleImpl<AsPathTuple<R>, `${PS}.${AsKey<K>}`>
: PS;
/**
* Type to join a {@link PathTuple} to a {@link PathString}.
* @typeParam PT - {@link PathTuple} which should be joined.
* @example
* ```
* JoinPathTuple<['foo']> = 'foo'
* JoinPathTuple<['foo', 'bar', '0', 'baz']> = 'foo.bar.0.baz'
* JoinPathTuple<[]> = never
* ```
*/
export type JoinPathTuple<PT extends PathTuple> = PT extends [
infer K,
...infer R,
]
? JoinPathTupleImpl<AsPathTuple<R>, AsKey<K>>
: never;
/**
* Type which converts all keys of an object to {@link Key}s.
* @typeParam T - object type
* @example
* ```
* MapKeys<{0: string}> = {'0': string}
* ```
*/
type MapKeys<T> = { [K in keyof T as ToKey<K>]: T[K] };
/**
* Type to access a type by a key.
* - Returns undefined if it can't be indexed by that key.
* - Returns null if the type is null.
* - Returns undefined if the type is not traversable.
* @typeParam T - type which is indexed by the key
* @typeParam K - key into the type
* ```
* TryAccess<{foo: string}, 'foo'> = string
* TryAccess<{foo: string}, 'bar'> = undefined
* TryAccess<null, 'foo'> = null
* TryAccess<string, 'foo'> = undefined
* ```
*/
type TryAccess<T, K> = K extends keyof T
? T[K]
: T extends null
? null
: undefined;
/**
* Type to access an array type by a key.
* Returns undefined if the key is non-numeric.
* @typeParam T - type which is indexed by the key
* @typeParam K - key into the type
* ```
* TryAccessArray<string[], '0'> = string
* TryAccessArray<string[], 'foo'> = undefined
* ```
*/
type TryAccessArray<
T extends ReadonlyArray<any>,
K extends Key,
> = K extends `${ArrayKey}` ? T[number] : TryAccess<T, K>;
/**
* Type to evaluate the type which the given key points to.
* @typeParam T - type which is indexed by the key
* @typeParam K - key into the type
* @example
* ```
* EvaluateKey<{foo: string}, 'foo'> = string
* EvaluateKey<[number, string], '1'> = string
* EvaluateKey<string[], '1'> = string
* ```
*/
export type EvaluateKey<T, K extends Key> = T extends ReadonlyArray<any>
? IsTuple<T> extends true
? TryAccess<T, K>
: TryAccessArray<T, K>
: TryAccess<MapKeys<T>, K>;
/**
* Type to evaluate the type which the given path points to.
* @typeParam T - deeply nested type which is indexed by the path
* @typeParam PT - path into the deeply nested type
* @example
* ```
* EvaluatePath<{foo: {bar: string}}, ['foo', 'bar']> = string
* EvaluatePath<[number, string], ['1']> = string
* EvaluatePath<number, []> = number
* EvaluatePath<number, ['foo']> = undefined
* ```
*/
export type EvaluatePath<T, PT extends PathTuple> = PT extends [
infer K,
...infer R,
]
? EvaluatePath<EvaluateKey<T, AsKey<K>>, AsPathTuple<R>>
: T;
/**
* Type which given a tuple type returns its own keys, i.e. only its indices.
* @typeParam T - tuple type
* @example
* ```
* TupleKeys<[number, string]> = '0' | '1'
* ```
*/
export type TupleKeys<T extends ReadonlyArray<any>> = Exclude<
keyof T,
keyof any[]
>;
/**
* Type which extracts all numeric keys from an object.
* @typeParam T - type
* @example
* ```
* NumericObjectKeys<{0: string, '1': string, foo: string}> = '0' | '1'
* ```
*/
type NumericObjectKeys<T extends Traversable> = ToKey<
Extract<keyof T, ArrayKey | `${ArrayKey}`>
>;
/**
* Type which extracts all numeric keys from an object, tuple, or array.
* If a union is passed, it evaluates to the overlapping numeric keys.
* @typeParam T - type
* @example
* ```
* NumericKeys<{0: string, '1': string, foo: string}> = '0' | '1'
* NumericKeys<number[]> = `${number}`
* NumericKeys<[string, number]> = '0' | '1'
* NumericKeys<{0: string, '1': string} | [number] | number[]> = '0'
* ```
*/
export type NumericKeys<T extends Traversable> = UnionToIntersection<
T extends ReadonlyArray<any>
? IsTuple<T> extends true
? [TupleKeys<T>]
: [ToKey<ArrayKey>]
: [NumericObjectKeys<T>]
>[never];
/**
* Type which extracts all keys from an object.
* If a union is passed, it evaluates to the overlapping keys.
* @typeParam T - object type
* @example
* ```
* ObjectKeys<{foo: string, bar: string}, string> = 'foo' | 'bar'
* ObjectKeys<{foo: string, bar: number}, string> = 'foo'
* ```
*/
export type ObjectKeys<T extends Traversable> = Exclude<
ToKey<keyof T>,
`${string}.${string}` | ''
>;
/**
* Type to check whether a type's property matches the constraint type
* and return its key. Converts the key to a {@link Key}.
* @typeParam T - type whose property should be checked
* @typeParam K - key of the property
* @typeParam U - constraint type
* @example
* ```
* CheckKeyConstraint<{foo: string}, 'foo', string> = 'foo'
* CheckKeyConstraint<{foo: string}, 'foo', number> = never
* CheckKeyConstraint<string[], number, string> = `${number}`
* ```
*/
export type CheckKeyConstraint<T, K extends Key, U> = K extends any
? EvaluateKey<T, K> extends U
? K
: never
: never;
/**
* Type which evaluates to true when the type is an array or tuple or is a union
* which contains an array or tuple.
* @typeParam T - type
* @example
* ```
* ContainsIndexable<{foo: string}> = false
* ContainsIndexable<{foo: string} | number[]> = true
* ```
*/
export type ContainsIndexable<T> = IsNever<
Extract<T, ReadonlyArray<any>>
> extends true
? false
: true;
/**
* Type to implement {@link Keys} for non-nullable values.
* @typeParam T - non-nullable type whose property should be checked
*/
type KeysImpl<T> = [T] extends [Traversable]
? ContainsIndexable<T> extends true
? NumericKeys<T>
: ObjectKeys<T>
: never;
/**
* Type to find all properties of a type that match the constraint type
* and return their keys.
* If a union is passed, it evaluates to the overlapping keys.
* @typeParam T - type whose property should be checked
* @typeParam U - constraint type
* @example
* ```
* Keys<{foo: string, bar: string}, string> = 'foo' | 'bar'
* Keys<{foo?: string, bar?: string}> = 'foo' | 'bar'
* Keys<{foo: string, bar: number}, string> = 'foo'
* Keys<[string, number], string> = '0'
* Keys<string[], string> = `${number}`
* Keys<{0: string, '1': string} | [number] | number[]> = '0'
* ```
*/
export type Keys<T, U = unknown> = IsAny<T> extends true
? Key
: IsNever<T> extends true
? Key
: IsNever<NonNullable<T>> extends true
? never
: CheckKeyConstraint<T, KeysImpl<NonNullable<T>>, U>;
/**
* Type to check whether a {@link Key} is present in a type.
* If a union of {@link Key}s is passed, all {@link Key}s have to be present
* in the type.
* @typeParam T - type which is introspected
* @typeParam K - key
* @example
* ```
* HasKey<{foo: string}, 'foo'> = true
* HasKey<{foo: string}, 'bar'> = false
* HasKey<{foo: string}, 'foo' | 'bar'> = false
* ```
*/
export type HasKey<T, K extends Key> = IsNever<Exclude<K, Keys<T>>>;
/**
* Type to implement {@link ValidPathPrefix} tail recursively.
* @typeParam T - type which the path should be checked against
* @typeParam PT - path which should exist within the given type
* @typeParam VPT - accumulates the prefix of {@link Key}s which have been
* confirmed to exist already
*/
type ValidPathPrefixImpl<
T,
PT extends PathTuple,
VPT extends PathTuple,
> = PT extends [infer K, ...infer R]
? HasKey<T, AsKey<K>> extends true
? ValidPathPrefixImpl<
EvaluateKey<T, AsKey<K>>,
AsPathTuple<R>,
AsPathTuple<[...VPT, K]>
>
: VPT
: VPT;
/**
* Type to find the longest path prefix which is still valid,
* i.e. exists within the given type.
* @typeParam T - type which the path should be checked against
* @typeParam PT - path which should exist within the given type
* @example
* ```
* ValidPathPrefix<{foo: {bar: string}}, ['foo', 'bar']> = ['foo', 'bar']
* ValidPathPrefix<{foo: {bar: string}}, ['foo', 'ba']> = ['foo']
* ```
*/
export type ValidPathPrefix<T, PT extends PathTuple> = ValidPathPrefixImpl<
T,
PT,
[]
>;
/**
* Type to check whether a path through a type exists.
* @typeParam T - type which the path should be checked against
* @typeParam PT - path which should exist within the given type
* @example
* ```
* HasPath<{foo: {bar: string}}, ['foo', 'bar']> = true
* HasPath<{foo: {bar: string}}, ['foo', 'ba']> = false
* ```
*/
export type HasPath<T, PT extends PathTuple> = ValidPathPrefix<T, PT> extends PT
? true
: false;
/**
* Helper function to break apart T1 and check if any are equal to T2
*
* See {@link IsEqual}
*/
type AnyIsEqual<T1, T2> = T1 extends T2
? IsEqual<T1, T2> extends true
? true
: never
: never;
type HttpMethod = 'get' | 'post' | 'put' | 'delete' | 'all';
/**
* Helper type for recursively constructing paths through a type.
* This actually constructs the strings and recurses into nested
* object types.
*
* See {@link Path}
*/
type PathImpl<K extends string | number, V, TraversedTypes> = V extends
| Primitive
| BrowserNativeObject
? `${K}`
: // Check so that we don't recurse into the same type
// by ensuring that the types are mutually assignable
// mutually required to avoid false positives of subtypes
true extends AnyIsEqual<TraversedTypes, V>
? `${K}`
: true extends AnyIsEqual<HttpMethod, K>
? ``
: `${K}` | `${K}.${PathInternal<V, TraversedTypes | V>}`;
/**
* Helper type for recursively constructing paths through a type.
* This obsucres the internal type param TraversedTypes from exported contract.
*
* See {@link Path}
*/
type PathInternal<T, TraversedTypes = T> = T extends ReadonlyArray<infer V>
? IsTuple<T> extends true
? {
[K in TupleKeys<T>]-?: PathImpl<K & string, T[K], TraversedTypes>;
}[TupleKeys<T>]
: PathImpl<ArrayKey, V, TraversedTypes>
: {
[K in keyof T]-?: PathImpl<K & string, T[K], TraversedTypes>;
}[keyof T];
/**
* Type which eagerly collects all paths through a type
* @typeParam T - type which should be introspected
* @example
* ```
* Path<{foo: {bar: string}}> = 'foo' | 'foo.bar'
* ```
*/
// We want to explode the union type and process each individually
// so assignable types don't leak onto the stack from the base.
export type Path<T> = T extends any ? PathInternal<T> : never;
/**
* See {@link Path}
*/
export type FieldPath<TFieldValues extends FieldValues> = Path<TFieldValues>;
/**
* Helper type for recursively constructing paths through a type.
* This actually constructs the strings and recurses into nested
* object types.
*
* See {@link ArrayPath}
*/
type ArrayPathImpl<K extends string | number, V, TraversedTypes> = V extends
| Primitive
| BrowserNativeObject
? IsAny<V> extends true
? string
: never
: V extends ReadonlyArray<infer U>
? U extends Primitive | BrowserNativeObject
? IsAny<V> extends true
? string
: never
: // Check so that we don't recurse into the same type
// by ensuring that the types are mutually assignable
// mutually required to avoid false positives of subtypes
true extends AnyIsEqual<TraversedTypes, V>
? never
: `${K}` | `${K}.${ArrayPathInternal<V, TraversedTypes | V>}`
: true extends AnyIsEqual<TraversedTypes, V>
? never
: `${K}.${ArrayPathInternal<V, TraversedTypes | V>}`;
/**
* Helper type for recursively constructing paths through a type.
* This obsucres the internal type param TraversedTypes from exported contract.
*
* See {@link ArrayPath}
*/
type ArrayPathInternal<T, TraversedTypes = T> = T extends ReadonlyArray<infer V>
? IsTuple<T> extends true
? {
[K in TupleKeys<T>]-?: ArrayPathImpl<K & string, T[K], TraversedTypes>;
}[TupleKeys<T>]
: ArrayPathImpl<ArrayKey, V, TraversedTypes>
: {
[K in keyof T]-?: ArrayPathImpl<K & string, T[K], TraversedTypes>;
}[keyof T];
/**
* Type which eagerly collects all paths through a type which point to an array
* type.
* @typeParam T - type which should be introspected.
* @example
* ```
* Path<{foo: {bar: string[], baz: number[]}}> = 'foo.bar' | 'foo.baz'
* ```
*/
// We want to explode the union type and process each individually
// so assignable types don't leak onto the stack from the base.
export type ArrayPath<T> = T extends any ? ArrayPathInternal<T> : never;
/**
* See {@link ArrayPath}
*/
export type FieldArrayPath<TFieldValues extends FieldValues> =
ArrayPath<TFieldValues>;
/**
* Type to evaluate the type which the given path points to.
* @typeParam T - deeply nested type which is indexed by the path
* @typeParam P - path into the deeply nested type
* @example
* ```
* PathValue<{foo: {bar: string}}, 'foo.bar'> = string
* PathValue<[number, string], '1'> = string
* ```
*/
export type PathValue<T, P extends Path<T> | ArrayPath<T>> = T extends any
? P extends `${infer K}.${infer R}`
? K extends keyof T
? R extends Path<T[K]>
? PathValue<T[K], R>
: never
: K extends `${ArrayKey}`
? T extends ReadonlyArray<infer V>
? PathValue<V, R & Path<V>>
: never
: never
: P extends keyof T
? T[P]
: P extends `${ArrayKey}`
? T extends ReadonlyArray<infer V>
? V
: never
: never
: never;
/**
* See {@link PathValue}
*/
export type FieldPathValue<
TFieldValues extends FieldValues,
TFieldPath extends FieldPath<TFieldValues>,
> = PathValue<TFieldValues, TFieldPath>;
/**
* See {@link PathValue}
*/
export type FieldArrayPathValue<
TFieldValues extends FieldValues,
TFieldArrayPath extends FieldArrayPath<TFieldValues>,
> = PathValue<TFieldValues, TFieldArrayPath>;
/**
* Type to evaluate the type which the given paths point to.
* @typeParam TFieldValues - field values which are indexed by the paths
* @typeParam TPath - paths into the deeply nested field values
* @example
* ```
* FieldPathValues<{foo: {bar: string}}, ['foo', 'foo.bar']>
* = [{bar: string}, string]
* ```
*/
export type FieldPathValues<
TFieldValues extends FieldValues,
TPath extends FieldPath<TFieldValues>[] | readonly FieldPath<TFieldValues>[],
> = {} & {
[K in keyof TPath]: FieldPathValue<
TFieldValues,
TPath[K] & FieldPath<TFieldValues>
>;
};
/**
* Type which eagerly collects all paths through a fieldType that matches a give type
* @typeParam TFieldValues - field values which are indexed by the paths
* @typeParam TValue - the value you want to match into each type
* @example
* ```typescript
* FieldPathByValue<{foo: {bar: number}, baz: number, bar: string}, number>
* = 'foo.bar' | 'baz'
* ```
*/
export type FieldPathByValue<TFieldValues extends FieldValues, TValue> = {
[FieldKey in FieldPath<TFieldValues>]: FieldPathValue<
TFieldValues,
FieldKey
> extends TValue
? FieldKey
: never;
}[FieldPath<TFieldValues>];
@@ -0,0 +1,36 @@
/**
* Pulled from https://github.com/varanauskas/oatx.
*/
import type { ReferenceObject, RequestBodyObject } from 'openapi3-ts';
import type {
ComponentRef,
ComponentTypes,
ObjectWithContentSchema,
RequiredDoc,
DocOperation,
DocPath,
DocPathMethod,
DocPathTemplate,
} from './common';
type RequestBody<
Doc extends RequiredDoc,
Path extends Extract<keyof Doc['paths'], string>,
Method extends keyof Doc['paths'][Path],
> = DocOperation<Doc, Path, Method>['requestBody'] extends ReferenceObject
? 'requestBodies' extends ComponentTypes<Doc>
? ComponentRef<
Doc,
'requestBodies',
DocOperation<Doc, Path, Method>['requestBody']
>
: never
: DocOperation<Doc, Path, Method>['requestBody'];
export type RequestBodySchema<
Doc extends RequiredDoc,
Path extends DocPathTemplate<Doc>,
Method extends DocPathMethod<Doc, Path>,
> = RequestBody<Doc, DocPath<Doc, Path>, Method> extends RequestBodyObject
? ObjectWithContentSchema<Doc, RequestBody<Doc, DocPath<Doc, Path>, Method>>
: never;
@@ -0,0 +1,68 @@
/**
* Pulled from https://github.com/varanauskas/oatx.
*/
import type { ReferenceObject, ResponseObject } from 'openapi3-ts';
import type {
ComponentRef,
ComponentTypes,
ObjectWithContentSchema,
RequiredDoc,
DocOperation,
DocPath,
DocPathMethod,
DocPathTemplate,
} from './common';
type Response<
Doc extends RequiredDoc,
Path extends keyof Doc['paths'],
Method extends keyof Doc['paths'][Path],
StatusCode extends keyof Doc['paths'][Path]['responses'],
> = DocOperation<
Doc,
Path,
Method
>['responses'][StatusCode] extends ReferenceObject
? 'responses' extends ComponentTypes<Doc>
? ComponentRef<
Doc,
'responses',
DocOperation<Doc, Path, Method>['responses'][StatusCode]
>
: never
: DocOperation<Doc, Path, Method>['responses'][StatusCode];
type Responses<
Doc extends RequiredDoc,
Path extends keyof Doc['paths'],
Method extends keyof Doc['paths'][Path],
> = {
[StatusCode in keyof DocOperation<Doc, Path, Method>['responses']]: Response<
Doc,
Path,
Method,
StatusCode
>;
};
export type ResponseSchema<
Doc extends RequiredDoc,
Object extends ResponseObject,
> = ObjectWithContentSchema<Doc, Object>;
export type ResponseSchemas<
Doc extends RequiredDoc,
Path extends DocPathTemplate<Doc>,
Method extends DocPathMethod<Doc, Path>,
> = {
[StatusCode in keyof Responses<Doc, DocPath<Doc, Path>, Method>]: Responses<
Doc,
DocPath<Doc, Path>,
Method
>[StatusCode] extends ResponseObject
? ResponseSchema<
Doc,
Responses<Doc, DocPath<Doc, Path>, Method>[StatusCode]
>
: never;
};
@@ -1,144 +0,0 @@
/*
* Copyright 2023 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
declare const $NestedValue: unique symbol;
/**
* @deprecated to be removed in the next major version
*/
export type NestedValue<TValue extends object = object> = {
[$NestedValue]: never;
} & TValue;
/*
Projects that React Hook Form installed don't include the DOM library need these interfaces to compile.
React Native applications is no DOM available. The JavaScript runtime is ES6/ES2015 only.
These definitions allow such projects to compile with only --lib ES6.
Warning: all of these interfaces are empty.
If you want type definitions for various properties, you need to add `--lib DOM` (via command line or tsconfig.json).
*/
export type Noop = () => void;
interface File extends Blob {
readonly lastModified: number;
readonly name: string;
}
interface FileList {
readonly length: number;
item(index: number): File | null;
[index: number]: File;
}
export type Primitive =
| null
| undefined
| string
| number
| boolean
| symbol
| bigint;
export type BrowserNativeObject = Date | FileList | File;
export type EmptyObject = { [K in string | number]: never };
export type NonUndefined<T> = T extends undefined ? never : T;
export type LiteralUnion<T extends U, U extends Primitive> =
| T
| (U & { _?: never });
export type DeepPartial<T> = T extends BrowserNativeObject | NestedValue
? T
: { [K in keyof T]?: DeepPartial<T[K]> };
export type DeepPartialSkipArrayKey<T> = T extends
| BrowserNativeObject
| NestedValue
? T
: T extends ReadonlyArray<any>
? { [K in keyof T]: DeepPartialSkipArrayKey<T[K]> }
: { [K in keyof T]?: DeepPartialSkipArrayKey<T[K]> };
/**
* Checks whether the type is any
* See {@link https://stackoverflow.com/a/49928360/3406963}
* @typeParam T - type which may be any
* ```
* IsAny<any> = true
* IsAny<string> = false
* ```
*/
export type IsAny<T> = 0 extends 1 & T ? true : false;
/**
* Checks whether the type is never
* @typeParam T - type which may be never
* ```
* IsAny<never> = true
* IsAny<string> = false
* ```
*/
export type IsNever<T> = [T] extends [never] ? true : false;
/**
* Checks whether T1 can be exactly (mutually) assigned to T2
* @typeParam T1 - type to check
* @typeParam T2 - type to check against
* ```
* IsEqual<string, string> = true
* IsEqual<'foo', 'foo'> = true
* IsEqual<string, number> = false
* IsEqual<string, number> = false
* IsEqual<string, 'foo'> = false
* IsEqual<'foo', string> = false
* IsEqual<'foo' | 'bar', 'foo'> = boolean // 'foo' is assignable, but 'bar' is not (true | false) -> boolean
* ```
*/
export type IsEqual<T1, T2> = T1 extends T2
? (<G>() => G extends T1 ? 1 : 2) extends <G>() => G extends T2 ? 1 : 2
? true
: false
: false;
export type DeepMap<T, TValue> = IsAny<T> extends true
? any
: T extends BrowserNativeObject | NestedValue
? TValue
: T extends object
? { [K in keyof T]: DeepMap<NonUndefined<T[K]>, TValue> }
: TValue;
export type IsFlatObject<T extends object> = Extract<
Exclude<T[keyof T], NestedValue | Date | FileList>,
any[] | object
> extends never
? true
: false;
export type Merge<A, B> = {
[K in keyof A | keyof B]?: K extends keyof A & keyof B
? [A[K], B[K]] extends [object, object]
? Merge<A[K], B[K]>
: A[K] | B[K]
: K extends keyof A
? A[K]
: K extends keyof B
? B[K]
: never;
};
+11
View File
@@ -7454,6 +7454,8 @@ __metadata:
express: ^4.18.2
json-schema-to-ts: ^2.6.2
openapi-types: ^12.1.0
openapi3-ts: ^3.1.2
ts-node: ^10.9.1
languageName: unknown
linkType: soft
@@ -32110,6 +32112,15 @@ __metadata:
languageName: node
linkType: hard
"openapi3-ts@npm:^3.1.2":
version: 3.1.2
resolution: "openapi3-ts@npm:3.1.2"
dependencies:
yaml: ^2.1.3
checksum: 77e5064ea9bff119ba8d8398a90246ae99174d6c23b2ed68355bb48be3243b6f4f0041a3fdcd9dba5ffe6ace786a8041d86d8a4049d65d2d40cd4bfca5bba459
languageName: node
linkType: hard
"openid-client@npm:^5.2.1, openid-client@npm:^5.3.0":
version: 5.4.0
resolution: "openid-client@npm:5.4.0"