chore: address comments

This commit is contained in:
Fredrik Adelöw
2020-09-30 13:43:25 +02:00
parent c6ba9cba49
commit 7ec6dc7f30
27 changed files with 608 additions and 413 deletions
+12 -12
View File
@@ -23,12 +23,12 @@ import {
SchemaValidEntityPolicy,
} from './entity';
import {
ApiEntityV1alpha1Policy,
ComponentEntityV1alpha1Policy,
GroupEntityV1alpha1Policy,
LocationEntityV1alpha1Policy,
TemplateEntityV1alpha1Policy,
UserEntityV1alpha1Policy,
apiEntityV1alpha1Policy,
componentEntityV1alpha1Policy,
groupEntityV1alpha1Policy,
locationEntityV1alpha1Policy,
templateEntityV1alpha1Policy,
userEntityV1alpha1Policy,
} from './kinds';
import { EntityPolicy } from './types';
@@ -78,12 +78,12 @@ export class EntityPolicies implements EntityPolicy {
new ReservedFieldsEntityPolicy(),
]),
EntityPolicies.anyOf([
new ComponentEntityV1alpha1Policy(),
new GroupEntityV1alpha1Policy(),
new UserEntityV1alpha1Policy(),
new LocationEntityV1alpha1Policy(),
new TemplateEntityV1alpha1Policy(),
new ApiEntityV1alpha1Policy(),
componentEntityV1alpha1Policy,
groupEntityV1alpha1Policy,
userEntityV1alpha1Policy,
locationEntityV1alpha1Policy,
templateEntityV1alpha1Policy,
apiEntityV1alpha1Policy,
]),
]);
}
@@ -14,15 +14,13 @@
* limitations under the License.
*/
import { EntityPolicy } from '../types';
import {
ApiEntityV1alpha1,
ApiEntityV1alpha1Policy,
apiEntityV1alpha1Policy as policy,
} from './ApiEntityV1alpha1';
describe('ApiV1alpha1Policy', () => {
let entity: ApiEntityV1alpha1;
let policy: EntityPolicy;
beforeEach(() => {
entity = {
@@ -74,7 +72,6 @@ components:
`,
},
};
policy = new ApiEntityV1alpha1Policy();
});
it('happy path: accepts valid data', async () => {
@@ -16,11 +16,24 @@
import * as yup from 'yup';
import type { Entity } from '../entity/Entity';
import type { EntityPolicy } from '../types';
import { schemaPolicy } from './util';
const API_VERSION = ['backstage.io/v1alpha1', 'backstage.io/v1beta1'] as const;
const KIND = 'API' as const;
const schema = yup.object<Partial<ApiEntityV1alpha1>>({
apiVersion: yup.string().required().oneOf(API_VERSION),
kind: yup.string().required().equals([KIND]),
spec: yup
.object({
type: yup.string().required().min(1),
lifecycle: yup.string().required().min(1),
owner: yup.string().required().min(1),
definition: yup.string().required().min(1),
})
.required(),
});
export interface ApiEntityV1alpha1 extends Entity {
apiVersion: typeof API_VERSION[number];
kind: typeof KIND;
@@ -32,31 +45,4 @@ export interface ApiEntityV1alpha1 extends Entity {
};
}
export class ApiEntityV1alpha1Policy implements EntityPolicy {
private schema: yup.Schema<any>;
constructor() {
this.schema = yup.object<Partial<ApiEntityV1alpha1>>({
apiVersion: yup.string().required().oneOf(API_VERSION),
kind: yup.string().required().equals([KIND]),
spec: yup
.object({
type: yup.string().required().min(1),
lifecycle: yup.string().required().min(1),
owner: yup.string().required().min(1),
definition: yup.string().required().min(1),
})
.required(),
});
}
async enforce(envelope: Entity): Promise<Entity | undefined> {
if (
KIND !== envelope.kind ||
!API_VERSION.includes(envelope.apiVersion as any)
) {
return undefined;
}
return await this.schema.validate(envelope, { strict: true });
}
}
export const apiEntityV1alpha1Policy = schemaPolicy(KIND, API_VERSION, schema);
@@ -14,15 +14,13 @@
* limitations under the License.
*/
import { EntityPolicy } from '../types';
import {
ComponentEntityV1alpha1,
ComponentEntityV1alpha1Policy,
componentEntityV1alpha1Policy as policy,
} from './ComponentEntityV1alpha1';
describe('ComponentV1alpha1Policy', () => {
let entity: ComponentEntityV1alpha1;
let policy: EntityPolicy;
beforeEach(() => {
entity = {
@@ -38,7 +36,6 @@ describe('ComponentV1alpha1Policy', () => {
implementsApis: ['api-0'],
},
};
policy = new ComponentEntityV1alpha1Policy();
});
it('happy path: accepts valid data', async () => {
@@ -16,11 +16,24 @@
import * as yup from 'yup';
import type { Entity } from '../entity/Entity';
import type { EntityPolicy } from '../types';
import { schemaPolicy } from './util';
const API_VERSION = ['backstage.io/v1alpha1', 'backstage.io/v1beta1'] as const;
const KIND = 'Component' as const;
const schema = yup.object<Partial<ComponentEntityV1alpha1>>({
apiVersion: yup.string().required().oneOf(API_VERSION),
kind: yup.string().required().equals([KIND]),
spec: yup
.object({
type: yup.string().required().min(1),
lifecycle: yup.string().required().min(1),
owner: yup.string().required().min(1),
implementsApis: yup.array(yup.string()).notRequired(),
})
.required(),
});
export interface ComponentEntityV1alpha1 extends Entity {
apiVersion: typeof API_VERSION[number];
kind: typeof KIND;
@@ -32,31 +45,8 @@ export interface ComponentEntityV1alpha1 extends Entity {
};
}
export class ComponentEntityV1alpha1Policy implements EntityPolicy {
private schema: yup.Schema<any>;
constructor() {
this.schema = yup.object<Partial<ComponentEntityV1alpha1>>({
apiVersion: yup.string().required().oneOf(API_VERSION),
kind: yup.string().required().equals([KIND]),
spec: yup
.object({
type: yup.string().required().min(1),
lifecycle: yup.string().required().min(1),
owner: yup.string().required().min(1),
implementsApis: yup.array(yup.string()).notRequired(),
})
.required(),
});
}
async enforce(envelope: Entity): Promise<Entity | undefined> {
if (
KIND !== envelope.kind ||
!API_VERSION.includes(envelope.apiVersion as any)
) {
return undefined;
}
return await this.schema.validate(envelope, { strict: true });
}
}
export const componentEntityV1alpha1Policy = schemaPolicy(
KIND,
API_VERSION,
schema,
);
@@ -14,15 +14,13 @@
* limitations under the License.
*/
import { EntityPolicy } from '../types';
import {
GroupEntityV1alpha1,
GroupEntityV1alpha1Policy,
groupEntityV1alpha1Policy as policy,
} from './GroupEntityV1alpha1';
describe('GroupV1alpha1Policy', () => {
let entity: GroupEntityV1alpha1;
let policy: EntityPolicy;
beforeEach(() => {
entity = {
@@ -41,7 +39,6 @@ describe('GroupV1alpha1Policy', () => {
descendants: ['desc-a', 'desc-b'],
},
};
policy = new GroupEntityV1alpha1Policy();
});
it('happy path: accepts valid data', async () => {
@@ -16,11 +16,39 @@
import * as yup from 'yup';
import type { Entity } from '../entity/Entity';
import type { EntityPolicy } from '../types';
import { schemaPolicy } from './util';
const API_VERSION = ['backstage.io/v1alpha1', 'backstage.io/v1beta1'] as const;
const KIND = 'Group' as const;
const schema = yup.object<Partial<GroupEntityV1alpha1>>({
apiVersion: yup.string().required().oneOf(API_VERSION),
kind: yup.string().required().equals([KIND]),
spec: yup
.object({
type: yup.string().required().min(1),
parent: yup.string().notRequired().min(1),
// Use these manual tests because yup .required() requires at least
// one element and there is no simple workaround -_-
ancestors: yup.array(yup.string()).test({
name: 'isDefined',
message: 'ancestors must be defined',
test: v => Boolean(v),
}),
children: yup.array(yup.string()).test({
name: 'isDefined',
message: 'children must be defined',
test: v => Boolean(v),
}),
descendants: yup.array(yup.string()).test({
name: 'isDefined',
message: 'descendants must be defined',
test: v => Boolean(v),
}),
})
.required(),
});
export interface GroupEntityV1alpha1 extends Entity {
apiVersion: typeof API_VERSION[number];
kind: typeof KIND;
@@ -33,46 +61,8 @@ export interface GroupEntityV1alpha1 extends Entity {
};
}
export class GroupEntityV1alpha1Policy implements EntityPolicy {
private schema: yup.Schema<any>;
constructor() {
this.schema = yup.object<Partial<GroupEntityV1alpha1>>({
apiVersion: yup.string().required().oneOf(API_VERSION),
kind: yup.string().required().equals([KIND]),
spec: yup
.object({
type: yup.string().required().min(1),
parent: yup.string().notRequired().min(1),
// Use these manual tests because yup .required() requires at least
// one element and there is no simple workaround -_-
ancestors: yup.array(yup.string()).test({
name: 'isDefined',
message: 'ancestors must be defined',
test: v => Boolean(v),
}),
children: yup.array(yup.string()).test({
name: 'isDefined',
message: 'children must be defined',
test: v => Boolean(v),
}),
descendants: yup.array(yup.string()).test({
name: 'isDefined',
message: 'descendants must be defined',
test: v => Boolean(v),
}),
})
.required(),
});
}
async enforce(envelope: Entity): Promise<Entity | undefined> {
if (
KIND !== envelope.kind ||
!API_VERSION.includes(envelope.apiVersion as any)
) {
return undefined;
}
return await this.schema.validate(envelope, { strict: true });
}
}
export const groupEntityV1alpha1Policy = schemaPolicy(
KIND,
API_VERSION,
schema,
);
@@ -14,15 +14,13 @@
* limitations under the License.
*/
import { EntityPolicy } from '../types';
import {
LocationEntityV1alpha1,
LocationEntityV1alpha1Policy,
locationEntityV1alpha1Policy as policy,
} from './LocationEntityV1alpha1';
describe('LocationV1alpha1Policy', () => {
let entity: LocationEntityV1alpha1;
let policy: EntityPolicy;
beforeEach(() => {
entity = {
@@ -35,7 +33,6 @@ describe('LocationV1alpha1Policy', () => {
type: 'github',
},
};
policy = new LocationEntityV1alpha1Policy();
});
it('happy path: accepts valid data', async () => {
@@ -16,11 +16,23 @@
import * as yup from 'yup';
import type { Entity } from '../entity/Entity';
import type { EntityPolicy } from '../types';
import { schemaPolicy } from './util';
const API_VERSION = ['backstage.io/v1alpha1', 'backstage.io/v1beta1'] as const;
const KIND = 'Location' as const;
const schema = yup.object<Partial<LocationEntityV1alpha1>>({
apiVersion: yup.string().required().oneOf(API_VERSION),
kind: yup.string().required().equals([KIND]),
spec: yup
.object({
type: yup.string().required().min(1),
target: yup.string().notRequired().min(1),
targets: yup.array(yup.string()).notRequired(),
})
.required(),
});
export interface LocationEntityV1alpha1 extends Entity {
apiVersion: typeof API_VERSION[number];
kind: typeof KIND;
@@ -31,30 +43,8 @@ export interface LocationEntityV1alpha1 extends Entity {
};
}
export class LocationEntityV1alpha1Policy implements EntityPolicy {
private schema: yup.Schema<any>;
constructor() {
this.schema = yup.object<Partial<LocationEntityV1alpha1>>({
apiVersion: yup.string().required().oneOf(API_VERSION),
kind: yup.string().required().equals([KIND]),
spec: yup
.object({
type: yup.string().required().min(1),
target: yup.string().notRequired().min(1),
targets: yup.array(yup.string()).notRequired(),
})
.required(),
});
}
async enforce(envelope: Entity): Promise<Entity | undefined> {
if (
KIND !== envelope.kind ||
!API_VERSION.includes(envelope.apiVersion as any)
) {
return undefined;
}
return await this.schema.validate(envelope, { strict: true });
}
}
export const locationEntityV1alpha1Policy = schemaPolicy(
KIND,
API_VERSION,
schema,
);
@@ -14,15 +14,13 @@
* limitations under the License.
*/
import { EntityPolicy } from '../types';
import {
TemplateEntityV1alpha1,
TemplateEntityV1alpha1Policy,
templateEntityV1alpha1Policy as policy,
} from './TemplateEntityV1alpha1';
describe('TemplateEntityV1alpah1', () => {
describe('templateEntityV1alpha1', () => {
let entity: TemplateEntityV1alpha1;
let policy: EntityPolicy;
beforeEach(() => {
entity = {
@@ -52,7 +50,6 @@ describe('TemplateEntityV1alpah1', () => {
},
},
};
policy = new TemplateEntityV1alpha1Policy();
});
it('happy path: accepts valid data', async () => {
@@ -16,11 +16,25 @@
import * as yup from 'yup';
import type { Entity } from '../entity/Entity';
import type { EntityPolicy, JSONSchema } from '../types';
import type { JSONSchema } from '../types';
import { schemaPolicy } from './util';
const API_VERSION = ['backstage.io/v1alpha1', 'backstage.io/v1beta1'] as const;
const KIND = 'Template' as const;
const schema = yup.object<Partial<TemplateEntityV1alpha1>>({
apiVersion: yup.string().required().oneOf(API_VERSION),
kind: yup.string().required().equals([KIND]),
spec: yup
.object({
type: yup.string().required().min(1),
path: yup.string(),
schema: yup.object().required(),
templater: yup.string().required(),
})
.required(),
});
export interface TemplateEntityV1alpha1 extends Entity {
apiVersion: typeof API_VERSION[number];
kind: typeof KIND;
@@ -32,31 +46,8 @@ export interface TemplateEntityV1alpha1 extends Entity {
};
}
export class TemplateEntityV1alpha1Policy implements EntityPolicy {
private schema: yup.Schema<any>;
constructor() {
this.schema = yup.object<Partial<TemplateEntityV1alpha1>>({
apiVersion: yup.string().required().oneOf(API_VERSION),
kind: yup.string().required().equals([KIND]),
spec: yup
.object({
type: yup.string().required().min(1),
path: yup.string(),
schema: yup.object().required(),
templater: yup.string().required(),
})
.required(),
});
}
async enforce(envelope: Entity): Promise<Entity | undefined> {
if (
KIND !== envelope.kind ||
!API_VERSION.includes(envelope.apiVersion as any)
) {
return undefined;
}
return await this.schema.validate(envelope, { strict: true });
}
}
export const templateEntityV1alpha1Policy = schemaPolicy(
KIND,
API_VERSION,
schema,
);
@@ -14,15 +14,13 @@
* limitations under the License.
*/
import { EntityPolicy } from '../types';
import {
UserEntityV1alpha1,
UserEntityV1alpha1Policy,
userEntityV1alpha1Policy as policy,
} from './UserEntityV1alpha1';
describe('UserV1alpha1Policy', () => {
describe('userEntityV1alpha1Policy', () => {
let entity: UserEntityV1alpha1;
let policy: EntityPolicy;
beforeEach(() => {
entity = {
@@ -40,7 +38,6 @@ describe('UserV1alpha1Policy', () => {
memberOf: ['team-a', 'developers'],
},
};
policy = new UserEntityV1alpha1Policy();
});
it('happy path: accepts valid data', async () => {
@@ -16,11 +16,34 @@
import * as yup from 'yup';
import type { Entity } from '../entity/Entity';
import type { EntityPolicy } from '../types';
import { schemaPolicy } from './util';
const API_VERSION = ['backstage.io/v1alpha1', 'backstage.io/v1beta1'] as const;
const KIND = 'User' as const;
const schema = yup.object<Partial<UserEntityV1alpha1>>({
apiVersion: yup.string().required().oneOf(API_VERSION),
kind: yup.string().required().equals([KIND]),
spec: yup
.object({
profile: yup
.object({
displayName: yup.string().min(1).notRequired(),
email: yup.string().min(1).notRequired(),
picture: yup.string().min(1).notRequired(),
})
.notRequired(),
// Use this manual test because yup .required() requires at least one
// element and there is no simple workaround -_-
memberOf: yup.array(yup.string()).test({
name: 'isDefined',
message: 'memberOf must be defined',
test: v => Boolean(v),
}),
})
.required(),
});
export interface UserEntityV1alpha1 extends Entity {
apiVersion: typeof API_VERSION[number];
kind: typeof KIND;
@@ -34,41 +57,4 @@ export interface UserEntityV1alpha1 extends Entity {
};
}
export class UserEntityV1alpha1Policy implements EntityPolicy {
private schema: yup.Schema<any>;
constructor() {
this.schema = yup.object<Partial<UserEntityV1alpha1>>({
apiVersion: yup.string().required().oneOf(API_VERSION),
kind: yup.string().required().equals([KIND]),
spec: yup
.object({
profile: yup
.object({
displayName: yup.string().min(1).notRequired(),
email: yup.string().min(1).notRequired(),
picture: yup.string().min(1).notRequired(),
})
.notRequired(),
// Use this manual test because yup .required() requires at least one
// element and there is no simple workaround -_-
memberOf: yup.array(yup.string()).test({
name: 'isDefined',
message: 'memberOf must be defined',
test: v => Boolean(v),
}),
})
.required(),
});
}
async enforce(envelope: Entity): Promise<Entity | undefined> {
if (
KIND !== envelope.kind ||
!API_VERSION.includes(envelope.apiVersion as any)
) {
return undefined;
}
return await this.schema.validate(envelope, { strict: true });
}
}
export const userEntityV1alpha1Policy = schemaPolicy(KIND, API_VERSION, schema);
+6 -6
View File
@@ -14,32 +14,32 @@
* limitations under the License.
*/
export { ApiEntityV1alpha1Policy } from './ApiEntityV1alpha1';
export { apiEntityV1alpha1Policy } from './ApiEntityV1alpha1';
export type {
ApiEntityV1alpha1 as ApiEntity,
ApiEntityV1alpha1,
} from './ApiEntityV1alpha1';
export { ComponentEntityV1alpha1Policy } from './ComponentEntityV1alpha1';
export { componentEntityV1alpha1Policy } from './ComponentEntityV1alpha1';
export type {
ComponentEntityV1alpha1 as ComponentEntity,
ComponentEntityV1alpha1,
} from './ComponentEntityV1alpha1';
export { GroupEntityV1alpha1Policy } from './GroupEntityV1alpha1';
export { groupEntityV1alpha1Policy } from './GroupEntityV1alpha1';
export type {
GroupEntityV1alpha1 as GroupEntity,
GroupEntityV1alpha1,
} from './GroupEntityV1alpha1';
export { LocationEntityV1alpha1Policy } from './LocationEntityV1alpha1';
export { locationEntityV1alpha1Policy } from './LocationEntityV1alpha1';
export type {
LocationEntityV1alpha1 as LocationEntity,
LocationEntityV1alpha1,
} from './LocationEntityV1alpha1';
export { TemplateEntityV1alpha1Policy } from './TemplateEntityV1alpha1';
export { templateEntityV1alpha1Policy } from './TemplateEntityV1alpha1';
export type {
TemplateEntityV1alpha1 as TemplateEntity,
TemplateEntityV1alpha1,
} from './TemplateEntityV1alpha1';
export { UserEntityV1alpha1Policy } from './UserEntityV1alpha1';
export { userEntityV1alpha1Policy } from './UserEntityV1alpha1';
export type {
UserEntityV1alpha1 as UserEntity,
UserEntityV1alpha1,
+37
View File
@@ -0,0 +1,37 @@
/*
* Copyright 2020 Spotify AB
*
* 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 * as yup from 'yup';
import { Entity } from '../entity';
import { EntityPolicy } from '../types';
export function schemaPolicy(
kind: string,
apiVersion: readonly string[],
schema: yup.Schema<any>,
): EntityPolicy {
return {
async enforce(envelope: Entity): Promise<Entity | undefined> {
if (
kind !== envelope.kind ||
!apiVersion.includes(envelope.apiVersion as any)
) {
return undefined;
}
return await schema.validate(envelope, { strict: true });
},
};
}
@@ -10,7 +10,7 @@ import { setupServer } from 'msw/node';
describe('ExampleComponent', () => {
const server = setupServer();
// Enable API mocking before tests.
beforeAll(() => server.listen())
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }))
// Reset any runtime request handlers we may add during the tests.
afterEach(() => server.resetHandlers())
@@ -32,4 +32,3 @@ describe('ExampleComponent', () => {
expect(rendered.getByText('Welcome to {{ id }}!')).toBeInTheDocument();
});
});
@@ -7,7 +7,7 @@ import { setupServer } from 'msw/node';
describe('ExampleFetchComponent', () => {
const server = setupServer();
// Enable API mocking before tests.
beforeAll(() => server.listen())
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }))
// Reset any runtime request handlers we may add during the tests.
afterEach(() => server.resetHandlers())