Merge pull request #2669 from spotify/freben/github-org

feat(catalog-backend): implement github org entity ingestion
This commit is contained in:
Fredrik Adelöw
2020-09-30 15:08:24 +02:00
committed by GitHub
34 changed files with 1122 additions and 238 deletions
+1 -1
View File
@@ -45,7 +45,7 @@ export default async function createPlugin({
useHotCleanup(
module,
runPeriodically(() => higherOrderOperation.refreshAllLocations(), 10000),
runPeriodically(() => higherOrderOperation.refreshAllLocations(), 100000),
);
return await createRouter({
+21 -19
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';
@@ -40,7 +40,10 @@ class AllEntityPolicies implements EntityPolicy {
async enforce(entity: Entity): Promise<Entity> {
let result = entity;
for (const policy of this.policies) {
result = await policy.enforce(entity);
const output = await policy.enforce(entity);
if (output) {
result = output;
}
}
return result;
}
@@ -51,12 +54,11 @@ class AllEntityPolicies implements EntityPolicy {
class AnyEntityPolicy implements EntityPolicy {
constructor(private readonly policies: EntityPolicy[]) {}
async enforce(entity: Entity): Promise<Entity> {
async enforce(entity: Entity): Promise<Entity | undefined> {
for (const policy of this.policies) {
try {
return await policy.enforce(entity);
} catch {
continue;
const output = await policy.enforce(entity);
if (output !== null) {
return output;
}
}
throw new Error(`The entity did not match any known policy`);
@@ -76,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,
]),
]);
}
@@ -98,7 +100,7 @@ export class EntityPolicies implements EntityPolicy {
this.policy = policy;
}
enforce(entity: Entity): Promise<Entity> {
enforce(entity: Entity): Promise<Entity | undefined> {
return this.policy.enforce(entity);
}
}
@@ -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 = {
@@ -49,7 +47,7 @@ paths:
'200':
description: A paged array of pets
content:
application/json:
application/json:
schema:
$ref: "#/components/schemas/Pets"
components:
@@ -74,7 +72,6 @@ components:
`,
},
};
policy = new ApiEntityV1alpha1Policy();
});
it('happy path: accepts valid data', async () => {
@@ -86,14 +83,14 @@ components:
await expect(policy.enforce(entity)).resolves.toBe(entity);
});
it('rejects unknown apiVersion', async () => {
it('ignores unknown apiVersion', async () => {
(entity as any).apiVersion = 'backstage.io/v1beta0';
await expect(policy.enforce(entity)).rejects.toThrow(/apiVersion/);
await expect(policy.enforce(entity)).resolves.toBeUndefined();
});
it('rejects unknown kind', async () => {
it('ignores unknown kind', async () => {
(entity as any).kind = 'Wizard';
await expect(policy.enforce(entity)).rejects.toThrow(/kind/);
await expect(policy.enforce(entity)).resolves.toBeUndefined();
});
it('rejects missing type', 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,25 +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> {
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 () => {
@@ -50,14 +47,14 @@ describe('ComponentV1alpha1Policy', () => {
await expect(policy.enforce(entity)).resolves.toBe(entity);
});
it('rejects unknown apiVersion', async () => {
it('ignores unknown apiVersion', async () => {
(entity as any).apiVersion = 'backstage.io/v1beta0';
await expect(policy.enforce(entity)).rejects.toThrow(/apiVersion/);
await expect(policy.enforce(entity)).resolves.toBeUndefined();
});
it('rejects unknown kind', async () => {
it('ignores unknown kind', async () => {
(entity as any).kind = 'Wizard';
await expect(policy.enforce(entity)).rejects.toThrow(/kind/);
await expect(policy.enforce(entity)).resolves.toBeUndefined();
});
it('rejects missing type', 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,25 +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> {
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 () => {
@@ -53,14 +50,14 @@ describe('GroupV1alpha1Policy', () => {
await expect(policy.enforce(entity)).resolves.toBe(entity);
});
it('rejects unknown apiVersion', async () => {
it('ignores unknown apiVersion', async () => {
(entity as any).apiVersion = 'backstage.io/v1beta0';
await expect(policy.enforce(entity)).rejects.toThrow(/apiVersion/);
await expect(policy.enforce(entity)).resolves.toBeUndefined();
});
it('rejects unknown kind', async () => {
it('ignores unknown kind', async () => {
(entity as any).kind = 'Wizard';
await expect(policy.enforce(entity)).rejects.toThrow(/kind/);
await expect(policy.enforce(entity)).resolves.toBeUndefined();
});
it('rejects missing type', async () => {
@@ -98,6 +95,11 @@ describe('GroupV1alpha1Policy', () => {
await expect(policy.enforce(entity)).resolves.toBe(entity);
});
it('accepts no ancestors', async () => {
(entity as any).spec.ancestors = [];
await expect(policy.enforce(entity)).resolves.toBe(entity);
});
it('rejects missing children', async () => {
delete (entity as any).spec.children;
await expect(policy.enforce(entity)).rejects.toThrow(/children/);
@@ -108,6 +110,11 @@ describe('GroupV1alpha1Policy', () => {
await expect(policy.enforce(entity)).resolves.toBe(entity);
});
it('accepts no children', async () => {
(entity as any).spec.children = [];
await expect(policy.enforce(entity)).resolves.toBe(entity);
});
it('rejects missing descendants', async () => {
delete (entity as any).spec.descendants;
await expect(policy.enforce(entity)).rejects.toThrow(/descendants/);
@@ -117,4 +124,9 @@ describe('GroupV1alpha1Policy', () => {
(entity as any).spec.descendants = [''];
await expect(policy.enforce(entity)).resolves.toBe(entity);
});
it('accepts no descendants', async () => {
(entity as any).spec.descendants = [];
await expect(policy.enforce(entity)).resolves.toBe(entity);
});
});
@@ -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,26 +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),
ancestors: yup.array(yup.string()).required(),
children: yup.array(yup.string()).required(),
descendants: yup.array(yup.string()).required(),
})
.required(),
});
}
async enforce(envelope: Entity): Promise<Entity> {
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 () => {
@@ -47,14 +44,14 @@ describe('LocationV1alpha1Policy', () => {
await expect(policy.enforce(entity)).resolves.toBe(entity);
});
it('rejects unknown apiVersion', async () => {
it('ignores unknown apiVersion', async () => {
(entity as any).apiVersion = 'backstage.io/v1beta0';
await expect(policy.enforce(entity)).rejects.toThrow(/apiVersion/);
await expect(policy.enforce(entity)).resolves.toBeUndefined();
});
it('rejects unknown kind', async () => {
it('ignores unknown kind', async () => {
(entity as any).kind = 'Wizard';
await expect(policy.enforce(entity)).rejects.toThrow(/kind/);
await expect(policy.enforce(entity)).resolves.toBeUndefined();
});
it('rejects missing type', 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,24 +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> {
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 () => {
@@ -64,14 +61,14 @@ describe('TemplateEntityV1alpah1', () => {
await expect(policy.enforce(entity)).resolves.toBe(entity);
});
it('rejects unknown apiVersion', async () => {
it('ignores unknown apiVersion', async () => {
(entity as any).apiVersion = 'backstage.io/v1beta0';
await expect(policy.enforce(entity)).rejects.toThrow(/apiVersion/);
await expect(policy.enforce(entity)).resolves.toBeUndefined();
});
it('rejects unknown kind', async () => {
it('ignores unknown kind', async () => {
(entity as any).kind = 'Wizard';
await expect(policy.enforce(entity)).rejects.toThrow(/kind/);
await expect(policy.enforce(entity)).resolves.toBeUndefined();
});
it('rejects missing type', 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,25 +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> {
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 () => {
@@ -54,14 +51,14 @@ describe('UserV1alpha1Policy', () => {
await expect(policy.enforce(entity)).resolves.toBe(entity);
});
it('rejects unknown apiVersion', async () => {
it('ignores unknown apiVersion', async () => {
(entity as any).apiVersion = 'backstage.io/v1beta0';
await expect(policy.enforce(entity)).rejects.toThrow(/apiVersion/);
await expect(policy.enforce(entity)).resolves.toBeUndefined();
});
it('rejects unknown kind', async () => {
it('ignores unknown kind', async () => {
(entity as any).kind = 'Wizard';
await expect(policy.enforce(entity)).rejects.toThrow(/kind/);
await expect(policy.enforce(entity)).resolves.toBeUndefined();
});
it('spec accepts unknown additional fields', async () => {
@@ -147,4 +144,14 @@ describe('UserV1alpha1Policy', () => {
(entity as any).spec.memberOf[0] = 7;
await expect(policy.enforce(entity)).rejects.toThrow(/memberOf/);
});
it('accepts empty memberOf', async () => {
(entity as any).spec.memberOf = [];
await expect(policy.enforce(entity)).resolves.toBe(entity);
});
it('rejects null memberOf', async () => {
(entity as any).spec.memberOf = null;
await expect(policy.enforce(entity)).rejects.toThrow(/memberOf/);
});
});
@@ -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,29 +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(),
memberOf: yup.array(yup.string()).required(),
})
.required(),
});
}
async enforce(envelope: Entity): Promise<Entity> {
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 });
},
};
}
+3 -2
View File
@@ -27,10 +27,11 @@ export type EntityPolicy = {
* Applies validation or mutation on an entity.
*
* @param entity The entity, as validated/mutated so far in the policy tree
* @returns The incoming entity, or a mutated version of the same
* @returns The incoming entity, or a mutated version of the same, or
* undefined if this processor could not handle the entity
* @throws An error if the entity should be rejected
*/
enforce(entity: Entity): Promise<Entity>;
enforce(entity: Entity): Promise<Entity | undefined>;
};
export type JSONSchema = JSONSchema7 & { [key in string]?: JsonValue };
@@ -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())
@@ -66,7 +66,7 @@ scaffolder:
catalog:
rules:
- allow: [Component, API, Group, Template, Location]
- allow: [Component, API, Group, User, Template, Location]
processors:
github:
providers: