initial work

Signed-off-by: aramissennyeydd <aramis.sennyey@doordash.com>
Signed-off-by: Aramis Sennyey <159921952+aramissennyeydd@users.noreply.github.com>
Signed-off-by: aramissennyeydd <aramis.sennyey@doordash.com>
This commit is contained in:
Aramis
2024-01-21 10:35:02 -05:00
committed by aramissennyeydd
parent 16b8fb9dc6
commit ddf9e55c3a
28 changed files with 668 additions and 18 deletions
@@ -31,5 +31,10 @@ export type {
PathParameters,
} from './utility';
export type { ApiRouter } from './router';
export type {
RequestMatcherByModelAndPath,
RequestMatcherByModelAndPathParams,
} from './types/express';
export type { PathTemplate } from './types/common';
export { createValidatedOpenApiRouter, getOpenApiSpecRoute } from './stub';
export { wrapInOpenApiTestServer, wrapServer } from './testUtils';
@@ -100,3 +100,29 @@ export interface DocRequestMatcher<
>
): T;
}
export type RequestMatcherByModelAndPath<
PathParams,
QueryParams,
RequestBody,
ResponseBody,
> = core.RequestHandler<
PathParams,
ResponseBody,
RequestBody,
QueryParams,
Record<string, string>
>;
export type RequestMatcherByModelAndPathParams<
PathParams,
QueryParams,
RequestBody,
ResponseBody,
> = core.RequestHandlerParams<
PathParams,
ResponseBody,
RequestBody,
QueryParams,
Record<string, string>
>;
+3 -1
View File
@@ -21,4 +21,6 @@
*/
export { CatalogClient } from './CatalogClient';
export * from './types';
// export * from './types';
export * from './generated/models';
@@ -0,0 +1,23 @@
*.md
apis/baseapi.ts
apis/exception.ts
auth/*
http/*
middleware.ts
servers.ts
util.ts
configuration.ts
rxjsStub.ts
.gitignore
apis/*.ts
!apis/*.client.ts
models/*.ts
!models/*.model.ts
!index.ts
!**/index.ts
types/ObjectParamAPI.ts
types/ObservableAPI.ts
types/PromiseAPI.ts
git_push.sh
package.json
tsconfig.json
@@ -0,0 +1,101 @@
/*
* 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 chalk from 'chalk';
import { resolve } from 'path';
import { OPENAPI_IGNORE_FILES, OUTPUT_PATH } from '../constants';
import { paths as cliPaths } from '../../../lib/paths';
import { mkdirpSync } from 'fs-extra';
import fs from 'fs-extra';
import { exec } from '../../../lib/exec';
import { resolvePackagePath } from '@backstage/backend-common';
async function generate(spec: string, outputDirectory: string) {
const resolvedOpenapiPath = resolve(spec);
const resolvedOutputDirectory = resolve(outputDirectory, OUTPUT_PATH);
mkdirpSync(resolvedOutputDirectory);
await fs.mkdirp(resolvedOutputDirectory);
await fs.writeFile(
resolve(resolvedOutputDirectory, '.openapi-generator-ignore'),
OPENAPI_IGNORE_FILES.join('\n'),
);
await exec(
'node',
[
resolvePackagePath('@openapitools/openapi-generator-cli', 'main.js'),
'generate',
'-i',
resolvedOpenapiPath,
'-o',
resolvedOutputDirectory,
'-g',
'typescript',
'-c',
resolvePackagePath(
'@backstage/repo-tools',
'templates/typescript-backstage.server.yaml',
),
`--additional-properties=clientPackageName=@backstage/catalog-client`,
'--generator-key',
'v3.0',
],
{
maxBuffer: Number.MAX_VALUE,
cwd: resolvePackagePath('@backstage/repo-tools'),
env: {
...process.env,
},
},
);
await exec(
`yarn backstage-cli package lint --fix ${resolvedOutputDirectory}`,
);
const prettier = cliPaths.resolveTargetRoot('node_modules/.bin/prettier');
if (prettier) {
await exec(`${prettier} --write ${resolvedOutputDirectory}`);
}
fs.removeSync(resolve(resolvedOutputDirectory, '.openapi-generator-ignore'));
fs.rmSync(resolve(resolvedOutputDirectory, '.openapi-generator'), {
recursive: true,
force: true,
});
}
export async function singleCommand({
inputSpec,
outputDirectory,
}: {
inputSpec: string;
outputDirectory: string;
}): Promise<void> {
try {
await generate(inputSpec, outputDirectory);
console.log(chalk.green(`Generated client for ${inputSpec}`));
} catch (err) {
console.log();
console.log(chalk.red(`Client generation failed in ${outputDirectory}:`));
console.log(err);
process.exit(1);
}
}
@@ -63,7 +63,7 @@ async function generate(
'-c',
resolvePackagePath(
'@backstage/repo-tools',
'templates/typescript-backstage.yaml',
'templates/typescript-backstage.client.yaml',
),
'--generator-key',
'v3.0',
@@ -1,35 +1,35 @@
templateDir: templates/typescript-backstage
files:
api.mustache:
client/api.mustache:
templateType: API
# For some reason, they check for destinationFilename differences. We have to change the ending to override the file.
destinationFilename: .client.ts
model.mustache:
client/model.mustache:
templateType: Model
destinationFilename: .model.ts
modelGeneric.mustache:
client/models/modelGeneric.mustache:
templateType: SupportingFiles
modelOneOf.mustache:
client/models/modelOneOf.mustache:
templateType: SupportingFiles
modelGenericAdditionalProperties.mustache:
client/models/modelGenericAdditionalProperties.mustache:
templateType: SupportingFiles
modelGenericEnums.mustache:
client/models/ modelGenericEnums.mustache:
templateType: SupportingFiles
modelAlias.mustache:
client/models/modelAlias.mustache:
templateType: SupportingFiles
modelEnum.mustache:
client/models/modelEnum.mustache:
templateType: SupportingFiles
modelTaggedUnion.mustache:
client/models/modelTaggedUnion.mustache:
templateType: SupportingFiles
models/models_all.mustache:
client/models/models_all.mustache:
templateType: SupportingFiles
destinationFilename: models/index.ts
types/fetch.ts: {}
types/discovery.ts: {}
apis/index.mustache:
client/types/fetch.ts: {}
client/types/discovery.ts: {}
client/apis/index.mustache:
templateType: SupportingFiles
destinationFilename: apis/index.ts
pluginId.mustache:
client/pluginId.mustache:
templateType: SupportingFiles
destinationFilename: pluginId.ts
@@ -0,0 +1,10 @@
templateDir: templates/typescript-backstage
files:
server/api.mustache:
templateType: API
# For some reason, they check for destinationFilename differences. We have to change the ending to override the file.
destinationFilename: .client.ts
client/apis/index.mustache:
templateType: SupportingFiles
destinationFilename: apis/index.ts
@@ -0,0 +1,45 @@
{{>licenseInfo}}
import type {
PathTemplate,RequestMatcherByModelAndPath,RequestMatcherByModelAndPathParams
} from '@backstage/backend-openapi-utils';
import type {
{{#imports}}
{{classname}},
{{/imports}}
} from '{{clientPackageName}}';
import {Router} from 'express';
type ExtendsString<Path extends string> = Path;
{{#operations}}
/**
* {{{description}}}{{^description}}no description{{/description}}
*/
{{! export interface TypedRouter extends Router { }}
type InputOutput = {
{{#operation}}
'#{{httpMethod}}|{{path}}': {
path: {
{{#pathParams}}
{{paramName}}{{^required}}?{{/required}}: {{{dataType}}},
{{/pathParams}}
},
query: {
{{#queryParams}}
{{paramName}}{{^required}}?{{/required}}: {{{dataType}}},
{{/queryParams}}
},
{{#bodyParam}}
body: {{{dataType}}},
{{/bodyParam}}
response: {{{returnType}}}{{^returnType}}void{{/returnType}},
},
{{/operation}}
}
{{! } }}
{{/operations}}
@@ -0,0 +1,61 @@
{{#lambda.lowercase}}{{httpMethod}}({{/lambda.lowercase}}
path: ExtendsString<PathTemplate<`{{path}}`>>,
...handlers: Array<RequestMatcherByModelAndPath<
{
{{#pathParams}}
{{paramName}}{{^required}}?{{/required}}: {{{dataType}}},
{{/pathParams}}
},
{
{{#queryParams}}
{{paramName}}{{^required}}?{{/required}}: {{{dataType}}},
{{/queryParams}}
},
{{#hasBodyParam}}
{{#bodyParam}}
{{{dataType}}}
{{/bodyParam}}
{{/hasBodyParam}}
{{^hasBodyParam}}
{}
{{/hasBodyParam}},
{{#responses}}
{{^-first}}
|
{{/-first}}
/**
* {{code}} {{message}}
*/
{{#dataType}}
{{{dataType}}}
{{/dataType}}
{{/responses}}
>>
): this;
{{#lambda.lowercase}}{{httpMethod}}({{/lambda.lowercase}}
path: ExtendsString<PathTemplate<`{{path}}`>>,
...handlers: Array<RequestMatcherByModelAndPathParams<
{
{{#pathParams}}
{{paramName}}{{^required}}?{{/required}}: {{{dataType}}},
{{/pathParams}}
},
{
{{#queryParams}}
{{paramName}}{{^required}}?{{/required}}: {{{dataType}}},
{{/queryParams}}
},
{{#hasBodyParam}}
{{#bodyParam}}
{{{dataType}}}
{{/bodyParam}}
{{/hasBodyParam}}
{{^hasBodyParam}}
{}
{{/hasBodyParam}},
{{{returnType}}}{{^returnType}}void{{/returnType}}
>>
): this;
+1
View File
@@ -80,6 +80,7 @@
"@types/express": "^4.17.6",
"codeowners-utils": "^1.0.2",
"core-js": "^3.6.5",
"cross-fetch": "^4.0.0",
"express": "^4.17.1",
"fast-json-stable-stringify": "^2.1.0",
"fs-extra": "^11.2.0",
@@ -0,0 +1,23 @@
*.md
apis/baseapi.ts
apis/exception.ts
auth/*
http/*
middleware.ts
servers.ts
util.ts
configuration.ts
rxjsStub.ts
.gitignore
apis/*.ts
!apis/*.client.ts
models/*.ts
!models/*.model.ts
!index.ts
!**/index.ts
types/ObjectParamAPI.ts
types/ObservableAPI.ts
types/PromiseAPI.ts
git_push.sh
package.json
tsconfig.json
@@ -0,0 +1,3 @@
apis/DefaultApi.client.ts
apis/index.ts
index.ts
@@ -0,0 +1 @@
6.5.0
@@ -0,0 +1,301 @@
/*
* Copyright 2024 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 type core from 'express-serve-static-core';
import type {
AnalyzeLocationRequest,
AnalyzeLocationResponse,
CreateLocation201Response,
CreateLocationRequest,
EntitiesBatchResponse,
EntitiesQueryResponse,
Entity,
EntityAncestryResponse,
EntityFacetsResponse,
GetEntitiesByRefsRequest,
GetLocations200ResponseInner,
Location,
RefreshEntityRequest,
ValidateEntityRequest,
} from '@backstage/catalog-client';
import { Router } from 'express';
/**
* no description
*/
type InputOutput = {
'#POST|/analyze-location': {
path: {
},
query: {
},
body: AnalyzeLocationRequest,
response: AnalyzeLocationResponse,
},
'#POST|/locations': {
path: {
},
query: {
dryRun?: string,
},
body: CreateLocationRequest,
response: CreateLocation201Response,
},
'#DELETE|/entities/by-uid/{uid}': {
path: {
uid: string,
},
query: {
},
response: void,
},
'#DELETE|/locations/{id}': {
path: {
id: string,
},
query: {
},
response: void,
},
'#GET|/entities': {
path: {
},
query: {
fields?: Array<string>,
limit?: number,
filter?: Array<string>,
offset?: number,
after?: string,
order?: Array<string>,
},
response: Array<Entity>,
},
'#GET|/entities/by-query': {
path: {
},
query: {
fields?: Array<string>,
limit?: number,
orderField?: Array<string>,
cursor?: string,
filter?: Array<string>,
fullTextFilterTerm?: string,
fullTextFilterFields?: Array<string>,
},
response: EntitiesQueryResponse,
},
'#POST|/entities/by-refs': {
path: {
},
query: {
},
body: GetEntitiesByRefsRequest,
response: EntitiesBatchResponse,
},
'#GET|/entities/by-name/{kind}/{namespace}/{name}/ancestry': {
path: {
kind: string,
namespace: string,
name: string,
},
query: {
},
response: EntityAncestryResponse,
},
'#GET|/entities/by-name/{kind}/{namespace}/{name}': {
path: {
kind: string,
namespace: string,
name: string,
},
query: {
},
response: Entity,
},
'#GET|/entities/by-uid/{uid}': {
path: {
uid: string,
},
query: {
},
response: Entity,
},
'#GET|/entity-facets': {
path: {
},
query: {
facet: Array<string>,
filter?: Array<string>,
},
response: EntityFacetsResponse,
},
'#GET|/locations/{id}': {
path: {
id: string,
},
query: {
},
response: Location,
},
'#GET|/locations': {
path: {
},
query: {
},
response: Array<GetLocations200ResponseInner>,
},
'#POST|/refresh': {
path: {
},
query: {
},
body: RefreshEntityRequest,
response: void,
},
'#POST|/validate-entity': {
path: {
},
query: {
},
body: ValidateEntityRequest,
response: void,
},
};
type ValueOf<T> = T[keyof T];
type Filter<T, K> = T extends K ? K : never;
type ExtractDocPath<Path extends string> = Path extends `#${string}|${infer OpenApiPath}` ? OpenApiPath : never;
type ExtractDocMethod<Path extends string> = Path extends `#${infer Method}|${string}` ? Method : never
type DocPath<Doc extends InputOutput> = ExtractDocPath<Filter<keyof Doc, string>>;
type DocPathMethod<Doc extends InputOutput, Path extends string> = ExtractDocMethod<Path>;
/**
* Typed express request handler.
* @public
*/
export type DocRequestHandler<
Doc extends InputOutput,
Path extends DocPath<Doc>,
Method extends DocPathMethod<Doc, Path>,
> = core.RequestHandler<
PathSchema<Doc, Path, Method>,
ResponseBodyToJsonSchema<Doc, Path, Method>,
RequestBodyToJsonSchema<Doc, Path, Method>,
QuerySchema<Doc, Path, Method>,
Record<string, string>
>;
/**
* Typed express error handler / request handler union type.
* @public
*/
export type DocRequestHandlerParams<
Doc extends RequiredDoc,
Path extends DocPath<Doc>,
Method extends DocPathMethod<Doc, Path>,
> = core.RequestHandlerParams<
PathSchema<Doc, Path, Method>,
ResponseBodyToJsonSchema<Doc, Path, Method>,
RequestBodyToJsonSchema<Doc, Path, Method>,
QuerySchema<Doc, Path, Method>,
Record<string, string>
>;
/**
* Superset of the express router path matcher that enforces typed request and response bodies.
* @public
*/
export interface DocRequestMatcher<
Doc extends RequiredDoc,
T,
Method extends
| 'all'
| 'get'
| 'post'
| 'put'
| 'delete'
| 'patch'
| 'options'
| 'head',
> {
<
Path extends MethodAwareDocPath<
Doc,
PathTemplate<Extract<keyof Doc['paths'], string>>,
Method
>,
>(
path: Path,
...handlers: Array<
DocRequestHandler<Doc, TemplateToDocPath<Doc, Path>, Method>
>
): T;
<
Path extends MethodAwareDocPath<
Doc,
PathTemplate<Extract<keyof Doc['paths'], string>>,
Method
>,
>(
path: Path,
...handlers: Array<
DocRequestHandlerParams<Doc, TemplateToDocPath<Doc, Path>, Method>
>
): T;
}
export interface TypedRouter extends Router {
get:
}
@@ -0,0 +1,18 @@
/*
* Copyright 2024 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.
*/
export * from './DefaultApi.client';
@@ -0,0 +1,27 @@
/*
* Copyright 2024 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.
*/
export * from "./http/http";
export * from "./auth/auth";
export * from "./models/all";
export { createConfiguration } from "./configuration"
export { Configuration } from "./configuration"
export * from "./apis/exception";
export * from "./servers";
export { RequiredError } from "./apis/baseapi";
export { PromiseMiddleware as Middleware } from './middleware';
export { PromiseDefaultApi as DefaultApi } from './types/PromiseAPI';
@@ -18,6 +18,7 @@
// * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. *
// ******************************************************************
import { createValidatedOpenApiRouter } from '@backstage/backend-openapi-utils';
import { TypedRouter } from '../generated/apis';
export const spec = {
openapi: '3.0.3',
@@ -57,6 +57,7 @@ import {
} from '@backstage/backend-plugin-api';
import { LocationAnalyzer } from '@backstage/plugin-catalog-node';
import { AuthorizedValidationService } from './AuthorizedValidationService';
import { TypedRouter } from '../generated/apis';
/**
* Options used by {@link createRouter}.
@@ -85,13 +86,13 @@ export interface RouterOptions {
export async function createRouter(
options: RouterOptions,
): Promise<express.Router> {
const router = await createOpenApiRouter({
const router = (await createOpenApiRouter({
validatorOptions: {
// We want the spec to be up to date with the expected value, but the return type needs
// to be controlled by the router implementation not the request validator.
ignorePaths: /^\/validate-entity\/?$/,
},
});
})) as TypedRouter;
const {
entitiesCatalog,
locationAnalyzer,
+1
View File
@@ -6038,6 +6038,7 @@ __metadata:
better-sqlite3: ^11.0.0
codeowners-utils: ^1.0.2
core-js: ^3.6.5
cross-fetch: ^4.0.0
express: ^4.17.1
fast-json-stable-stringify: ^2.1.0
fs-extra: ^11.2.0