mirror of
https://github.com/graphql-hive/console
synced 2026-04-21 14:37:17 +00:00
4591 lines
106 KiB
TypeScript
4591 lines
106 KiB
TypeScript
import 'reflect-metadata';
|
|
import { createPool, sql } from 'slonik';
|
|
import { graphql } from 'testkit/gql';
|
|
/* eslint-disable no-process-env */
|
|
import { ProjectType } from 'testkit/gql/graphql';
|
|
import { execute } from 'testkit/graphql';
|
|
import { assertNonNull, getServiceHost } from 'testkit/utils';
|
|
// eslint-disable-next-line import/no-extraneous-dependencies
|
|
import { createStorage } from '@hive/storage';
|
|
import { createTarget, publishSchema, updateSchemaComposition } from '../../../testkit/flow';
|
|
import { initSeed } from '../../../testkit/seed';
|
|
|
|
test.concurrent(
|
|
'cannot publish a schema without target:registry:write access',
|
|
async ({ expect }) => {
|
|
const { createOrg } = await initSeed().createOwner();
|
|
const { createProject } = await createOrg();
|
|
const { createTargetAccessToken } = await createProject(ProjectType.Federation);
|
|
const readToken = await createTargetAccessToken({
|
|
mode: 'readOnly',
|
|
});
|
|
|
|
const resultErrors = await readToken
|
|
.publishSchema({
|
|
sdl: /* GraphQL */ `
|
|
type Query {
|
|
ping: String
|
|
}
|
|
`,
|
|
})
|
|
.then(r => r.expectGraphQLErrors());
|
|
|
|
expect(resultErrors).toHaveLength(1);
|
|
expect(resultErrors[0].message).toMatch(
|
|
`No access (reason: "Missing permission for performing 'schemaVersion:publish' on resource")`,
|
|
);
|
|
},
|
|
);
|
|
|
|
test.concurrent('can publish a schema with target:registry:write access', async ({ expect }) => {
|
|
const { createOrg } = await initSeed().createOwner();
|
|
const { createProject } = await createOrg();
|
|
const { createTargetAccessToken, fetchVersions } = await createProject(ProjectType.Single);
|
|
const readWriteToken = await createTargetAccessToken({});
|
|
|
|
const result1 = await readWriteToken
|
|
.publishSchema({
|
|
sdl: /* GraphQL */ `
|
|
type Query {
|
|
ping: String
|
|
}
|
|
`,
|
|
})
|
|
.then(r => r.expectNoGraphQLErrors());
|
|
|
|
expect(result1.schemaPublish.__typename).toBe('SchemaPublishSuccess');
|
|
|
|
const result2 = await readWriteToken
|
|
.publishSchema({
|
|
sdl: /* GraphQL */ `
|
|
type Query {
|
|
ping: String
|
|
pong: String
|
|
}
|
|
`,
|
|
})
|
|
.then(r => r.expectNoGraphQLErrors());
|
|
|
|
expect(result2.schemaPublish.__typename).toBe('SchemaPublishSuccess');
|
|
|
|
const versionsResult = await fetchVersions(3);
|
|
expect(versionsResult).toHaveLength(2);
|
|
});
|
|
|
|
test.concurrent(
|
|
'base schema should not affect the output schema persisted in db',
|
|
async ({ expect }) => {
|
|
const { createOrg } = await initSeed().createOwner();
|
|
const { createProject } = await createOrg();
|
|
const { createTargetAccessToken, updateBaseSchema, fetchVersions } = await createProject(
|
|
ProjectType.Single,
|
|
);
|
|
const readWriteToken = await createTargetAccessToken({});
|
|
|
|
// Publish schema with write rights
|
|
const publishResult = await readWriteToken
|
|
.publishSchema({
|
|
commit: '1',
|
|
sdl: `type Query { ping: String }`,
|
|
})
|
|
.then(r => r.expectNoGraphQLErrors());
|
|
|
|
// Schema publish should be successful
|
|
expect(publishResult.schemaPublish.__typename).toBe('SchemaPublishSuccess');
|
|
|
|
await updateBaseSchema(`
|
|
directive @auth on OBJECT | FIELD_DEFINITION
|
|
`);
|
|
|
|
const extendedPublishResult = await readWriteToken
|
|
.publishSchema({
|
|
commit: '2',
|
|
sdl: `type Query { ping: String @auth pong: String }`,
|
|
})
|
|
.then(r => r.expectNoGraphQLErrors());
|
|
expect(extendedPublishResult.schemaPublish.__typename).toBe('SchemaPublishSuccess');
|
|
|
|
const versionsResult = await fetchVersions(5);
|
|
expect(versionsResult).toHaveLength(2);
|
|
|
|
const latestResult = await readWriteToken.latestSchema();
|
|
expect(latestResult.latestVersion?.schemas.total).toBe(1);
|
|
|
|
const firstNode = latestResult.latestVersion?.schemas.nodes[0];
|
|
|
|
expect(firstNode).toEqual(
|
|
expect.objectContaining({
|
|
commit: '2',
|
|
source: `type Query {
|
|
ping: String @auth
|
|
pong: String
|
|
}`,
|
|
}),
|
|
);
|
|
expect(firstNode).not.toEqual(
|
|
expect.objectContaining({
|
|
source: expect.stringContaining('directive'),
|
|
}),
|
|
);
|
|
|
|
expect(latestResult.latestVersion?.baseSchema).toMatch(
|
|
'directive @auth on OBJECT | FIELD_DEFINITION',
|
|
);
|
|
},
|
|
);
|
|
|
|
test.concurrent('directives should not be removed (federation)', async () => {
|
|
const { createOrg } = await initSeed().createOwner();
|
|
const { createProject } = await createOrg();
|
|
const { createTargetAccessToken, fetchVersions } = await createProject(ProjectType.Federation);
|
|
const readWriteToken = await createTargetAccessToken({});
|
|
|
|
// Publish schema with write rights
|
|
const publishResult = await readWriteToken
|
|
.publishSchema({
|
|
commit: 'abc123',
|
|
service: 'users',
|
|
url: 'https://api.com/users',
|
|
sdl: `type Query { me: User } type User @key(fields: "id") { id: ID! name: String }`,
|
|
})
|
|
.then(r => r.expectNoGraphQLErrors());
|
|
|
|
// Schema publish should be successful
|
|
expect(publishResult.schemaPublish.__typename).toBe('SchemaPublishSuccess');
|
|
const versionsResult = await fetchVersions(5);
|
|
expect(versionsResult).toHaveLength(1);
|
|
|
|
const latestResult = await readWriteToken.latestSchema();
|
|
expect(latestResult.latestVersion?.schemas.total).toBe(1);
|
|
|
|
expect(latestResult.latestVersion?.schemas.nodes[0]).toEqual(
|
|
expect.objectContaining({
|
|
commit: 'abc123',
|
|
source: `type Query {
|
|
me: User
|
|
}
|
|
|
|
type User @key(fields: "id") {
|
|
id: ID!
|
|
name: String
|
|
}`,
|
|
}),
|
|
);
|
|
});
|
|
|
|
test.concurrent('directives should not be removed (stitching)', async () => {
|
|
const { createOrg } = await initSeed().createOwner();
|
|
const { createProject } = await createOrg();
|
|
const { createTargetAccessToken, fetchVersions } = await createProject(ProjectType.Stitching);
|
|
const readWriteToken = await createTargetAccessToken({});
|
|
|
|
// Publish schema with write rights
|
|
const publishResult = await readWriteToken
|
|
.publishSchema({
|
|
author: 'Kamil',
|
|
sdl: `type Query { me: User } type User @key(selectionSet: "{ id }") { id: ID! name: String }`,
|
|
service: 'test',
|
|
url: 'https://api.com/users',
|
|
commit: 'abc123',
|
|
})
|
|
.then(r => r.expectNoGraphQLErrors());
|
|
|
|
// Schema publish should be successful
|
|
expect(publishResult.schemaPublish.__typename).toBe('SchemaPublishSuccess');
|
|
|
|
const versionsResult = await fetchVersions(5);
|
|
expect(versionsResult).toHaveLength(1);
|
|
|
|
const latestResult = await readWriteToken.latestSchema();
|
|
expect(latestResult.latestVersion?.schemas.total).toBe(1);
|
|
|
|
expect(latestResult.latestVersion?.schemas.nodes[0]).toEqual(
|
|
expect.objectContaining({
|
|
commit: 'abc123',
|
|
source: `type Query {
|
|
me: User
|
|
}
|
|
|
|
type User @key(selectionSet: "{ id }") {
|
|
id: ID!
|
|
name: String
|
|
}`,
|
|
}),
|
|
);
|
|
});
|
|
|
|
test.concurrent('directives should not be removed (single)', async () => {
|
|
const { createOrg } = await initSeed().createOwner();
|
|
const { createProject } = await createOrg();
|
|
const { createTargetAccessToken, fetchVersions } = await createProject(ProjectType.Single);
|
|
const readWriteToken = await createTargetAccessToken({});
|
|
// Publish schema with write rights
|
|
const publishResult = await readWriteToken
|
|
.publishSchema({
|
|
author: 'Kamil',
|
|
commit: 'abc123',
|
|
sdl: `directive @auth on FIELD_DEFINITION type Query { me: User @auth } type User { id: ID! name: String }`,
|
|
service: 'test',
|
|
url: 'https://api.com/users',
|
|
})
|
|
.then(r => r.expectNoGraphQLErrors());
|
|
|
|
// Schema publish should be successful
|
|
expect(publishResult.schemaPublish.__typename).toBe('SchemaPublishSuccess');
|
|
|
|
const versionsResult = await fetchVersions(5);
|
|
expect(versionsResult).toHaveLength(1);
|
|
|
|
const latestResult = await readWriteToken.latestSchema();
|
|
expect(latestResult.latestVersion?.schemas.total).toBe(1);
|
|
|
|
expect(latestResult.latestVersion?.schemas.nodes[0]).toEqual(
|
|
expect.objectContaining({
|
|
commit: 'abc123',
|
|
source: `directive @auth on FIELD_DEFINITION
|
|
|
|
type Query {
|
|
me: User @auth
|
|
}
|
|
|
|
type User {
|
|
id: ID!
|
|
name: String
|
|
}`,
|
|
}),
|
|
);
|
|
});
|
|
|
|
test.concurrent('share publication of schema using redis', async ({ expect }) => {
|
|
const { createOrg } = await initSeed().createOwner();
|
|
const { createProject } = await createOrg();
|
|
const { createTargetAccessToken, fetchVersions } = await createProject(ProjectType.Federation);
|
|
const readWriteToken = await createTargetAccessToken({});
|
|
|
|
// Publish schema with write rights
|
|
const publishResult = await readWriteToken
|
|
.publishSchema({
|
|
author: 'Kamil',
|
|
commit: 'abc123',
|
|
sdl: `type Query { ping: String }`,
|
|
service: 'ping',
|
|
url: 'https://api.com/ping',
|
|
})
|
|
.then(r => r.expectNoGraphQLErrors());
|
|
|
|
// Schema publish should be successful
|
|
expect(publishResult.schemaPublish.__typename).toBe('SchemaPublishSuccess');
|
|
|
|
await expect(fetchVersions(2)).resolves.toHaveLength(1);
|
|
|
|
const [publishResult1, publishResult2] = await Promise.all([
|
|
readWriteToken
|
|
.publishSchema({
|
|
sdl: `type Query { ping: String pong: String }`,
|
|
author: 'Kamil',
|
|
commit: 'abc234',
|
|
service: 'ping', // case insensitive
|
|
url: 'https://api.com/ping',
|
|
})
|
|
.then(r => r.expectNoGraphQLErrors()),
|
|
readWriteToken
|
|
.publishSchema({
|
|
sdl: `type Query { ping: String pong: String }`,
|
|
author: 'Kamil',
|
|
commit: 'abc234',
|
|
service: 'PiNg', // case insensitive
|
|
url: 'https://api.com/ping',
|
|
})
|
|
.then(r => r.expectNoGraphQLErrors()),
|
|
]);
|
|
expect(publishResult1.schemaPublish.__typename).toBe('SchemaPublishSuccess');
|
|
expect(publishResult2.schemaPublish.__typename).toBe('SchemaPublishSuccess');
|
|
|
|
await expect(fetchVersions(3)).resolves.toHaveLength(2);
|
|
});
|
|
|
|
test.concurrent('CDN data can not be fetched with an invalid access token', async ({ expect }) => {
|
|
const { createOrg } = await initSeed().createOwner();
|
|
const { createProject } = await createOrg();
|
|
const { createTargetAccessToken, createCdnAccess } = await createProject(ProjectType.Single);
|
|
const readWriteToken = await createTargetAccessToken({});
|
|
|
|
// Initial schema
|
|
const result = await readWriteToken
|
|
.publishSchema({
|
|
author: 'Kamil',
|
|
commit: 'c0',
|
|
sdl: `type Query { ping: String }`,
|
|
metadata: JSON.stringify({ c0: 1 }),
|
|
})
|
|
.then(r => r.expectNoGraphQLErrors());
|
|
|
|
expect(result.schemaPublish.__typename).toBe('SchemaPublishSuccess');
|
|
|
|
const cdn = await createCdnAccess();
|
|
const res = await fetch(cdn.cdnUrl + '/sdl', {
|
|
method: 'GET',
|
|
headers: {
|
|
'X-Hive-CDN-Key': 'i-like-turtles',
|
|
},
|
|
});
|
|
|
|
expect(res.status).toEqual(403);
|
|
});
|
|
|
|
test.concurrent('CDN data can be fetched with an valid access token', async ({ expect }) => {
|
|
const { createOrg } = await initSeed().createOwner();
|
|
const { createProject } = await createOrg();
|
|
const { createTargetAccessToken, createCdnAccess } = await createProject(ProjectType.Single);
|
|
const readWriteToken = await createTargetAccessToken({});
|
|
|
|
// Initial schema
|
|
const result = await readWriteToken
|
|
.publishSchema({
|
|
author: 'Kamil',
|
|
commit: 'c0',
|
|
sdl: `type Query { ping: String }`,
|
|
metadata: JSON.stringify({ c0: 1 }),
|
|
})
|
|
.then(r => r.expectNoGraphQLErrors());
|
|
|
|
expect(result.schemaPublish.__typename).toBe('SchemaPublishSuccess');
|
|
|
|
const cdn = await createCdnAccess();
|
|
const artifactUrl = cdn.cdnUrl + '/sdl';
|
|
|
|
const cdnResult = await fetch(artifactUrl, {
|
|
method: 'GET',
|
|
headers: {
|
|
'X-Hive-CDN-Key': cdn.secretAccessToken,
|
|
},
|
|
});
|
|
|
|
expect(cdnResult.status).toEqual(200);
|
|
});
|
|
|
|
test.concurrent('cannot do API request with invalid access token', async ({ expect }) => {
|
|
const errors = await publishSchema(
|
|
{
|
|
commit: '1',
|
|
sdl: 'type Query { smokeBangBang: String }',
|
|
author: 'Kamil',
|
|
},
|
|
'foobars',
|
|
).then(r => r.expectGraphQLErrors());
|
|
|
|
expect(errors).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
message: 'Invalid token provided',
|
|
}),
|
|
]),
|
|
);
|
|
});
|
|
|
|
test.concurrent(
|
|
'should publish only one schema if multiple same publishes are started in parallel',
|
|
async ({ expect }) => {
|
|
const { createOrg } = await initSeed().createOwner();
|
|
const { createProject } = await createOrg();
|
|
const { createTargetAccessToken, fetchVersions } = await createProject(ProjectType.Single);
|
|
const readWriteToken = await createTargetAccessToken({});
|
|
|
|
const commits = ['a1', 'a2', 'a3', 'a4', 'a5', 'a6'];
|
|
const publishes = await Promise.all(
|
|
commits.map(commit =>
|
|
readWriteToken
|
|
.publishSchema({
|
|
author: 'John',
|
|
commit,
|
|
sdl: 'type Query { ping: String }',
|
|
})
|
|
.then(r => r.expectNoGraphQLErrors()),
|
|
),
|
|
);
|
|
expect(
|
|
publishes.every(({ schemaPublish }) => schemaPublish.__typename === 'SchemaPublishSuccess'),
|
|
).toBeTruthy();
|
|
|
|
const versionsResult = await fetchVersions(commits.length);
|
|
expect(versionsResult.length).toBe(1); // all publishes have same schema
|
|
},
|
|
);
|
|
|
|
describe.each([ProjectType.Stitching, ProjectType.Federation])('$projectType', projectType => {
|
|
const serviceName =
|
|
projectType === ProjectType.Single
|
|
? {}
|
|
: {
|
|
service: 'test',
|
|
};
|
|
const serviceUrl = projectType === ProjectType.Single ? {} : { url: 'http://localhost:4000' };
|
|
|
|
test.concurrent(
|
|
'linkToWebsite should be available when publishing initial schema',
|
|
async ({ expect }) => {
|
|
const { createOrg } = await initSeed().createOwner();
|
|
const { createProject, organization } = await createOrg();
|
|
const { project, target, createTargetAccessToken } = await createProject(projectType);
|
|
const readWriteToken = await createTargetAccessToken({});
|
|
|
|
const result = await readWriteToken
|
|
.publishSchema({
|
|
author: 'Kamil',
|
|
commit: 'abc123',
|
|
sdl: `type Query { ping: String }`,
|
|
...serviceName,
|
|
...serviceUrl,
|
|
})
|
|
.then(r => r.expectNoGraphQLErrors());
|
|
|
|
expect(result.schemaPublish.__typename).toBe('SchemaPublishSuccess');
|
|
|
|
const linkToWebsite =
|
|
result.schemaPublish.__typename === 'SchemaPublishSuccess'
|
|
? result.schemaPublish.linkToWebsite
|
|
: null;
|
|
|
|
expect(linkToWebsite).toEqual(
|
|
`${process.env.HIVE_APP_BASE_URL}/${organization.slug}/${project.slug}/${target.slug}`,
|
|
);
|
|
},
|
|
);
|
|
|
|
test.concurrent(
|
|
'linkToWebsite should be available when publishing non-initial schema',
|
|
async ({ expect }) => {
|
|
const { createOrg } = await initSeed().createOwner();
|
|
const { createProject, organization } = await createOrg();
|
|
const { createTargetAccessToken, project, target } = await createProject(projectType);
|
|
const readWriteToken = await createTargetAccessToken({});
|
|
|
|
let result = await readWriteToken
|
|
.publishSchema({
|
|
author: 'Kamil',
|
|
commit: 'abc123',
|
|
sdl: `type Query { ping: String }`,
|
|
...serviceName,
|
|
...serviceUrl,
|
|
})
|
|
.then(r => r.expectNoGraphQLErrors());
|
|
|
|
expect(result.schemaPublish.__typename).toBe('SchemaPublishSuccess');
|
|
|
|
result = await readWriteToken
|
|
.publishSchema({
|
|
author: 'Kamil',
|
|
commit: 'abc123',
|
|
sdl: `type Query { ping: String }`,
|
|
...serviceName,
|
|
...serviceUrl,
|
|
force: true,
|
|
experimental_acceptBreakingChanges: true,
|
|
})
|
|
.then(r => r.expectNoGraphQLErrors());
|
|
|
|
expect(result.schemaPublish.__typename).toBe('SchemaPublishSuccess');
|
|
const linkToWebsite =
|
|
result.schemaPublish.__typename === 'SchemaPublishSuccess'
|
|
? result.schemaPublish.linkToWebsite
|
|
: null;
|
|
|
|
expect(linkToWebsite).toMatch(
|
|
`${process.env.HIVE_APP_BASE_URL}/${organization.slug}/${project.slug}/${target.slug}/history/`,
|
|
);
|
|
expect(linkToWebsite).toMatch(/history\/[a-z0-9-]+$/);
|
|
},
|
|
);
|
|
|
|
test("Two targets with the same commit id shouldn't return an error", async () => {
|
|
const { createOrg, ownerToken } = await initSeed().createOwner();
|
|
const { organization, createProject } = await createOrg();
|
|
const { project, createTargetAccessToken } = await createProject(projectType);
|
|
const readWriteToken = await createTargetAccessToken({});
|
|
|
|
const publishResult = await readWriteToken
|
|
.publishSchema({
|
|
author: 'gilad',
|
|
commit: 'abc123',
|
|
sdl: `type Query { ping: String }`,
|
|
...serviceName,
|
|
...serviceUrl,
|
|
})
|
|
.then(r => r.expectNoGraphQLErrors());
|
|
const createTargetResult = await createTarget(
|
|
{
|
|
project: {
|
|
bySelector: {
|
|
organizationSlug: organization.slug,
|
|
projectSlug: project.slug,
|
|
},
|
|
},
|
|
slug: 'target2',
|
|
},
|
|
ownerToken,
|
|
).then(r => r.expectNoGraphQLErrors());
|
|
const target2 = createTargetResult.createTarget.ok!.createdTarget;
|
|
const writeTokenResult2 = await createTargetAccessToken({
|
|
target: target2,
|
|
});
|
|
const publishResult2 = await writeTokenResult2
|
|
.publishSchema({
|
|
author: 'gilad',
|
|
commit: 'abc123',
|
|
sdl: `type Query { ping: String }`,
|
|
...serviceName,
|
|
...serviceUrl,
|
|
})
|
|
.then(r => r.expectNoGraphQLErrors());
|
|
|
|
// Schema publish should be successful
|
|
expect(publishResult.schemaPublish.__typename).toBe('SchemaPublishSuccess');
|
|
expect(publishResult2.schemaPublish.__typename).toBe('SchemaPublishSuccess');
|
|
});
|
|
});
|
|
|
|
function connectionString() {
|
|
const {
|
|
POSTGRES_USER = 'postgres',
|
|
POSTGRES_PASSWORD = 'postgres',
|
|
POSTGRES_HOST = 'localhost',
|
|
POSTGRES_PORT = 5432,
|
|
POSTGRES_DB = 'registry',
|
|
POSTGRES_SSL = null,
|
|
POSTGRES_CONNECTION_STRING = null,
|
|
} = process.env;
|
|
return (
|
|
POSTGRES_CONNECTION_STRING ||
|
|
`postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}${
|
|
POSTGRES_SSL ? '?sslmode=require' : '?sslmode=disable'
|
|
}`
|
|
);
|
|
}
|
|
|
|
type Awaited<T> = T extends PromiseLike<infer U> ? U : T;
|
|
|
|
describe('schema publishing changes are persisted', () => {
|
|
let storage: Awaited<ReturnType<typeof createStorage>>;
|
|
beforeAll(async () => {
|
|
storage = await createStorage(connectionString(), 1);
|
|
});
|
|
afterAll(async () => {
|
|
await storage.destroy();
|
|
});
|
|
|
|
function persistedTest(args: {
|
|
name: string;
|
|
type?: ProjectType;
|
|
schemaBefore: string;
|
|
schemaAfter: string;
|
|
equalsObject: {
|
|
meta: unknown;
|
|
type: unknown;
|
|
};
|
|
/** Only provide if you want to test a service url change */
|
|
serviceUrlAfter?: string;
|
|
}) {
|
|
test.concurrent(`[Schema change] ${args.name}`, async ({ expect }) => {
|
|
const serviceName = {
|
|
service: 'test',
|
|
};
|
|
|
|
const serviceUrl = { url: 'http://localhost:4000' };
|
|
|
|
const { createOrg } = await initSeed().createOwner();
|
|
const { createProject, organization } = await createOrg();
|
|
const { createTargetAccessToken, target, project } = await createProject(
|
|
args.type ?? ProjectType.Single,
|
|
);
|
|
const readWriteToken = await createTargetAccessToken({});
|
|
|
|
const publishResult = await readWriteToken
|
|
.publishSchema({
|
|
author: 'gilad',
|
|
commit: '123',
|
|
sdl: args.schemaBefore,
|
|
...serviceName,
|
|
...serviceUrl,
|
|
})
|
|
.then(r => r.expectNoGraphQLErrors());
|
|
expect(publishResult.schemaPublish.__typename).toBe('SchemaPublishSuccess');
|
|
|
|
const publishResult2 = await readWriteToken
|
|
.publishSchema({
|
|
force: true,
|
|
author: 'gilad',
|
|
commit: '456',
|
|
sdl: args.schemaAfter,
|
|
...serviceName,
|
|
url: args.serviceUrlAfter ?? serviceUrl.url,
|
|
})
|
|
.then(r => r.expectNoGraphQLErrors());
|
|
|
|
if (publishResult2.schemaPublish.__typename !== 'SchemaPublishSuccess') {
|
|
expect(publishResult2.schemaPublish.__typename).toBe('SchemaPublishSuccess');
|
|
return;
|
|
}
|
|
|
|
const latestVersion = await storage.getMaybeLatestVersion({
|
|
targetId: target.id,
|
|
projectId: project.id,
|
|
organizationId: organization.id,
|
|
});
|
|
assertNonNull(latestVersion);
|
|
|
|
const changes = await storage.getSchemaChangesForVersion({
|
|
versionId: latestVersion.id,
|
|
});
|
|
|
|
if (!Array.isArray(changes)) {
|
|
throw new Error('Expected changes to be an array');
|
|
}
|
|
|
|
expect(changes[0]['meta']).toEqual(args.equalsObject['meta']);
|
|
expect(changes[0]['type']).toEqual(args.equalsObject['type']);
|
|
});
|
|
}
|
|
|
|
persistedTest({
|
|
name: 'FieldArgumentDescriptionChanged (description removed)',
|
|
schemaBefore: /* GraphQL */ `
|
|
type Query {
|
|
ping(
|
|
"""
|
|
oi
|
|
"""
|
|
a: Int
|
|
): String
|
|
}
|
|
`,
|
|
schemaAfter: /* GraphQL */ `
|
|
type Query {
|
|
ping(a: Int): String
|
|
}
|
|
`,
|
|
equalsObject: {
|
|
meta: {
|
|
typeName: 'Query',
|
|
fieldName: 'ping',
|
|
argumentName: 'a',
|
|
oldDescription: 'oi',
|
|
newDescription: null,
|
|
},
|
|
type: 'FIELD_ARGUMENT_DESCRIPTION_CHANGED',
|
|
},
|
|
});
|
|
|
|
persistedTest({
|
|
name: 'FieldArgumentDescriptionChanged (description added)',
|
|
schemaBefore: /* GraphQL */ `
|
|
type Query {
|
|
ping(a: Int): String
|
|
}
|
|
`,
|
|
schemaAfter: /* GraphQL */ `
|
|
type Query {
|
|
ping(
|
|
"""
|
|
oi
|
|
"""
|
|
a: Int
|
|
): String
|
|
}
|
|
`,
|
|
equalsObject: {
|
|
meta: {
|
|
typeName: 'Query',
|
|
fieldName: 'ping',
|
|
argumentName: 'a',
|
|
oldDescription: null,
|
|
newDescription: 'oi',
|
|
},
|
|
type: 'FIELD_ARGUMENT_DESCRIPTION_CHANGED',
|
|
},
|
|
});
|
|
|
|
persistedTest({
|
|
name: 'FieldArgumentDefaultChangedModel',
|
|
schemaBefore: /* GraphQL */ `
|
|
type Query {
|
|
ping(a: Int = 1): String
|
|
}
|
|
`,
|
|
schemaAfter: /* GraphQL */ `
|
|
type Query {
|
|
ping(a: Int = 2): String
|
|
}
|
|
`,
|
|
equalsObject: {
|
|
meta: {
|
|
argumentName: 'a',
|
|
fieldName: 'ping',
|
|
newDefaultValue: '2',
|
|
oldDefaultValue: '1',
|
|
typeName: 'Query',
|
|
},
|
|
type: 'FIELD_ARGUMENT_DEFAULT_CHANGED',
|
|
},
|
|
});
|
|
|
|
persistedTest({
|
|
name: 'FieldArgumentDefaultChangedModel (removed)',
|
|
schemaBefore: /* GraphQL */ `
|
|
type Query {
|
|
ping(a: Int = 1): String
|
|
}
|
|
`,
|
|
schemaAfter: /* GraphQL */ `
|
|
type Query {
|
|
ping(a: Int): String
|
|
}
|
|
`,
|
|
equalsObject: {
|
|
meta: {
|
|
argumentName: 'a',
|
|
fieldName: 'ping',
|
|
oldDefaultValue: '1',
|
|
typeName: 'Query',
|
|
},
|
|
type: 'FIELD_ARGUMENT_DEFAULT_CHANGED',
|
|
},
|
|
});
|
|
|
|
persistedTest({
|
|
name: 'FieldArgumentDefaultChangedModel (added)',
|
|
schemaBefore: /* GraphQL */ `
|
|
type Query {
|
|
ping(a: Int): String
|
|
}
|
|
`,
|
|
schemaAfter: /* GraphQL */ `
|
|
type Query {
|
|
ping(a: Int = 1): String
|
|
}
|
|
`,
|
|
equalsObject: {
|
|
meta: {
|
|
argumentName: 'a',
|
|
fieldName: 'ping',
|
|
newDefaultValue: '1',
|
|
typeName: 'Query',
|
|
},
|
|
type: 'FIELD_ARGUMENT_DEFAULT_CHANGED',
|
|
},
|
|
});
|
|
|
|
persistedTest({
|
|
name: 'FieldArgumentTypeChangedModel',
|
|
schemaBefore: /* GraphQL */ `
|
|
type Query {
|
|
ping(a: Int): String
|
|
}
|
|
`,
|
|
schemaAfter: /* GraphQL */ `
|
|
type Query {
|
|
ping(a: String): String
|
|
}
|
|
`,
|
|
equalsObject: {
|
|
meta: {
|
|
argumentName: 'a',
|
|
fieldName: 'ping',
|
|
isSafeArgumentTypeChange: false,
|
|
newArgumentType: 'String',
|
|
oldArgumentType: 'Int',
|
|
typeName: 'Query',
|
|
},
|
|
type: 'FIELD_ARGUMENT_TYPE_CHANGED',
|
|
},
|
|
});
|
|
|
|
persistedTest({
|
|
name: 'DirectiveRemovedModel',
|
|
schemaBefore: /* GraphQL */ `
|
|
type Query {
|
|
ping(a: Int): String
|
|
}
|
|
|
|
directive @foo on FIELD
|
|
`,
|
|
schemaAfter: /* GraphQL */ `
|
|
type Query {
|
|
ping(a: Int): String
|
|
}
|
|
`,
|
|
equalsObject: {
|
|
meta: {
|
|
removedDirectiveName: 'foo',
|
|
},
|
|
type: 'DIRECTIVE_REMOVED',
|
|
},
|
|
});
|
|
|
|
persistedTest({
|
|
name: 'DirectiveAddedLiteral',
|
|
schemaBefore: /* GraphQL */ `
|
|
type Query {
|
|
ping(a: Int): String
|
|
}
|
|
`,
|
|
schemaAfter: /* GraphQL */ `
|
|
type Query {
|
|
ping(a: Int): String
|
|
}
|
|
|
|
directive @foo on FIELD
|
|
`,
|
|
equalsObject: {
|
|
meta: {
|
|
addedDirectiveName: 'foo',
|
|
addedDirectiveDescription: null,
|
|
addedDirectiveLocations: ['FIELD'],
|
|
addedDirectiveRepeatable: false,
|
|
},
|
|
type: 'DIRECTIVE_ADDED',
|
|
},
|
|
});
|
|
|
|
persistedTest({
|
|
name: 'DirectiveDescriptionChangedModel (removed)',
|
|
schemaBefore: /* GraphQL */ `
|
|
type Query {
|
|
ping(a: Int): String
|
|
}
|
|
|
|
"""
|
|
yoyoyo
|
|
"""
|
|
directive @foo on FIELD
|
|
`,
|
|
schemaAfter: /* GraphQL */ `
|
|
type Query {
|
|
ping(a: Int): String
|
|
}
|
|
|
|
directive @foo on FIELD
|
|
`,
|
|
equalsObject: {
|
|
meta: {
|
|
directiveName: 'foo',
|
|
oldDirectiveDescription: 'yoyoyo',
|
|
newDirectiveDescription: null,
|
|
},
|
|
type: 'DIRECTIVE_DESCRIPTION_CHANGED',
|
|
},
|
|
});
|
|
|
|
persistedTest({
|
|
name: 'DirectiveDescriptionChangedModel (added)',
|
|
schemaBefore: /* GraphQL */ `
|
|
type Query {
|
|
ping(a: Int): String
|
|
}
|
|
|
|
directive @foo on FIELD
|
|
`,
|
|
schemaAfter: /* GraphQL */ `
|
|
type Query {
|
|
ping(a: Int): String
|
|
}
|
|
|
|
"""
|
|
yoyoyo
|
|
"""
|
|
directive @foo on FIELD
|
|
`,
|
|
equalsObject: {
|
|
meta: {
|
|
directiveName: 'foo',
|
|
oldDirectiveDescription: null,
|
|
newDirectiveDescription: 'yoyoyo',
|
|
},
|
|
type: 'DIRECTIVE_DESCRIPTION_CHANGED',
|
|
},
|
|
});
|
|
|
|
persistedTest({
|
|
name: 'DirectiveDescriptionChangedModel (changed)',
|
|
schemaBefore: /* GraphQL */ `
|
|
type Query {
|
|
ping(a: Int): String
|
|
}
|
|
"""
|
|
yo
|
|
"""
|
|
directive @foo on FIELD
|
|
`,
|
|
schemaAfter: /* GraphQL */ `
|
|
type Query {
|
|
ping(a: Int): String
|
|
}
|
|
|
|
"""
|
|
yoyo
|
|
"""
|
|
directive @foo on FIELD
|
|
`,
|
|
equalsObject: {
|
|
meta: {
|
|
directiveName: 'foo',
|
|
oldDirectiveDescription: 'yo',
|
|
newDirectiveDescription: 'yoyo',
|
|
},
|
|
type: 'DIRECTIVE_DESCRIPTION_CHANGED',
|
|
},
|
|
});
|
|
|
|
persistedTest({
|
|
name: 'DirectiveLocationAddedModel',
|
|
schemaBefore: /* GraphQL */ `
|
|
type Query {
|
|
ping(a: Int): String
|
|
}
|
|
|
|
directive @foo on FIELD
|
|
`,
|
|
schemaAfter: /* GraphQL */ `
|
|
type Query {
|
|
ping(a: Int): String
|
|
}
|
|
|
|
directive @foo on FIELD | FRAGMENT_SPREAD
|
|
`,
|
|
equalsObject: {
|
|
meta: {
|
|
directiveName: 'foo',
|
|
addedDirectiveLocation: 'FRAGMENT_SPREAD',
|
|
},
|
|
type: 'DIRECTIVE_LOCATION_ADDED',
|
|
},
|
|
});
|
|
|
|
persistedTest({
|
|
name: 'DirectiveLocationRemovedModel',
|
|
schemaBefore: /* GraphQL */ `
|
|
type Query {
|
|
ping(a: Int): String
|
|
}
|
|
directive @foo on FIELD | FRAGMENT_SPREAD
|
|
`,
|
|
schemaAfter: /* GraphQL */ `
|
|
type Query {
|
|
ping(a: Int): String
|
|
}
|
|
|
|
directive @foo on FIELD
|
|
`,
|
|
equalsObject: {
|
|
meta: {
|
|
directiveName: 'foo',
|
|
removedDirectiveLocation: 'FRAGMENT_SPREAD',
|
|
},
|
|
type: 'DIRECTIVE_LOCATION_REMOVED',
|
|
},
|
|
});
|
|
|
|
persistedTest({
|
|
name: 'DirectiveArgumentRemovedModel',
|
|
schemaBefore: /* GraphQL */ `
|
|
type Query {
|
|
ping(a: Int): String
|
|
}
|
|
directive @foo(a: Int) on FIELD
|
|
`,
|
|
schemaAfter: /* GraphQL */ `
|
|
type Query {
|
|
ping(a: Int): String
|
|
}
|
|
|
|
directive @foo on FIELD
|
|
`,
|
|
equalsObject: {
|
|
meta: {
|
|
directiveName: 'foo',
|
|
removedDirectiveArgumentName: 'a',
|
|
},
|
|
type: 'DIRECTIVE_ARGUMENT_REMOVED',
|
|
},
|
|
});
|
|
|
|
persistedTest({
|
|
name: 'DirectiveArgumentDescriptionChangedModel (changed)',
|
|
schemaBefore: /* GraphQL */ `
|
|
type Query {
|
|
ping(a: Int): String
|
|
}
|
|
|
|
directive @foo(
|
|
"""
|
|
yo
|
|
"""
|
|
a: Int
|
|
) on FIELD
|
|
`,
|
|
schemaAfter: /* GraphQL */ `
|
|
type Query {
|
|
ping(a: Int): String
|
|
}
|
|
|
|
directive @foo(
|
|
"""
|
|
yoyo
|
|
"""
|
|
a: Int
|
|
) on FIELD
|
|
`,
|
|
equalsObject: {
|
|
meta: {
|
|
directiveName: 'foo',
|
|
directiveArgumentName: 'a',
|
|
oldDirectiveArgumentDescription: 'yo',
|
|
newDirectiveArgumentDescription: 'yoyo',
|
|
},
|
|
type: 'DIRECTIVE_ARGUMENT_DESCRIPTION_CHANGED',
|
|
},
|
|
});
|
|
|
|
persistedTest({
|
|
name: 'DirectiveArgumentDescriptionChangedModel (removed)',
|
|
schemaBefore: /* GraphQL */ `
|
|
type Query {
|
|
ping(a: Int): String
|
|
}
|
|
|
|
directive @foo(
|
|
"""
|
|
yo
|
|
"""
|
|
a: Int
|
|
) on FIELD
|
|
`,
|
|
schemaAfter: /* GraphQL */ `
|
|
type Query {
|
|
ping(a: Int): String
|
|
}
|
|
|
|
directive @foo(a: Int) on FIELD
|
|
`,
|
|
equalsObject: {
|
|
meta: {
|
|
directiveName: 'foo',
|
|
directiveArgumentName: 'a',
|
|
oldDirectiveArgumentDescription: 'yo',
|
|
newDirectiveArgumentDescription: null,
|
|
},
|
|
type: 'DIRECTIVE_ARGUMENT_DESCRIPTION_CHANGED',
|
|
},
|
|
});
|
|
|
|
persistedTest({
|
|
name: 'DirectiveArgumentDescriptionChangedModel (added)',
|
|
schemaBefore: /* GraphQL */ `
|
|
type Query {
|
|
ping(a: Int): String
|
|
}
|
|
|
|
directive @foo(a: Int) on FIELD
|
|
`,
|
|
schemaAfter: /* GraphQL */ `
|
|
type Query {
|
|
ping(a: Int): String
|
|
}
|
|
|
|
directive @foo(
|
|
"""
|
|
yo
|
|
"""
|
|
a: Int
|
|
) on FIELD
|
|
`,
|
|
equalsObject: {
|
|
meta: {
|
|
directiveName: 'foo',
|
|
directiveArgumentName: 'a',
|
|
oldDirectiveArgumentDescription: null,
|
|
newDirectiveArgumentDescription: 'yo',
|
|
},
|
|
type: 'DIRECTIVE_ARGUMENT_DESCRIPTION_CHANGED',
|
|
},
|
|
});
|
|
|
|
persistedTest({
|
|
name: 'DirectiveArgumentDefaultValueChangedModel (changed)',
|
|
schemaBefore: /* GraphQL */ `
|
|
type Query {
|
|
ping(a: Int): String
|
|
}
|
|
|
|
directive @foo(a: Int = 1) on FIELD
|
|
`,
|
|
schemaAfter: /* GraphQL */ `
|
|
type Query {
|
|
ping(a: Int): String
|
|
}
|
|
|
|
directive @foo(a: Int = 2) on FIELD
|
|
`,
|
|
equalsObject: {
|
|
meta: {
|
|
directiveName: 'foo',
|
|
directiveArgumentName: 'a',
|
|
oldDirectiveArgumentDefaultValue: '1',
|
|
newDirectiveArgumentDefaultValue: '2',
|
|
},
|
|
type: 'DIRECTIVE_ARGUMENT_DEFAULT_VALUE_CHANGED',
|
|
},
|
|
});
|
|
|
|
persistedTest({
|
|
name: 'DirectiveArgumentDefaultValueChangedModel (added)',
|
|
schemaBefore: /* GraphQL */ `
|
|
type Query {
|
|
ping(a: Int): String
|
|
}
|
|
|
|
directive @foo(a: Int) on FIELD
|
|
`,
|
|
schemaAfter: /* GraphQL */ `
|
|
type Query {
|
|
ping(a: Int): String
|
|
}
|
|
|
|
directive @foo(a: Int = 2) on FIELD
|
|
`,
|
|
equalsObject: {
|
|
meta: {
|
|
directiveName: 'foo',
|
|
directiveArgumentName: 'a',
|
|
newDirectiveArgumentDefaultValue: '2',
|
|
},
|
|
type: 'DIRECTIVE_ARGUMENT_DEFAULT_VALUE_CHANGED',
|
|
},
|
|
});
|
|
|
|
persistedTest({
|
|
name: 'DirectiveArgumentDefaultValueChangedModel (removed)',
|
|
schemaBefore: /* GraphQL */ `
|
|
type Query {
|
|
ping(a: Int): String
|
|
}
|
|
|
|
directive @foo(a: Int = 2) on FIELD
|
|
`,
|
|
schemaAfter: /* GraphQL */ `
|
|
type Query {
|
|
ping(a: Int): String
|
|
}
|
|
|
|
directive @foo(a: Int) on FIELD
|
|
`,
|
|
equalsObject: {
|
|
meta: {
|
|
directiveName: 'foo',
|
|
directiveArgumentName: 'a',
|
|
oldDirectiveArgumentDefaultValue: '2',
|
|
},
|
|
type: 'DIRECTIVE_ARGUMENT_DEFAULT_VALUE_CHANGED',
|
|
},
|
|
});
|
|
|
|
persistedTest({
|
|
name: 'DirectiveArgumentTypeChangedModel',
|
|
schemaBefore: /* GraphQL */ `
|
|
type Query {
|
|
ping(a: Int): String
|
|
}
|
|
|
|
directive @foo(a: Int) on FIELD
|
|
`,
|
|
schemaAfter: /* GraphQL */ `
|
|
type Query {
|
|
ping(a: Int): String
|
|
}
|
|
|
|
directive @foo(a: String) on FIELD
|
|
`,
|
|
equalsObject: {
|
|
meta: {
|
|
directiveName: 'foo',
|
|
directiveArgumentName: 'a',
|
|
oldDirectiveArgumentType: 'Int',
|
|
newDirectiveArgumentType: 'String',
|
|
isSafeDirectiveArgumentTypeChange: false,
|
|
},
|
|
type: 'DIRECTIVE_ARGUMENT_TYPE_CHANGED',
|
|
},
|
|
});
|
|
|
|
persistedTest({
|
|
name: 'DirectiveArgumentTypeChangedModel (non deprecated)',
|
|
schemaBefore: /* GraphQL */ `
|
|
type Query {
|
|
ping(a: Int): String
|
|
}
|
|
|
|
enum Foo {
|
|
a
|
|
b
|
|
}
|
|
`,
|
|
schemaAfter: /* GraphQL */ `
|
|
type Query {
|
|
ping(a: Int): String
|
|
}
|
|
|
|
enum Foo {
|
|
a
|
|
}
|
|
`,
|
|
equalsObject: {
|
|
meta: {
|
|
enumName: 'Foo',
|
|
removedEnumValueName: 'b',
|
|
isEnumValueDeprecated: false,
|
|
},
|
|
type: 'ENUM_VALUE_REMOVED',
|
|
},
|
|
});
|
|
|
|
persistedTest({
|
|
name: 'DirectiveArgumentTypeChangedModel (deprecated)',
|
|
schemaBefore: /* GraphQL */ `
|
|
type Query {
|
|
ping(a: Int): String
|
|
}
|
|
|
|
enum Foo {
|
|
a
|
|
b @deprecated(reason: "reason")
|
|
}
|
|
`,
|
|
schemaAfter: /* GraphQL */ `
|
|
type Query {
|
|
ping(a: Int): String
|
|
}
|
|
|
|
enum Foo {
|
|
a
|
|
}
|
|
`,
|
|
equalsObject: {
|
|
meta: {
|
|
enumName: 'Foo',
|
|
removedEnumValueName: 'b',
|
|
isEnumValueDeprecated: true,
|
|
},
|
|
type: 'ENUM_VALUE_REMOVED',
|
|
},
|
|
});
|
|
|
|
persistedTest({
|
|
name: 'EnumValueAdded',
|
|
schemaBefore: /* GraphQL */ `
|
|
type Query {
|
|
ping(a: Int): String
|
|
}
|
|
|
|
enum Foo {
|
|
a
|
|
}
|
|
`,
|
|
schemaAfter: /* GraphQL */ `
|
|
type Query {
|
|
ping(a: Int): String
|
|
}
|
|
|
|
enum Foo {
|
|
a
|
|
b
|
|
}
|
|
`,
|
|
equalsObject: {
|
|
meta: {
|
|
addedDirectiveDescription: null,
|
|
enumName: 'Foo',
|
|
addedEnumValueName: 'b',
|
|
addedToNewType: false,
|
|
},
|
|
type: 'ENUM_VALUE_ADDED',
|
|
},
|
|
});
|
|
|
|
persistedTest({
|
|
name: 'EnumValueDescriptionChangedModel (changed)',
|
|
schemaBefore: /* GraphQL */ `
|
|
type Query {
|
|
ping(a: Int): String
|
|
}
|
|
|
|
enum Foo {
|
|
"""
|
|
yo
|
|
"""
|
|
a
|
|
}
|
|
`,
|
|
schemaAfter: /* GraphQL */ `
|
|
type Query {
|
|
ping(a: Int): String
|
|
}
|
|
|
|
enum Foo {
|
|
"""
|
|
yoyo
|
|
"""
|
|
a
|
|
}
|
|
`,
|
|
equalsObject: {
|
|
meta: {
|
|
enumName: 'Foo',
|
|
enumValueName: 'a',
|
|
oldEnumValueDescription: 'yo',
|
|
newEnumValueDescription: 'yoyo',
|
|
},
|
|
type: 'ENUM_VALUE_DESCRIPTION_CHANGED',
|
|
},
|
|
});
|
|
|
|
persistedTest({
|
|
name: 'EnumValueDescriptionChangedModel (added)',
|
|
schemaBefore: /* GraphQL */ `
|
|
type Query {
|
|
ping(a: Int): String
|
|
}
|
|
|
|
enum Foo {
|
|
a
|
|
}
|
|
`,
|
|
schemaAfter: /* GraphQL */ `
|
|
type Query {
|
|
ping(a: Int): String
|
|
}
|
|
|
|
enum Foo {
|
|
"""
|
|
yo
|
|
"""
|
|
a
|
|
}
|
|
`,
|
|
equalsObject: {
|
|
meta: {
|
|
enumName: 'Foo',
|
|
enumValueName: 'a',
|
|
oldEnumValueDescription: null,
|
|
newEnumValueDescription: 'yo',
|
|
},
|
|
type: 'ENUM_VALUE_DESCRIPTION_CHANGED',
|
|
},
|
|
});
|
|
|
|
persistedTest({
|
|
name: 'EnumValueDescriptionChangedModel (removed)',
|
|
schemaBefore: /* GraphQL */ `
|
|
type Query {
|
|
ping(a: Int): String
|
|
}
|
|
|
|
enum Foo {
|
|
"""
|
|
yo
|
|
"""
|
|
a
|
|
}
|
|
`,
|
|
schemaAfter: /* GraphQL */ `
|
|
type Query {
|
|
ping(a: Int): String
|
|
}
|
|
|
|
enum Foo {
|
|
a
|
|
}
|
|
`,
|
|
equalsObject: {
|
|
meta: {
|
|
enumName: 'Foo',
|
|
enumValueName: 'a',
|
|
oldEnumValueDescription: 'yo',
|
|
newEnumValueDescription: null,
|
|
},
|
|
type: 'ENUM_VALUE_DESCRIPTION_CHANGED',
|
|
},
|
|
});
|
|
|
|
persistedTest({
|
|
name: 'EnumValueDeprecationReasonChangedModel (deprecated)',
|
|
schemaBefore: /* GraphQL */ `
|
|
type Query {
|
|
ping(a: Int): String
|
|
}
|
|
|
|
enum Foo {
|
|
a @deprecated(reason: "a")
|
|
}
|
|
`,
|
|
schemaAfter: /* GraphQL */ `
|
|
type Query {
|
|
ping(a: Int): String
|
|
}
|
|
|
|
enum Foo {
|
|
a @deprecated(reason: "b")
|
|
}
|
|
`,
|
|
equalsObject: {
|
|
meta: {
|
|
enumName: 'Foo',
|
|
enumValueName: 'a',
|
|
oldEnumValueDeprecationReason: 'a',
|
|
newEnumValueDeprecationReason: 'b',
|
|
},
|
|
type: 'ENUM_VALUE_DEPRECATION_REASON_CHANGED',
|
|
},
|
|
});
|
|
|
|
persistedTest({
|
|
name: 'EnumValueDeprecationReasonAddedModel',
|
|
schemaBefore: /* GraphQL */ `
|
|
type Query {
|
|
ping(a: Int): String
|
|
}
|
|
|
|
enum Foo {
|
|
a
|
|
}
|
|
`,
|
|
schemaAfter: /* GraphQL */ `
|
|
type Query {
|
|
ping(a: Int): String
|
|
}
|
|
|
|
enum Foo {
|
|
a @deprecated(reason: "b")
|
|
}
|
|
`,
|
|
equalsObject: {
|
|
meta: {
|
|
enumName: 'Foo',
|
|
enumValueName: 'a',
|
|
addedValueDeprecationReason: 'b',
|
|
},
|
|
type: 'ENUM_VALUE_DEPRECATION_REASON_ADDED',
|
|
},
|
|
});
|
|
|
|
persistedTest({
|
|
name: 'EnumValueDeprecationReasonAddedModel',
|
|
schemaBefore: /* GraphQL */ `
|
|
type Query {
|
|
ping(a: Int): String
|
|
}
|
|
|
|
enum Foo {
|
|
a @deprecated(reason: "b")
|
|
}
|
|
`,
|
|
schemaAfter: /* GraphQL */ `
|
|
type Query {
|
|
ping(a: Int): String
|
|
}
|
|
|
|
enum Foo {
|
|
a
|
|
}
|
|
`,
|
|
equalsObject: {
|
|
meta: {
|
|
enumName: 'Foo',
|
|
enumValueName: 'a',
|
|
removedEnumValueDeprecationReason: 'b',
|
|
},
|
|
type: 'ENUM_VALUE_DEPRECATION_REASON_REMOVED',
|
|
},
|
|
});
|
|
|
|
persistedTest({
|
|
name: 'FieldRemovedModel',
|
|
schemaBefore: /* GraphQL */ `
|
|
type Query {
|
|
a: String
|
|
b: String
|
|
}
|
|
`,
|
|
schemaAfter: /* GraphQL */ `
|
|
type Query {
|
|
a: String
|
|
}
|
|
`,
|
|
equalsObject: {
|
|
meta: {
|
|
typeName: 'Query',
|
|
isRemovedFieldDeprecated: false,
|
|
removedFieldName: 'b',
|
|
typeType: 'object type',
|
|
},
|
|
type: 'FIELD_REMOVED',
|
|
},
|
|
});
|
|
|
|
persistedTest({
|
|
name: 'FieldAddedModel',
|
|
schemaBefore: /* GraphQL */ `
|
|
type Query {
|
|
a: String
|
|
}
|
|
`,
|
|
schemaAfter: /* GraphQL */ `
|
|
type Query {
|
|
a: String
|
|
b: String
|
|
}
|
|
`,
|
|
equalsObject: {
|
|
meta: {
|
|
typeName: 'Query',
|
|
addedFieldName: 'b',
|
|
typeType: 'object type',
|
|
addedFieldReturnType: 'String',
|
|
},
|
|
type: 'FIELD_ADDED',
|
|
},
|
|
});
|
|
|
|
persistedTest({
|
|
name: 'FieldDescriptionChangedModel',
|
|
schemaBefore: /* GraphQL */ `
|
|
type Query {
|
|
"""
|
|
yo
|
|
"""
|
|
a: String
|
|
}
|
|
`,
|
|
schemaAfter: /* GraphQL */ `
|
|
type Query {
|
|
"""
|
|
yoyo
|
|
"""
|
|
a: String
|
|
}
|
|
`,
|
|
equalsObject: {
|
|
meta: {
|
|
typeName: 'Query',
|
|
fieldName: 'a',
|
|
oldDescription: 'yo',
|
|
newDescription: 'yoyo',
|
|
},
|
|
type: 'FIELD_DESCRIPTION_CHANGED',
|
|
},
|
|
});
|
|
|
|
persistedTest({
|
|
name: 'FieldDescriptionAddedModel (added)',
|
|
schemaBefore: /* GraphQL */ `
|
|
type Query {
|
|
a: String
|
|
}
|
|
`,
|
|
schemaAfter: /* GraphQL */ `
|
|
type Query {
|
|
"""
|
|
yoyo
|
|
"""
|
|
a: String
|
|
}
|
|
`,
|
|
equalsObject: {
|
|
meta: {
|
|
typeName: 'Query',
|
|
fieldName: 'a',
|
|
addedDescription: 'yoyo',
|
|
},
|
|
type: 'FIELD_DESCRIPTION_ADDED',
|
|
},
|
|
});
|
|
|
|
persistedTest({
|
|
name: 'FieldDescriptionRemovedModel',
|
|
schemaBefore: /* GraphQL */ `
|
|
type Query {
|
|
"""
|
|
yo
|
|
"""
|
|
a: String
|
|
}
|
|
`,
|
|
schemaAfter: /* GraphQL */ `
|
|
type Query {
|
|
a: String
|
|
}
|
|
`,
|
|
equalsObject: {
|
|
meta: {
|
|
typeName: 'Query',
|
|
fieldName: 'a',
|
|
},
|
|
type: 'FIELD_DESCRIPTION_REMOVED',
|
|
},
|
|
});
|
|
|
|
persistedTest({
|
|
name: 'FieldDeprecationAddedModel',
|
|
schemaBefore: /* GraphQL */ `
|
|
type Query {
|
|
a: String
|
|
}
|
|
`,
|
|
schemaAfter: /* GraphQL */ `
|
|
type Query {
|
|
"""
|
|
yo
|
|
"""
|
|
a: String
|
|
}
|
|
`,
|
|
equalsObject: {
|
|
meta: {
|
|
typeName: 'Query',
|
|
fieldName: 'a',
|
|
addedDescription: 'yo',
|
|
},
|
|
type: 'FIELD_DESCRIPTION_ADDED',
|
|
},
|
|
});
|
|
|
|
persistedTest({
|
|
name: 'FieldDeprecationRemovedModel',
|
|
schemaBefore: /* GraphQL */ `
|
|
type Query {
|
|
a: String @deprecated(reason: "yo")
|
|
}
|
|
`,
|
|
schemaAfter: /* GraphQL */ `
|
|
type Query {
|
|
a: String
|
|
}
|
|
`,
|
|
equalsObject: {
|
|
meta: {
|
|
typeName: 'Query',
|
|
fieldName: 'a',
|
|
},
|
|
type: 'FIELD_DEPRECATION_REMOVED',
|
|
},
|
|
});
|
|
|
|
persistedTest({
|
|
name: 'FieldDeprecationReasonChangedModel',
|
|
schemaBefore: /* GraphQL */ `
|
|
type Query {
|
|
a: String @deprecated(reason: "yo")
|
|
}
|
|
`,
|
|
schemaAfter: /* GraphQL */ `
|
|
type Query {
|
|
a: String @deprecated(reason: "yoyo")
|
|
}
|
|
`,
|
|
equalsObject: {
|
|
meta: {
|
|
typeName: 'Query',
|
|
fieldName: 'a',
|
|
oldDeprecationReason: 'yo',
|
|
newDeprecationReason: 'yoyo',
|
|
},
|
|
type: 'FIELD_DEPRECATION_REASON_CHANGED',
|
|
},
|
|
});
|
|
|
|
// persistedTest({
|
|
// name: 'FieldDeprecationReasonAddedModel',
|
|
// schemaBefore: /* GraphQL */ `
|
|
// type Query {
|
|
// a: String @deprecated
|
|
// }
|
|
// `,
|
|
// schemaAfter: /* GraphQL */ `
|
|
// type Query {
|
|
// a: String @deprecated(reason: "yoyo")
|
|
// }
|
|
// `,
|
|
// equalsObject: {
|
|
// meta: {
|
|
// typeName: 'Query',
|
|
// fieldName: 'a',
|
|
// addedDeprecationReason: 'yoyo',
|
|
// },
|
|
// type: 'FIELD_DEPRECATION_REASON_ADDED',
|
|
// },
|
|
// });
|
|
|
|
// persistedTest({
|
|
// name: 'FieldDeprecationReasonRemovedModel',
|
|
// schemaBefore: /* GraphQL */ `
|
|
// type Query {
|
|
// a: String @deprecated(reason: "yoyo")
|
|
// }
|
|
// `,
|
|
// schemaAfter: /* GraphQL */ `
|
|
// type Query {
|
|
// a: String @deprecated
|
|
// }
|
|
// `,
|
|
// equalsObject: {
|
|
// meta: {
|
|
// typeName: 'Query',
|
|
// fieldName: 'a',
|
|
// },
|
|
// type: 'FIELD_DEPRECATION_REASON_REMOVED',
|
|
// },
|
|
// });
|
|
|
|
persistedTest({
|
|
name: 'FieldTypeChangedModel (unsafe)',
|
|
schemaBefore: /* GraphQL */ `
|
|
type Query {
|
|
a: String
|
|
}
|
|
`,
|
|
schemaAfter: /* GraphQL */ `
|
|
type Query {
|
|
a: Int
|
|
}
|
|
`,
|
|
equalsObject: {
|
|
meta: {
|
|
typeName: 'Query',
|
|
fieldName: 'a',
|
|
oldFieldType: 'String',
|
|
newFieldType: 'Int',
|
|
isSafeFieldTypeChange: false,
|
|
},
|
|
type: 'FIELD_TYPE_CHANGED',
|
|
},
|
|
});
|
|
|
|
persistedTest({
|
|
name: 'FieldTypeChangedModel (safe)',
|
|
schemaBefore: /* GraphQL */ `
|
|
type Query {
|
|
a: String
|
|
}
|
|
`,
|
|
schemaAfter: /* GraphQL */ `
|
|
type Query {
|
|
a: String!
|
|
}
|
|
`,
|
|
equalsObject: {
|
|
meta: {
|
|
typeName: 'Query',
|
|
fieldName: 'a',
|
|
oldFieldType: 'String',
|
|
newFieldType: 'String!',
|
|
isSafeFieldTypeChange: true,
|
|
},
|
|
type: 'FIELD_TYPE_CHANGED',
|
|
},
|
|
});
|
|
|
|
persistedTest({
|
|
name: 'FieldArgumentAddedModel (unsafe)',
|
|
schemaBefore: /* GraphQL */ `
|
|
type Query {
|
|
a: String
|
|
}
|
|
`,
|
|
schemaAfter: /* GraphQL */ `
|
|
type Query {
|
|
a(a: String!): String!
|
|
}
|
|
`,
|
|
equalsObject: {
|
|
meta: {
|
|
typeName: 'Query',
|
|
fieldName: 'a',
|
|
addedArgumentName: 'a',
|
|
addedArgumentType: 'String!',
|
|
hasDefaultValue: false,
|
|
isAddedFieldArgumentBreaking: true,
|
|
addedToNewField: false,
|
|
},
|
|
type: 'FIELD_ARGUMENT_ADDED',
|
|
},
|
|
});
|
|
|
|
persistedTest({
|
|
name: 'FieldArgumentAddedModel (safe)',
|
|
schemaBefore: /* GraphQL */ `
|
|
type Query {
|
|
a: String
|
|
}
|
|
`,
|
|
schemaAfter: /* GraphQL */ `
|
|
type Query {
|
|
a(a: String): String
|
|
}
|
|
`,
|
|
equalsObject: {
|
|
meta: {
|
|
typeName: 'Query',
|
|
fieldName: 'a',
|
|
addedArgumentName: 'a',
|
|
addedArgumentType: 'String',
|
|
hasDefaultValue: false,
|
|
isAddedFieldArgumentBreaking: false,
|
|
addedToNewField: false,
|
|
},
|
|
type: 'FIELD_ARGUMENT_ADDED',
|
|
},
|
|
});
|
|
|
|
persistedTest({
|
|
name: 'FieldArgumentRemovedModel (safe)',
|
|
schemaBefore: /* GraphQL */ `
|
|
type Query {
|
|
a(a: String): String
|
|
}
|
|
`,
|
|
schemaAfter: /* GraphQL */ `
|
|
type Query {
|
|
a: String!
|
|
}
|
|
`,
|
|
equalsObject: {
|
|
meta: {
|
|
typeName: 'Query',
|
|
fieldName: 'a',
|
|
removedFieldArgumentName: 'a',
|
|
removedFieldType: 'String',
|
|
},
|
|
type: 'FIELD_ARGUMENT_REMOVED',
|
|
},
|
|
});
|
|
|
|
persistedTest({
|
|
name: 'InputFieldAddedModel (safe)',
|
|
schemaBefore: /* GraphQL */ `
|
|
type Query {
|
|
a: String
|
|
}
|
|
|
|
input A {
|
|
a: String
|
|
}
|
|
`,
|
|
schemaAfter: /* GraphQL */ `
|
|
type Query {
|
|
a: String!
|
|
}
|
|
|
|
input A {
|
|
a: String
|
|
b: String
|
|
}
|
|
`,
|
|
equalsObject: {
|
|
meta: {
|
|
inputName: 'A',
|
|
addedInputFieldName: 'b',
|
|
isAddedInputFieldTypeNullable: true,
|
|
addedInputFieldType: 'String',
|
|
addedToNewType: false,
|
|
},
|
|
type: 'INPUT_FIELD_ADDED',
|
|
},
|
|
});
|
|
|
|
persistedTest({
|
|
name: 'InputFieldAddedModel (unsafe)',
|
|
schemaBefore: /* GraphQL */ `
|
|
type Query {
|
|
a: String
|
|
}
|
|
|
|
input A {
|
|
a: String
|
|
}
|
|
`,
|
|
schemaAfter: /* GraphQL */ `
|
|
type Query {
|
|
a: String!
|
|
}
|
|
|
|
input A {
|
|
a: String
|
|
b: String!
|
|
}
|
|
`,
|
|
equalsObject: {
|
|
meta: {
|
|
inputName: 'A',
|
|
addedInputFieldName: 'b',
|
|
isAddedInputFieldTypeNullable: false,
|
|
addedInputFieldType: 'String!',
|
|
addedToNewType: false,
|
|
},
|
|
type: 'INPUT_FIELD_ADDED',
|
|
},
|
|
});
|
|
|
|
persistedTest({
|
|
name: 'InputFieldDescriptionAddedModel',
|
|
schemaBefore: /* GraphQL */ `
|
|
type Query {
|
|
a: String
|
|
}
|
|
|
|
input A {
|
|
a: String
|
|
}
|
|
`,
|
|
schemaAfter: /* GraphQL */ `
|
|
type Query {
|
|
a: String!
|
|
}
|
|
|
|
input A {
|
|
"""
|
|
yo
|
|
"""
|
|
a: String
|
|
}
|
|
`,
|
|
equalsObject: {
|
|
meta: {
|
|
inputName: 'A',
|
|
inputFieldName: 'a',
|
|
addedInputFieldDescription: 'yo',
|
|
},
|
|
type: 'INPUT_FIELD_DESCRIPTION_ADDED',
|
|
},
|
|
});
|
|
|
|
persistedTest({
|
|
name: 'InputFieldDescriptionRemovedModel',
|
|
schemaBefore: /* GraphQL */ `
|
|
type Query {
|
|
a: String
|
|
}
|
|
|
|
input A {
|
|
"""
|
|
yo
|
|
"""
|
|
a: String
|
|
}
|
|
`,
|
|
schemaAfter: /* GraphQL */ `
|
|
type Query {
|
|
a: String!
|
|
}
|
|
|
|
input A {
|
|
a: String
|
|
}
|
|
`,
|
|
equalsObject: {
|
|
meta: {
|
|
inputName: 'A',
|
|
inputFieldName: 'a',
|
|
removedDescription: 'yo',
|
|
},
|
|
type: 'INPUT_FIELD_DESCRIPTION_REMOVED',
|
|
},
|
|
});
|
|
|
|
persistedTest({
|
|
name: 'InputFieldDescriptionChangedModel',
|
|
schemaBefore: /* GraphQL */ `
|
|
type Query {
|
|
a: String
|
|
}
|
|
|
|
input A {
|
|
"""
|
|
yo
|
|
"""
|
|
a: String
|
|
}
|
|
`,
|
|
schemaAfter: /* GraphQL */ `
|
|
type Query {
|
|
a: String!
|
|
}
|
|
|
|
input A {
|
|
"""
|
|
yoyo
|
|
"""
|
|
a: String
|
|
}
|
|
`,
|
|
equalsObject: {
|
|
meta: {
|
|
inputName: 'A',
|
|
inputFieldName: 'a',
|
|
oldInputFieldDescription: 'yo',
|
|
newInputFieldDescription: 'yoyo',
|
|
},
|
|
type: 'INPUT_FIELD_DESCRIPTION_CHANGED',
|
|
},
|
|
});
|
|
|
|
persistedTest({
|
|
name: 'InputFieldDefaultValueChangedModel',
|
|
schemaBefore: /* GraphQL */ `
|
|
type Query {
|
|
a: String
|
|
}
|
|
|
|
input A {
|
|
a: String = "yo"
|
|
}
|
|
`,
|
|
schemaAfter: /* GraphQL */ `
|
|
type Query {
|
|
a: String!
|
|
}
|
|
|
|
input A {
|
|
a: String = null
|
|
}
|
|
`,
|
|
equalsObject: {
|
|
meta: {
|
|
inputName: 'A',
|
|
inputFieldName: 'a',
|
|
oldDefaultValue: `"yo"`,
|
|
newDefaultValue: 'null',
|
|
},
|
|
type: 'INPUT_FIELD_DEFAULT_VALUE_CHANGED',
|
|
},
|
|
});
|
|
|
|
persistedTest({
|
|
name: 'InputFieldTypeChangedModel (safe)',
|
|
schemaBefore: /* GraphQL */ `
|
|
type Query {
|
|
a: String
|
|
}
|
|
|
|
input A {
|
|
a: String!
|
|
}
|
|
`,
|
|
schemaAfter: /* GraphQL */ `
|
|
type Query {
|
|
a: String!
|
|
}
|
|
|
|
input A {
|
|
a: String
|
|
}
|
|
`,
|
|
equalsObject: {
|
|
meta: {
|
|
inputName: 'A',
|
|
inputFieldName: 'a',
|
|
oldInputFieldType: 'String!',
|
|
newInputFieldType: 'String',
|
|
isInputFieldTypeChangeSafe: true,
|
|
},
|
|
type: 'INPUT_FIELD_TYPE_CHANGED',
|
|
},
|
|
});
|
|
|
|
persistedTest({
|
|
name: 'InputFieldTypeChangedModel (unsafe)',
|
|
schemaBefore: /* GraphQL */ `
|
|
type Query {
|
|
a: String
|
|
}
|
|
|
|
input A {
|
|
a: String
|
|
}
|
|
`,
|
|
schemaAfter: /* GraphQL */ `
|
|
type Query {
|
|
a: String!
|
|
}
|
|
|
|
input A {
|
|
a: String!
|
|
}
|
|
`,
|
|
equalsObject: {
|
|
meta: {
|
|
inputName: 'A',
|
|
inputFieldName: 'a',
|
|
oldInputFieldType: 'String',
|
|
newInputFieldType: 'String!',
|
|
isInputFieldTypeChangeSafe: false,
|
|
},
|
|
type: 'INPUT_FIELD_TYPE_CHANGED',
|
|
},
|
|
});
|
|
|
|
persistedTest({
|
|
name: 'ObjectTypeInterfaceAddedModel',
|
|
schemaBefore: /* GraphQL */ `
|
|
type Query {
|
|
a: String
|
|
}
|
|
|
|
interface Foo {
|
|
a: String
|
|
}
|
|
`,
|
|
schemaAfter: /* GraphQL */ `
|
|
type Query implements Foo {
|
|
a: String!
|
|
}
|
|
|
|
interface Foo {
|
|
a: String
|
|
}
|
|
`,
|
|
equalsObject: {
|
|
meta: {
|
|
objectTypeName: 'Query',
|
|
addedInterfaceName: 'Foo',
|
|
addedToNewType: false,
|
|
},
|
|
type: 'OBJECT_TYPE_INTERFACE_ADDED',
|
|
},
|
|
});
|
|
|
|
persistedTest({
|
|
name: 'ObjectTypeInterfaceAddedModel',
|
|
schemaBefore: /* GraphQL */ `
|
|
type Query implements Foo {
|
|
a: String!
|
|
}
|
|
|
|
interface Foo {
|
|
a: String
|
|
}
|
|
`,
|
|
schemaAfter: /* GraphQL */ `
|
|
type Query {
|
|
a: String
|
|
}
|
|
|
|
interface Foo {
|
|
a: String
|
|
}
|
|
`,
|
|
equalsObject: {
|
|
meta: {
|
|
objectTypeName: 'Query',
|
|
removedInterfaceName: 'Foo',
|
|
},
|
|
type: 'OBJECT_TYPE_INTERFACE_REMOVED',
|
|
},
|
|
});
|
|
|
|
// persistedTest({
|
|
// name: 'SchemaQueryTypeChangedModel',
|
|
// schemaBefore: /* GraphQL */ `
|
|
// type Query {
|
|
// a: Query
|
|
// b: Query2
|
|
// }
|
|
|
|
// type Query2 {
|
|
// a: Query
|
|
// b: Query2
|
|
// }
|
|
|
|
// schema {
|
|
// query: Query
|
|
// }
|
|
// `,
|
|
// schemaAfter: /* GraphQL */ `
|
|
// type Query {
|
|
// a: Query
|
|
// b: Query2
|
|
// }
|
|
|
|
// type Query2 {
|
|
// a: Query
|
|
// b: Query2
|
|
// }
|
|
|
|
// schema {
|
|
// query: Query2
|
|
// }
|
|
// `,
|
|
// equalsObject: {
|
|
// meta: {
|
|
// oldQueryTypeName: 'Query',
|
|
// newQueryTypeName: 'Query2',
|
|
// },
|
|
// type: 'SCHEMA_QUERY_TYPE_CHANGED',
|
|
// },
|
|
// });
|
|
|
|
// persistedTest({
|
|
// name: 'SchemaMutationTypeChangedModel',
|
|
// schemaBefore: /* GraphQL */ `
|
|
// type Query {
|
|
// a: String!
|
|
// }
|
|
|
|
// type Mutation {
|
|
// b: String!
|
|
// }
|
|
|
|
// type Mutation1 {
|
|
// c: String!
|
|
// }
|
|
|
|
// schema {
|
|
// query: Query
|
|
// mutation: Mutation
|
|
// }
|
|
// `,
|
|
// schemaAfter: /* GraphQL */ `
|
|
// type Query {
|
|
// a: String!
|
|
// }
|
|
|
|
// type Mutation {
|
|
// b: String!
|
|
// }
|
|
|
|
// type Mutation1 {
|
|
// c: String!
|
|
// }
|
|
|
|
// schema {
|
|
// query: Query
|
|
// mutation: Mutation1
|
|
// }
|
|
// `,
|
|
// equalsObject: {
|
|
// meta: {
|
|
// oldMutationTypeName: 'Mutation',
|
|
// newMutationTypeName: 'Mutation1',
|
|
// },
|
|
// type: 'SCHEMA_MUTATION_TYPE_CHANGED',
|
|
// },
|
|
// });
|
|
|
|
// persistedTest({
|
|
// name: 'SchemaSubscriptionTypeChangedModel',
|
|
// schemaBefore: /* GraphQL */ `
|
|
// type Query {
|
|
// a: String!
|
|
// }
|
|
|
|
// type Subscription {
|
|
// b: String!
|
|
// }
|
|
|
|
// type Subscription1 {
|
|
// c: String!
|
|
// }
|
|
|
|
// schema {
|
|
// query: Query
|
|
// subscription: Subscription
|
|
// }
|
|
// `,
|
|
// schemaAfter: /* GraphQL */ `
|
|
// type Query {
|
|
// a: String!
|
|
// }
|
|
|
|
// type Subscription {
|
|
// b: String!
|
|
// }
|
|
|
|
// type Subscription1 {
|
|
// c: String!
|
|
// }
|
|
|
|
// schema {
|
|
// query: Query
|
|
// subscription: Subscription1
|
|
// }
|
|
// `,
|
|
// equalsObject: {
|
|
// meta: {
|
|
// oldSubscriptionTypeName: 'Subscription',
|
|
// newSubscriptionTypeName: 'Subscription1',
|
|
// },
|
|
// type: 'SCHEMA_SUBSCRIPTION_TYPE_CHANGED',
|
|
// },
|
|
// });
|
|
|
|
persistedTest({
|
|
name: 'TypeRemovedModel',
|
|
schemaBefore: /* GraphQL */ `
|
|
type Query {
|
|
a: String!
|
|
}
|
|
|
|
type A {
|
|
b: String!
|
|
}
|
|
`,
|
|
schemaAfter: /* GraphQL */ `
|
|
type Query {
|
|
a: String!
|
|
}
|
|
`,
|
|
equalsObject: {
|
|
meta: {
|
|
removedTypeName: 'A',
|
|
},
|
|
type: 'TYPE_REMOVED',
|
|
},
|
|
});
|
|
|
|
persistedTest({
|
|
name: 'TypeAddedModel',
|
|
schemaBefore: /* GraphQL */ `
|
|
type Query {
|
|
a: String!
|
|
}
|
|
`,
|
|
schemaAfter: /* GraphQL */ `
|
|
type Query {
|
|
a: String!
|
|
}
|
|
|
|
type A {
|
|
b: String!
|
|
}
|
|
`,
|
|
equalsObject: {
|
|
meta: {
|
|
addedTypeName: 'A',
|
|
addedTypeKind: 'ObjectTypeDefinition',
|
|
},
|
|
type: 'TYPE_ADDED',
|
|
},
|
|
});
|
|
|
|
persistedTest({
|
|
name: 'TypeKindChangedModel',
|
|
schemaBefore: /* GraphQL */ `
|
|
type Query {
|
|
a: String!
|
|
}
|
|
|
|
type A {
|
|
b: String!
|
|
}
|
|
`,
|
|
schemaAfter: /* GraphQL */ `
|
|
type Query {
|
|
a: String!
|
|
}
|
|
|
|
interface A {
|
|
b: String!
|
|
}
|
|
`,
|
|
equalsObject: {
|
|
meta: {
|
|
typeName: 'A',
|
|
oldTypeKind: 'ObjectTypeDefinition',
|
|
newTypeKind: 'InterfaceTypeDefinition',
|
|
},
|
|
type: 'TYPE_KIND_CHANGED',
|
|
},
|
|
});
|
|
|
|
persistedTest({
|
|
name: 'TypeDescriptionChangedModel',
|
|
schemaBefore: /* GraphQL */ `
|
|
type Query {
|
|
a: String!
|
|
}
|
|
|
|
"""
|
|
yo
|
|
"""
|
|
type A {
|
|
b: String!
|
|
}
|
|
`,
|
|
schemaAfter: /* GraphQL */ `
|
|
type Query {
|
|
a: String!
|
|
}
|
|
|
|
"""
|
|
yoyo
|
|
"""
|
|
type A {
|
|
b: String!
|
|
}
|
|
`,
|
|
equalsObject: {
|
|
meta: {
|
|
typeName: 'A',
|
|
oldTypeDescription: 'yo',
|
|
newTypeDescription: 'yoyo',
|
|
},
|
|
type: 'TYPE_DESCRIPTION_CHANGED',
|
|
},
|
|
});
|
|
|
|
persistedTest({
|
|
name: 'TypeDescriptionAddedModel',
|
|
schemaBefore: /* GraphQL */ `
|
|
type Query {
|
|
a: String!
|
|
}
|
|
|
|
type A {
|
|
b: String!
|
|
}
|
|
`,
|
|
schemaAfter: /* GraphQL */ `
|
|
type Query {
|
|
a: String!
|
|
}
|
|
|
|
"""
|
|
yoyo
|
|
"""
|
|
type A {
|
|
b: String!
|
|
}
|
|
`,
|
|
equalsObject: {
|
|
meta: {
|
|
typeName: 'A',
|
|
addedTypeDescription: 'yoyo',
|
|
},
|
|
type: 'TYPE_DESCRIPTION_ADDED',
|
|
},
|
|
});
|
|
|
|
persistedTest({
|
|
name: 'TypeDescriptionRemovedModel',
|
|
schemaBefore: /* GraphQL */ `
|
|
type Query {
|
|
a: String!
|
|
}
|
|
|
|
"""
|
|
yoyo
|
|
"""
|
|
type A {
|
|
b: String!
|
|
}
|
|
`,
|
|
schemaAfter: /* GraphQL */ `
|
|
type Query {
|
|
a: String!
|
|
}
|
|
|
|
type A {
|
|
b: String!
|
|
}
|
|
`,
|
|
equalsObject: {
|
|
meta: {
|
|
typeName: 'A',
|
|
removedTypeDescription: 'yoyo',
|
|
},
|
|
type: 'TYPE_DESCRIPTION_REMOVED',
|
|
},
|
|
});
|
|
|
|
persistedTest({
|
|
name: 'UnionMemberAddedModel',
|
|
schemaBefore: /* GraphQL */ `
|
|
type Query {
|
|
a: String!
|
|
}
|
|
|
|
type A {
|
|
b: String!
|
|
}
|
|
|
|
type B {
|
|
d: String!
|
|
}
|
|
|
|
union C = A
|
|
`,
|
|
schemaAfter: /* GraphQL */ `
|
|
type Query {
|
|
a: String!
|
|
}
|
|
|
|
type A {
|
|
b: String!
|
|
}
|
|
|
|
type B {
|
|
d: String!
|
|
}
|
|
|
|
union C = A | B
|
|
`,
|
|
equalsObject: {
|
|
meta: {
|
|
unionName: 'C',
|
|
addedUnionMemberTypeName: 'B',
|
|
addedToNewType: false,
|
|
},
|
|
type: 'UNION_MEMBER_ADDED',
|
|
},
|
|
});
|
|
|
|
persistedTest({
|
|
name: 'UnionMemberRemovedModel',
|
|
schemaBefore: /* GraphQL */ `
|
|
type Query {
|
|
a: String!
|
|
}
|
|
|
|
type A {
|
|
b: String!
|
|
}
|
|
|
|
type B {
|
|
d: String!
|
|
}
|
|
|
|
union C = A | B
|
|
`,
|
|
schemaAfter: /* GraphQL */ `
|
|
type Query {
|
|
a: String!
|
|
}
|
|
|
|
type A {
|
|
b: String!
|
|
}
|
|
|
|
type B {
|
|
d: String!
|
|
}
|
|
|
|
union C = A
|
|
`,
|
|
equalsObject: {
|
|
meta: {
|
|
unionName: 'C',
|
|
removedUnionMemberTypeName: 'B',
|
|
},
|
|
type: 'UNION_MEMBER_REMOVED',
|
|
},
|
|
});
|
|
|
|
persistedTest({
|
|
name: 'RegistryServiceUrlChangeModel',
|
|
type: ProjectType.Federation,
|
|
schemaBefore: /* GraphQL */ `
|
|
type Query {
|
|
a: String!
|
|
}
|
|
`,
|
|
schemaAfter: /* GraphQL */ `
|
|
type Query {
|
|
a: String!
|
|
}
|
|
`,
|
|
serviceUrlAfter: 'http://iliketurtles.com/graphql',
|
|
equalsObject: {
|
|
meta: {
|
|
serviceName: 'test',
|
|
serviceUrls: {
|
|
old: 'http://localhost:4000',
|
|
new: 'http://iliketurtles.com/graphql',
|
|
},
|
|
},
|
|
type: 'REGISTRY_SERVICE_URL_CHANGED',
|
|
},
|
|
});
|
|
|
|
// DirectiveUsage tests
|
|
persistedTest({
|
|
name: 'DirectiveUsageEnumValueAddedModel',
|
|
schemaBefore: /* GraphQL */ `
|
|
directive @auth on ENUM_VALUE
|
|
|
|
enum Role {
|
|
ADMIN
|
|
USER
|
|
}
|
|
|
|
type Query {
|
|
me: String
|
|
}
|
|
`,
|
|
schemaAfter: /* GraphQL */ `
|
|
directive @auth on ENUM_VALUE
|
|
|
|
enum Role {
|
|
ADMIN
|
|
USER @auth
|
|
}
|
|
|
|
type Query {
|
|
me: String
|
|
}
|
|
`,
|
|
equalsObject: {
|
|
meta: {
|
|
enumName: 'Role',
|
|
enumValueName: 'USER',
|
|
addedDirectiveName: 'auth',
|
|
addedToNewType: false,
|
|
directiveRepeatedTimes: 1,
|
|
},
|
|
type: 'DIRECTIVE_USAGE_ENUM_VALUE_ADDED',
|
|
},
|
|
});
|
|
|
|
persistedTest({
|
|
name: 'DirectiveUsageEnumValueRemovedModel',
|
|
schemaBefore: /* GraphQL */ `
|
|
directive @auth on ENUM_VALUE
|
|
|
|
enum Role {
|
|
ADMIN
|
|
USER @auth
|
|
}
|
|
|
|
type Query {
|
|
me: String
|
|
}
|
|
`,
|
|
schemaAfter: /* GraphQL */ `
|
|
directive @auth on ENUM_VALUE
|
|
|
|
enum Role {
|
|
ADMIN
|
|
USER
|
|
}
|
|
|
|
type Query {
|
|
me: String
|
|
}
|
|
`,
|
|
equalsObject: {
|
|
meta: {
|
|
enumName: 'Role',
|
|
enumValueName: 'USER',
|
|
removedDirectiveName: 'auth',
|
|
directiveRepeatedTimes: 1,
|
|
},
|
|
type: 'DIRECTIVE_USAGE_ENUM_VALUE_REMOVED',
|
|
},
|
|
});
|
|
|
|
persistedTest({
|
|
name: 'DirectiveUsageFieldDefinitionAddedModel',
|
|
schemaBefore: /* GraphQL */ `
|
|
directive @deprecated on FIELD_DEFINITION
|
|
|
|
type User {
|
|
id: ID!
|
|
name: String
|
|
}
|
|
|
|
type Query {
|
|
user: User
|
|
}
|
|
`,
|
|
schemaAfter: /* GraphQL */ `
|
|
directive @deprecated on FIELD_DEFINITION
|
|
|
|
type User {
|
|
id: ID!
|
|
name: String @deprecated
|
|
}
|
|
|
|
type Query {
|
|
user: User
|
|
}
|
|
`,
|
|
equalsObject: {
|
|
meta: {
|
|
deprecationReason: 'No longer supported',
|
|
typeName: 'User',
|
|
fieldName: 'name',
|
|
},
|
|
type: 'FIELD_DEPRECATION_ADDED',
|
|
},
|
|
});
|
|
|
|
persistedTest({
|
|
name: 'DirectiveUsageFieldDefinitionRemovedModel',
|
|
schemaBefore: /* GraphQL */ `
|
|
directive @deprecated on FIELD_DEFINITION
|
|
|
|
type User {
|
|
id: ID!
|
|
name: String @deprecated
|
|
}
|
|
|
|
type Query {
|
|
user: User
|
|
}
|
|
`,
|
|
schemaAfter: /* GraphQL */ `
|
|
directive @deprecated on FIELD_DEFINITION
|
|
|
|
type User {
|
|
id: ID!
|
|
name: String
|
|
}
|
|
|
|
type Query {
|
|
user: User
|
|
}
|
|
`,
|
|
equalsObject: {
|
|
meta: {
|
|
typeName: 'User',
|
|
fieldName: 'name',
|
|
},
|
|
type: 'FIELD_DEPRECATION_REMOVED',
|
|
},
|
|
});
|
|
|
|
persistedTest({
|
|
name: 'DirectiveUsageArgumentDefinitionAddedModel',
|
|
schemaBefore: /* GraphQL */ `
|
|
directive @validate on ARGUMENT_DEFINITION
|
|
|
|
type Query {
|
|
user(id: ID!): String
|
|
}
|
|
`,
|
|
schemaAfter: /* GraphQL */ `
|
|
directive @validate on ARGUMENT_DEFINITION
|
|
|
|
type Query {
|
|
user(id: ID! @validate): String
|
|
}
|
|
`,
|
|
equalsObject: {
|
|
meta: {
|
|
typeName: 'Query',
|
|
fieldName: 'user',
|
|
argumentName: 'id',
|
|
addedDirectiveName: 'validate',
|
|
addedToNewType: false,
|
|
directiveRepeatedTimes: 1,
|
|
},
|
|
type: 'DIRECTIVE_USAGE_ARGUMENT_DEFINITION_ADDED',
|
|
},
|
|
});
|
|
|
|
persistedTest({
|
|
name: 'DirectiveUsageArgumentDefinitionRemovedModel',
|
|
schemaBefore: /* GraphQL */ `
|
|
directive @validate on ARGUMENT_DEFINITION
|
|
|
|
type Query {
|
|
user(id: ID! @validate): String
|
|
}
|
|
`,
|
|
schemaAfter: /* GraphQL */ `
|
|
directive @validate on ARGUMENT_DEFINITION
|
|
|
|
type Query {
|
|
user(id: ID!): String
|
|
}
|
|
`,
|
|
equalsObject: {
|
|
meta: {
|
|
typeName: 'Query',
|
|
fieldName: 'user',
|
|
argumentName: 'id',
|
|
removedDirectiveName: 'validate',
|
|
directiveRepeatedTimes: 1,
|
|
},
|
|
type: 'DIRECTIVE_USAGE_ARGUMENT_DEFINITION_REMOVED',
|
|
},
|
|
});
|
|
|
|
persistedTest({
|
|
name: 'DirectiveUsageObjectAddedModel',
|
|
schemaBefore: /* GraphQL */ `
|
|
directive @auth on OBJECT
|
|
|
|
type User {
|
|
id: ID!
|
|
}
|
|
|
|
type Query {
|
|
user: User
|
|
}
|
|
`,
|
|
schemaAfter: /* GraphQL */ `
|
|
directive @auth on OBJECT
|
|
|
|
type User @auth {
|
|
id: ID!
|
|
}
|
|
|
|
type Query {
|
|
user: User
|
|
}
|
|
`,
|
|
equalsObject: {
|
|
meta: {
|
|
objectName: 'User',
|
|
addedDirectiveName: 'auth',
|
|
addedToNewType: false,
|
|
directiveRepeatedTimes: 1,
|
|
},
|
|
type: 'DIRECTIVE_USAGE_OBJECT_ADDED',
|
|
},
|
|
});
|
|
|
|
persistedTest({
|
|
name: 'DirectiveUsageObjectRemovedModel',
|
|
schemaBefore: /* GraphQL */ `
|
|
directive @auth on OBJECT
|
|
|
|
type User @auth {
|
|
id: ID!
|
|
}
|
|
|
|
type Query {
|
|
user: User
|
|
}
|
|
`,
|
|
schemaAfter: /* GraphQL */ `
|
|
directive @auth on OBJECT
|
|
|
|
type User {
|
|
id: ID!
|
|
}
|
|
|
|
type Query {
|
|
user: User
|
|
}
|
|
`,
|
|
equalsObject: {
|
|
meta: {
|
|
objectName: 'User',
|
|
removedDirectiveName: 'auth',
|
|
directiveRepeatedTimes: 1,
|
|
},
|
|
type: 'DIRECTIVE_USAGE_OBJECT_REMOVED',
|
|
},
|
|
});
|
|
|
|
persistedTest({
|
|
name: 'DirectiveUsageInputFieldDefinitionAddedModel',
|
|
schemaBefore: /* GraphQL */ `
|
|
directive @validate on INPUT_FIELD_DEFINITION
|
|
|
|
input UserInput {
|
|
name: String!
|
|
email: String!
|
|
}
|
|
|
|
type Query {
|
|
createUser(input: UserInput!): String
|
|
}
|
|
`,
|
|
schemaAfter: /* GraphQL */ `
|
|
directive @validate on INPUT_FIELD_DEFINITION
|
|
|
|
input UserInput {
|
|
name: String!
|
|
email: String! @validate
|
|
}
|
|
|
|
type Query {
|
|
createUser(input: UserInput!): String
|
|
}
|
|
`,
|
|
equalsObject: {
|
|
meta: {
|
|
inputObjectName: 'UserInput',
|
|
inputFieldName: 'email',
|
|
addedDirectiveName: 'validate',
|
|
addedToNewType: false,
|
|
directiveRepeatedTimes: 1,
|
|
inputFieldType: 'String!',
|
|
},
|
|
type: 'DIRECTIVE_USAGE_INPUT_FIELD_DEFINITION_ADDED',
|
|
},
|
|
});
|
|
|
|
persistedTest({
|
|
name: 'DirectiveUsageInputFieldDefinitionRemovedModel',
|
|
schemaBefore: /* GraphQL */ `
|
|
directive @validate on INPUT_FIELD_DEFINITION
|
|
|
|
input UserInput {
|
|
name: String!
|
|
email: String! @validate
|
|
}
|
|
|
|
type Query {
|
|
createUser(input: UserInput!): String
|
|
}
|
|
`,
|
|
schemaAfter: /* GraphQL */ `
|
|
directive @validate on INPUT_FIELD_DEFINITION
|
|
|
|
input UserInput {
|
|
name: String!
|
|
email: String!
|
|
}
|
|
|
|
type Query {
|
|
createUser(input: UserInput!): String
|
|
}
|
|
`,
|
|
equalsObject: {
|
|
meta: {
|
|
directiveRepeatedTimes: 0,
|
|
inputObjectName: 'UserInput',
|
|
inputFieldName: 'email',
|
|
removedDirectiveName: 'validate',
|
|
},
|
|
type: 'DIRECTIVE_USAGE_INPUT_FIELD_DEFINITION_REMOVED',
|
|
},
|
|
});
|
|
|
|
persistedTest({
|
|
name: 'DirectiveUsageInterfaceAddedModel',
|
|
schemaBefore: /* GraphQL */ `
|
|
directive @auth on INTERFACE
|
|
|
|
interface Node {
|
|
id: ID!
|
|
}
|
|
|
|
type Query {
|
|
node: Node
|
|
}
|
|
`,
|
|
schemaAfter: /* GraphQL */ `
|
|
directive @auth on INTERFACE
|
|
|
|
interface Node @auth {
|
|
id: ID!
|
|
}
|
|
|
|
type Query {
|
|
node: Node
|
|
}
|
|
`,
|
|
equalsObject: {
|
|
meta: {
|
|
interfaceName: 'Node',
|
|
addedDirectiveName: 'auth',
|
|
addedToNewType: false,
|
|
directiveRepeatedTimes: 1,
|
|
},
|
|
type: 'DIRECTIVE_USAGE_INTERFACE_ADDED',
|
|
},
|
|
});
|
|
|
|
persistedTest({
|
|
name: 'DirectiveUsageInterfaceRemovedModel',
|
|
schemaBefore: /* GraphQL */ `
|
|
directive @auth on INTERFACE
|
|
|
|
interface Node @auth {
|
|
id: ID!
|
|
}
|
|
|
|
type Query {
|
|
node: Node
|
|
}
|
|
`,
|
|
schemaAfter: /* GraphQL */ `
|
|
directive @auth on INTERFACE
|
|
|
|
interface Node {
|
|
id: ID!
|
|
}
|
|
|
|
type Query {
|
|
node: Node
|
|
}
|
|
`,
|
|
equalsObject: {
|
|
meta: {
|
|
directiveRepeatedTimes: 1,
|
|
interfaceName: 'Node',
|
|
removedDirectiveName: 'auth',
|
|
},
|
|
type: 'DIRECTIVE_USAGE_INTERFACE_REMOVED',
|
|
},
|
|
});
|
|
|
|
persistedTest({
|
|
name: 'DirectiveUsageArgumentAdded',
|
|
schemaBefore: /* GraphQL */ `
|
|
directive @auth(roles: [String!]) on OBJECT
|
|
|
|
interface Node {
|
|
id: ID!
|
|
}
|
|
|
|
type Query @auth {
|
|
node: Node
|
|
}
|
|
`,
|
|
schemaAfter: /* GraphQL */ `
|
|
directive @auth(roles: [String!]) on OBJECT
|
|
|
|
interface Node {
|
|
id: ID!
|
|
}
|
|
|
|
type Query @auth(roles: ["node:read"]) {
|
|
node: Node
|
|
}
|
|
`,
|
|
equalsObject: {
|
|
meta: {
|
|
addedArgumentName: 'roles',
|
|
addedArgumentValue: '["node:read"]',
|
|
directiveName: 'auth',
|
|
directiveRepeatedTimes: 1,
|
|
oldArgumentValue: null,
|
|
parentArgumentName: null,
|
|
parentEnumValueName: null,
|
|
parentFieldName: null,
|
|
parentTypeName: 'Query',
|
|
},
|
|
type: 'DIRECTIVE_USAGE_ARGUMENT_ADDED',
|
|
},
|
|
});
|
|
|
|
persistedTest({
|
|
name: 'DirectiveUsageArgumentRemoved',
|
|
schemaBefore: /* GraphQL */ `
|
|
directive @auth(roles: [String!]) on OBJECT
|
|
|
|
interface Node {
|
|
id: ID!
|
|
}
|
|
|
|
type Query @auth(roles: ["node:read"]) {
|
|
node: Node
|
|
}
|
|
`,
|
|
schemaAfter: /* GraphQL */ `
|
|
directive @auth(roles: [String!]) on OBJECT
|
|
|
|
interface Node {
|
|
id: ID!
|
|
}
|
|
|
|
type Query @auth {
|
|
node: Node
|
|
}
|
|
`,
|
|
equalsObject: {
|
|
meta: {
|
|
directiveName: 'auth',
|
|
directiveRepeatedTimes: 1,
|
|
parentArgumentName: null,
|
|
parentEnumValueName: null,
|
|
parentFieldName: null,
|
|
parentTypeName: 'Query',
|
|
removedArgumentName: 'roles',
|
|
},
|
|
type: 'DIRECTIVE_USAGE_ARGUMENT_REMOVED',
|
|
},
|
|
});
|
|
|
|
persistedTest({
|
|
name: 'DirectiveRepeatableAdded',
|
|
schemaBefore: /* GraphQL */ `
|
|
directive @auth on OBJECT
|
|
|
|
interface Node {
|
|
id: ID!
|
|
}
|
|
|
|
type Query @auth {
|
|
node: Node
|
|
}
|
|
`,
|
|
schemaAfter: /* GraphQL */ `
|
|
directive @auth repeatable on OBJECT
|
|
|
|
interface Node {
|
|
id: ID!
|
|
}
|
|
|
|
type Query @auth {
|
|
node: Node
|
|
}
|
|
`,
|
|
equalsObject: {
|
|
meta: {
|
|
directiveName: 'auth',
|
|
},
|
|
type: 'DIRECTIVE_REPEATABLE_ADDED',
|
|
},
|
|
});
|
|
|
|
persistedTest({
|
|
name: 'DirectiveRepeatableRemoved',
|
|
schemaBefore: /* GraphQL */ `
|
|
directive @auth repeatable on OBJECT
|
|
|
|
interface Node {
|
|
id: ID!
|
|
}
|
|
|
|
type Query @auth {
|
|
node: Node
|
|
}
|
|
`,
|
|
schemaAfter: /* GraphQL */ `
|
|
directive @auth on OBJECT
|
|
|
|
interface Node {
|
|
id: ID!
|
|
}
|
|
|
|
type Query @auth {
|
|
node: Node
|
|
}
|
|
`,
|
|
equalsObject: {
|
|
meta: {
|
|
directiveName: 'auth',
|
|
},
|
|
type: 'DIRECTIVE_REPEATABLE_REMOVED',
|
|
},
|
|
});
|
|
});
|
|
|
|
const SchemaCompareToPreviousVersionQuery = graphql(`
|
|
query SchemaCompareToPreviousVersionQuery(
|
|
$organizationSlug: String!
|
|
$projectSlug: String!
|
|
$targetSlug: String!
|
|
$version: ID!
|
|
) {
|
|
target(
|
|
reference: {
|
|
bySelector: {
|
|
organizationSlug: $organizationSlug
|
|
projectSlug: $projectSlug
|
|
targetSlug: $targetSlug
|
|
}
|
|
}
|
|
) {
|
|
id
|
|
schemaVersion(id: $version) {
|
|
id
|
|
sdl
|
|
supergraph
|
|
log {
|
|
... on PushedSchemaLog {
|
|
id
|
|
author
|
|
service
|
|
commit
|
|
serviceSdl
|
|
previousServiceSdl
|
|
}
|
|
... on DeletedSchemaLog {
|
|
id
|
|
deletedService
|
|
previousServiceSdl
|
|
}
|
|
}
|
|
schemaCompositionErrors {
|
|
nodes {
|
|
message
|
|
}
|
|
}
|
|
isFirstComposableVersion
|
|
breakingSchemaChanges {
|
|
nodes {
|
|
message(withSafeBasedOnUsageNote: false)
|
|
criticality
|
|
criticalityReason
|
|
path
|
|
approval {
|
|
approvedBy {
|
|
id
|
|
displayName
|
|
}
|
|
approvedAt
|
|
schemaCheckId
|
|
}
|
|
isSafeBasedOnUsage
|
|
}
|
|
}
|
|
safeSchemaChanges {
|
|
nodes {
|
|
message(withSafeBasedOnUsageNote: false)
|
|
criticality
|
|
criticalityReason
|
|
path
|
|
approval {
|
|
approvedBy {
|
|
id
|
|
displayName
|
|
}
|
|
approvedAt
|
|
schemaCheckId
|
|
}
|
|
isSafeBasedOnUsage
|
|
}
|
|
}
|
|
previousDiffableSchemaVersion {
|
|
id
|
|
supergraph
|
|
sdl
|
|
}
|
|
}
|
|
}
|
|
}
|
|
`);
|
|
|
|
test('Target.schemaVersion: result is read from the database', async () => {
|
|
const storage = await createStorage(connectionString(), 1);
|
|
|
|
try {
|
|
const serviceName = {
|
|
service: 'test',
|
|
};
|
|
|
|
const serviceUrl = { url: 'http://localhost:4000' };
|
|
|
|
const { createOrg, ownerToken } = await initSeed().createOwner();
|
|
const { createProject, organization } = await createOrg();
|
|
const { createTargetAccessToken, target, project } = await createProject(
|
|
ProjectType.Federation,
|
|
);
|
|
const readWriteToken = await createTargetAccessToken({});
|
|
|
|
const publishResult = await readWriteToken
|
|
.publishSchema({
|
|
author: 'gilad',
|
|
commit: '123',
|
|
sdl: `type Query { ping: String }`,
|
|
...serviceName,
|
|
...serviceUrl,
|
|
})
|
|
.then(r => r.expectNoGraphQLErrors());
|
|
expect(publishResult.schemaPublish.__typename).toBe('SchemaPublishSuccess');
|
|
|
|
const publishResult2 = await readWriteToken
|
|
.publishSchema({
|
|
force: true,
|
|
author: 'gilad',
|
|
commit: '456',
|
|
sdl: `type Query { ping: Int }`,
|
|
...serviceName,
|
|
...serviceUrl,
|
|
})
|
|
.then(r => r.expectNoGraphQLErrors());
|
|
|
|
if (publishResult2.schemaPublish.__typename !== 'SchemaPublishSuccess') {
|
|
expect(publishResult2.schemaPublish.__typename).toBe('SchemaPublishSuccess');
|
|
return;
|
|
}
|
|
|
|
const latestVersion = await storage.getMaybeLatestVersion({
|
|
targetId: target.id,
|
|
projectId: project.id,
|
|
organizationId: organization.id,
|
|
});
|
|
assertNonNull(latestVersion);
|
|
|
|
const result = await execute({
|
|
document: SchemaCompareToPreviousVersionQuery,
|
|
variables: {
|
|
organizationSlug: organization.slug,
|
|
projectSlug: project.slug,
|
|
targetSlug: target.slug,
|
|
version: latestVersion.id,
|
|
},
|
|
authToken: ownerToken,
|
|
}).then(res => res.expectNoGraphQLErrors());
|
|
|
|
expect(result?.target?.schemaVersion?.breakingSchemaChanges?.nodes).toMatchInlineSnapshot(`
|
|
[
|
|
{
|
|
approval: null,
|
|
criticality: Breaking,
|
|
criticalityReason: null,
|
|
isSafeBasedOnUsage: false,
|
|
message: Field 'Query.ping' changed type from 'String' to 'Int',
|
|
path: [
|
|
Query,
|
|
ping,
|
|
],
|
|
},
|
|
]
|
|
`);
|
|
expect(result?.target?.schemaVersion?.safeSchemaChanges?.nodes).toBeUndefined();
|
|
} finally {
|
|
await storage.destroy();
|
|
}
|
|
});
|
|
|
|
test('Composition Error (Federation 2) can be served from the database', async () => {
|
|
const storage = await createStorage(connectionString(), 1);
|
|
const serviceAddress = await getServiceHost('composition_federation_2', 3069, false);
|
|
|
|
try {
|
|
const initialSchema = /* GraphQL */ `
|
|
type Product @key(fields: "id") {
|
|
id: ID!
|
|
title: String
|
|
url: String
|
|
description: String
|
|
salesRankOverall: Int
|
|
salesRankInCategory: Int
|
|
images(size: Int = 1000): [String]
|
|
primaryImage(size: Int = 1000): String
|
|
}
|
|
|
|
type Query {
|
|
product(id: ID!): Product
|
|
}
|
|
`;
|
|
|
|
const newSchema = /* GraphQL */ `
|
|
type Product @key(fields: "IDONOTEXIST") {
|
|
id: ID!
|
|
title: String
|
|
url: String
|
|
description: String
|
|
salesRankOverall: Int
|
|
salesRankInCategory: Int
|
|
images(size: Int = 1000): [String]
|
|
primaryImage(size: Int = 1000): String
|
|
}
|
|
|
|
type Query {
|
|
product(id: ID!): Product
|
|
}
|
|
`;
|
|
|
|
const serviceName = {
|
|
service: 'test',
|
|
};
|
|
|
|
const serviceUrl = { url: 'http://localhost:4000' };
|
|
|
|
const { createOrg, ownerToken } = await initSeed().createOwner();
|
|
const { createProject, organization } = await createOrg();
|
|
const { createTargetAccessToken, target, project, setNativeFederation } = await createProject(
|
|
ProjectType.Federation,
|
|
);
|
|
const readWriteToken = await createTargetAccessToken({});
|
|
|
|
await updateSchemaComposition(
|
|
{
|
|
project: {
|
|
bySelector: {
|
|
projectSlug: project.slug,
|
|
organizationSlug: organization.slug,
|
|
},
|
|
},
|
|
method: {
|
|
external: {
|
|
endpoint: `http://${serviceAddress}/compose`,
|
|
// eslint-disable-next-line no-process-env
|
|
secret: process.env.EXTERNAL_COMPOSITION_SECRET!,
|
|
},
|
|
},
|
|
},
|
|
ownerToken,
|
|
).then(r => r.expectNoGraphQLErrors());
|
|
// set native federation to false to force external composition
|
|
await setNativeFederation(false);
|
|
|
|
const publishResult = await readWriteToken
|
|
.publishSchema({
|
|
author: 'gilad',
|
|
commit: '123',
|
|
sdl: initialSchema,
|
|
...serviceName,
|
|
...serviceUrl,
|
|
})
|
|
.then(r => r.expectNoGraphQLErrors());
|
|
|
|
expect(publishResult.schemaPublish.__typename).toBe('SchemaPublishSuccess');
|
|
|
|
const publishResult2 = await readWriteToken
|
|
.publishSchema({
|
|
author: 'gilad',
|
|
commit: '456',
|
|
sdl: newSchema,
|
|
...serviceName,
|
|
...serviceUrl,
|
|
})
|
|
.then(r => r.expectNoGraphQLErrors());
|
|
|
|
if (publishResult2.schemaPublish.__typename !== 'SchemaPublishSuccess') {
|
|
expect(publishResult2.schemaPublish.__typename).toBe('SchemaPublishSuccess');
|
|
return;
|
|
}
|
|
|
|
const latestVersion = await storage.getMaybeLatestVersion({
|
|
targetId: target.id,
|
|
projectId: project.id,
|
|
organizationId: organization.id,
|
|
});
|
|
assertNonNull(latestVersion);
|
|
|
|
const result = await execute({
|
|
document: SchemaCompareToPreviousVersionQuery,
|
|
variables: {
|
|
organizationSlug: organization.slug,
|
|
projectSlug: project.slug,
|
|
targetSlug: target.slug,
|
|
version: latestVersion.id,
|
|
},
|
|
authToken: ownerToken,
|
|
}).then(res => res.expectNoGraphQLErrors());
|
|
|
|
expect(result?.target?.schemaVersion?.schemaCompositionErrors?.nodes).toMatchInlineSnapshot(`
|
|
[
|
|
{
|
|
message: [test] On type "Product", for @key(fields: "IDONOTEXIST"): Cannot query field "IDONOTEXIST" on type "Product" (the field should either be added to this subgraph or, if it should not be resolved by this subgraph, you need to add it to this subgraph with @external).,
|
|
},
|
|
]
|
|
`);
|
|
} finally {
|
|
await storage.destroy();
|
|
}
|
|
});
|
|
|
|
test('Composition Network Failure (Federation 2)', async () => {
|
|
const storage = await createStorage(connectionString(), 1);
|
|
const serviceAddress = await getServiceHost('composition_federation_2', 3069, false);
|
|
|
|
try {
|
|
const initialSchema = /* GraphQL */ `
|
|
type Product @key(fields: "id") {
|
|
id: ID!
|
|
}
|
|
|
|
type Query {
|
|
product(id: ID!): Product
|
|
}
|
|
`;
|
|
|
|
const newSchema = /* GraphQL */ `
|
|
type Product @key(fields: "id") {
|
|
id: ID!
|
|
title: String
|
|
}
|
|
|
|
type Query {
|
|
product(id: ID!): Product
|
|
}
|
|
`;
|
|
|
|
const newNewSchema = /* GraphQL */ `
|
|
type Product @key(fields: "id") {
|
|
id: ID!
|
|
title: String
|
|
url: String
|
|
}
|
|
|
|
type Query {
|
|
product(id: ID!): Product
|
|
}
|
|
`;
|
|
|
|
const serviceName = {
|
|
service: 'test',
|
|
};
|
|
|
|
const serviceUrl = { url: 'http://localhost:4000' };
|
|
|
|
const { createOrg, ownerToken } = await initSeed().createOwner();
|
|
const { createProject, organization } = await createOrg();
|
|
const { createTargetAccessToken, target, project, setNativeFederation } = await createProject(
|
|
ProjectType.Federation,
|
|
);
|
|
const readWriteToken = await createTargetAccessToken({});
|
|
|
|
await updateSchemaComposition(
|
|
{
|
|
project: {
|
|
bySelector: {
|
|
projectSlug: project.slug,
|
|
organizationSlug: organization.slug,
|
|
},
|
|
},
|
|
method: {
|
|
external: {
|
|
endpoint: `http://${serviceAddress}/compose`,
|
|
// eslint-disable-next-line no-process-env
|
|
secret: process.env.EXTERNAL_COMPOSITION_SECRET!,
|
|
},
|
|
},
|
|
},
|
|
ownerToken,
|
|
).then(r => r.expectNoGraphQLErrors());
|
|
|
|
// Disable Native Federation v2 composition to allow the external composition to take place
|
|
await setNativeFederation(false);
|
|
|
|
const publishResult = await readWriteToken
|
|
.publishSchema({
|
|
author: 'gilad',
|
|
commit: '123',
|
|
sdl: initialSchema,
|
|
...serviceName,
|
|
...serviceUrl,
|
|
})
|
|
.then(r => r.expectNoGraphQLErrors());
|
|
|
|
expect(publishResult.schemaPublish.__typename).toBe('SchemaPublishSuccess');
|
|
|
|
const publishResult2 = await readWriteToken
|
|
.publishSchema({
|
|
author: 'gilad',
|
|
commit: '456',
|
|
sdl: newSchema,
|
|
...serviceName,
|
|
...serviceUrl,
|
|
})
|
|
.then(r => r.expectNoGraphQLErrors());
|
|
|
|
if (publishResult2.schemaPublish.__typename !== 'SchemaPublishSuccess') {
|
|
expect(publishResult2.schemaPublish.__typename).toBe('SchemaPublishSuccess');
|
|
return;
|
|
}
|
|
|
|
await updateSchemaComposition(
|
|
{
|
|
project: {
|
|
bySelector: {
|
|
projectSlug: project.slug,
|
|
organizationSlug: organization.slug,
|
|
},
|
|
},
|
|
method: {
|
|
external: {
|
|
endpoint: `http://${serviceAddress}/no_compose`,
|
|
// eslint-disable-next-line no-process-env
|
|
secret: process.env.EXTERNAL_COMPOSITION_SECRET!,
|
|
},
|
|
},
|
|
},
|
|
ownerToken,
|
|
).then(r => r.expectNoGraphQLErrors());
|
|
// Disable Native Federation v2 composition to allow the external composition to take place
|
|
await setNativeFederation(false);
|
|
|
|
const publishResult3 = await readWriteToken
|
|
.publishSchema({
|
|
author: 'gilad',
|
|
commit: '456',
|
|
sdl: newNewSchema,
|
|
...serviceName,
|
|
...serviceUrl,
|
|
})
|
|
.then(r => r.expectNoGraphQLErrors());
|
|
|
|
if (publishResult3.schemaPublish.__typename !== 'SchemaPublishError') {
|
|
expect(publishResult3.schemaPublish.__typename).toBe('SchemaPublishError');
|
|
return;
|
|
}
|
|
|
|
const latestVersion = await storage.getMaybeLatestVersion({
|
|
targetId: target.id,
|
|
projectId: project.id,
|
|
organizationId: organization.id,
|
|
});
|
|
assertNonNull(latestVersion);
|
|
|
|
const result = await execute({
|
|
document: SchemaCompareToPreviousVersionQuery,
|
|
variables: {
|
|
organizationSlug: organization.slug,
|
|
projectSlug: project.slug,
|
|
targetSlug: target.slug,
|
|
version: latestVersion.id,
|
|
},
|
|
authToken: ownerToken,
|
|
}).then(res => res.expectNoGraphQLErrors());
|
|
|
|
expect(result?.target?.schemaVersion?.safeSchemaChanges?.nodes).toMatchInlineSnapshot(`
|
|
[
|
|
{
|
|
approval: null,
|
|
criticality: Safe,
|
|
criticalityReason: null,
|
|
isSafeBasedOnUsage: false,
|
|
message: Field 'title' was added to object type 'Product',
|
|
path: [
|
|
Product,
|
|
title,
|
|
],
|
|
},
|
|
]
|
|
`);
|
|
expect(result?.target?.schemaVersion?.breakingSchemaChanges?.nodes).toBeUndefined();
|
|
|
|
expect(result?.target?.schemaVersion?.sdl).toMatchInlineSnapshot(`
|
|
type Product {
|
|
id: ID!
|
|
title: String
|
|
}
|
|
|
|
type Query {
|
|
product(id: ID!): Product
|
|
}
|
|
`);
|
|
expect(result?.target?.schemaVersion?.previousDiffableSchemaVersion?.sdl)
|
|
.toMatchInlineSnapshot(`
|
|
type Product {
|
|
id: ID!
|
|
}
|
|
|
|
type Query {
|
|
product(id: ID!): Product
|
|
}
|
|
`);
|
|
} finally {
|
|
await storage.destroy();
|
|
}
|
|
});
|
|
|
|
test.concurrent(
|
|
'service url change is persisted and can be fetched via api',
|
|
async ({ expect }) => {
|
|
const { createOrg } = await initSeed().createOwner();
|
|
const { createProject } = await createOrg();
|
|
const { createTargetAccessToken, compareToPreviousVersion } = await createProject(
|
|
ProjectType.Federation,
|
|
);
|
|
|
|
// Create a token with write rights
|
|
const writeToken = await createTargetAccessToken({});
|
|
|
|
const sdl = /* GraphQL */ `
|
|
type Query {
|
|
products: [Product]
|
|
}
|
|
type Product @key(fields: "id") {
|
|
id: ID!
|
|
}
|
|
`;
|
|
|
|
let publishProductsResult = await writeToken
|
|
.publishSchema({
|
|
url: 'https://api.com/products',
|
|
sdl,
|
|
service: 'foo',
|
|
})
|
|
.then(r => r.expectNoGraphQLErrors());
|
|
|
|
expect(publishProductsResult.schemaPublish.__typename).toBe('SchemaPublishSuccess');
|
|
|
|
publishProductsResult = await writeToken
|
|
.publishSchema({
|
|
url: 'https://api.com/products-new',
|
|
sdl,
|
|
service: 'foo',
|
|
})
|
|
.then(r => r.expectNoGraphQLErrors());
|
|
|
|
expect(publishProductsResult.schemaPublish.__typename).toBe('SchemaPublishSuccess');
|
|
|
|
const result = await writeToken.fetchLatestValidSchema();
|
|
const versionId = result.latestValidVersion?.id;
|
|
|
|
if (!versionId) {
|
|
expect(versionId).toBeInstanceOf(String);
|
|
return;
|
|
}
|
|
|
|
const compareResult = await compareToPreviousVersion(versionId);
|
|
expect(compareResult?.target?.schemaVersion?.safeSchemaChanges?.nodes).toMatchInlineSnapshot(`
|
|
[
|
|
{
|
|
approval: null,
|
|
criticality: Dangerous,
|
|
criticalityReason: The registry service url has changed,
|
|
isSafeBasedOnUsage: false,
|
|
message: [foo] New service url: 'https://api.com/products-new' (previously: 'https://api.com/products'),
|
|
path: null,
|
|
},
|
|
]
|
|
`);
|
|
expect(compareResult?.target?.schemaVersion?.breakingSchemaChanges?.nodes).toBeUndefined();
|
|
},
|
|
);
|
|
|
|
test.concurrent(
|
|
'service url change is persisted and can be fetched via api (in combination with other change)',
|
|
async ({ expect }) => {
|
|
const { createOrg } = await initSeed().createOwner();
|
|
const { createProject } = await createOrg();
|
|
const { createTargetAccessToken, compareToPreviousVersion } = await createProject(
|
|
ProjectType.Federation,
|
|
);
|
|
|
|
// Create a token with write rights
|
|
const writeToken = await createTargetAccessToken({});
|
|
|
|
let publishProductsResult = await writeToken
|
|
.publishSchema({
|
|
url: 'https://api.com/products',
|
|
sdl: /* GraphQL */ `
|
|
type Query {
|
|
products: [Product]
|
|
}
|
|
type Product @key(fields: "id") {
|
|
id: ID!
|
|
}
|
|
`,
|
|
service: 'foo',
|
|
})
|
|
.then(r => r.expectNoGraphQLErrors());
|
|
|
|
expect(publishProductsResult.schemaPublish.__typename).toBe('SchemaPublishSuccess');
|
|
|
|
publishProductsResult = await writeToken
|
|
.publishSchema({
|
|
url: 'https://api.com/products-new',
|
|
sdl: /* GraphQL */ `
|
|
type Query {
|
|
products: [Product]
|
|
}
|
|
type Product @key(fields: "id") {
|
|
id: ID!
|
|
name: String!
|
|
}
|
|
`,
|
|
service: 'foo',
|
|
})
|
|
.then(r => r.expectNoGraphQLErrors());
|
|
|
|
expect(publishProductsResult.schemaPublish.__typename).toBe('SchemaPublishSuccess');
|
|
|
|
const result = await writeToken.fetchLatestValidSchema();
|
|
const versionId = result.latestValidVersion?.id;
|
|
|
|
if (!versionId) {
|
|
expect(versionId).toBeInstanceOf(String);
|
|
return;
|
|
}
|
|
|
|
const compareResult = await compareToPreviousVersion(versionId);
|
|
|
|
expect(compareResult?.target?.schemaVersion?.safeSchemaChanges?.nodes).toMatchInlineSnapshot(`
|
|
[
|
|
{
|
|
approval: null,
|
|
criticality: Safe,
|
|
criticalityReason: null,
|
|
isSafeBasedOnUsage: false,
|
|
message: Field 'name' was added to object type 'Product',
|
|
path: [
|
|
Product,
|
|
name,
|
|
],
|
|
},
|
|
{
|
|
approval: null,
|
|
criticality: Dangerous,
|
|
criticalityReason: The registry service url has changed,
|
|
isSafeBasedOnUsage: false,
|
|
message: [foo] New service url: 'https://api.com/products-new' (previously: 'https://api.com/products'),
|
|
path: null,
|
|
},
|
|
]
|
|
`);
|
|
expect(compareResult?.target?.schemaVersion?.breakingSchemaChanges?.nodes).toBeUndefined();
|
|
},
|
|
);
|
|
|
|
const insertLegacyVersion = async (
|
|
pool: Awaited<ReturnType<typeof createPool>>,
|
|
args: {
|
|
sdl: string;
|
|
projectId: string;
|
|
targetId: string;
|
|
serviceUrl: string;
|
|
},
|
|
) => {
|
|
const logId = await pool.oneFirst<string>(sql`
|
|
INSERT INTO schema_log
|
|
(
|
|
author,
|
|
service_name,
|
|
service_url,
|
|
commit,
|
|
sdl,
|
|
project_id,
|
|
target_id,
|
|
metadata,
|
|
action
|
|
)
|
|
VALUES
|
|
(
|
|
${'Laurin did it again'},
|
|
lower(${'foo'}),
|
|
${args.serviceUrl}::text,
|
|
${'42069'}::text,
|
|
${args.sdl}::text,
|
|
${args.projectId},
|
|
${args.targetId},
|
|
${null},
|
|
'PUSH'
|
|
)
|
|
RETURNING id
|
|
`);
|
|
|
|
const versionId = await pool.oneFirst<string>(sql`
|
|
INSERT INTO schema_versions
|
|
(
|
|
is_composable,
|
|
target_id,
|
|
action_id
|
|
)
|
|
VALUES
|
|
(
|
|
${true},
|
|
${args.targetId},
|
|
${logId}
|
|
)
|
|
RETURNING "id"
|
|
`);
|
|
|
|
await pool.query(sql`
|
|
INSERT INTO
|
|
schema_version_to_log
|
|
(version_id, action_id)
|
|
VALUES
|
|
(${versionId}, ${logId})
|
|
`);
|
|
|
|
return versionId;
|
|
};
|
|
|
|
test.concurrent(
|
|
'service url change from legacy to new version is displayed correctly',
|
|
async ({ expect }) => {
|
|
let pool: Awaited<ReturnType<typeof createPool>> | undefined;
|
|
try {
|
|
const { createOrg } = await initSeed().createOwner();
|
|
const { createProject } = await createOrg();
|
|
const { project, target, createTargetAccessToken, compareToPreviousVersion } =
|
|
await createProject(ProjectType.Federation);
|
|
|
|
// Create a token with write rights
|
|
const writeToken = await createTargetAccessToken({});
|
|
|
|
// We need to seed a legacy entry in the database
|
|
|
|
const conn = connectionString();
|
|
pool = await createPool(conn);
|
|
|
|
const sdl = 'type Query { ping: String! }';
|
|
|
|
await insertLegacyVersion(pool, {
|
|
projectId: project.id,
|
|
targetId: target.id,
|
|
sdl,
|
|
serviceUrl: 'https://api.com/products',
|
|
});
|
|
|
|
const publishProductsResult = await writeToken
|
|
.publishSchema({
|
|
url: 'https://api.com/nah',
|
|
sdl,
|
|
service: 'foo',
|
|
})
|
|
.then(r => r.expectNoGraphQLErrors());
|
|
|
|
expect(publishProductsResult.schemaPublish.__typename).toBe('SchemaPublishSuccess');
|
|
|
|
const newVersionId = (await writeToken.fetchLatestValidSchema())?.latestValidVersion?.id;
|
|
|
|
if (!newVersionId) {
|
|
expect(newVersionId).toBeInstanceOf(String);
|
|
return;
|
|
}
|
|
|
|
const compareResult = await compareToPreviousVersion(newVersionId);
|
|
expect(compareResult?.target?.schemaVersion?.safeSchemaChanges?.nodes).toMatchInlineSnapshot(`
|
|
[
|
|
{
|
|
approval: null,
|
|
criticality: Dangerous,
|
|
criticalityReason: The registry service url has changed,
|
|
isSafeBasedOnUsage: false,
|
|
message: [foo] New service url: 'https://api.com/nah' (previously: 'https://api.com/products'),
|
|
path: null,
|
|
},
|
|
]
|
|
`);
|
|
expect(compareResult?.target?.schemaVersion?.breakingSchemaChanges?.nodes).toBeUndefined();
|
|
} finally {
|
|
await pool?.end();
|
|
}
|
|
},
|
|
);
|
|
|
|
test.concurrent(
|
|
'service url change from legacy to legacy version is displayed correctly',
|
|
async ({ expect }) => {
|
|
let pool: Awaited<ReturnType<typeof createPool>> | undefined;
|
|
try {
|
|
const { createOrg } = await initSeed().createOwner();
|
|
const { createProject } = await createOrg();
|
|
const { project, target, createTargetAccessToken, compareToPreviousVersion } =
|
|
await createProject(ProjectType.Federation);
|
|
|
|
// Create a token with write rights
|
|
const writeToken = await createTargetAccessToken({});
|
|
|
|
// We need to seed a legacy entry in the database
|
|
|
|
const conn = connectionString();
|
|
pool = await createPool(conn);
|
|
|
|
const sdl = 'type Query { ping: String! }';
|
|
|
|
await insertLegacyVersion(pool, {
|
|
projectId: project.id,
|
|
targetId: target.id,
|
|
sdl,
|
|
serviceUrl: 'https://api.com/products',
|
|
});
|
|
|
|
await insertLegacyVersion(pool, {
|
|
projectId: project.id,
|
|
targetId: target.id,
|
|
sdl,
|
|
serviceUrl: 'https://api.com/nah',
|
|
});
|
|
|
|
const newVersionId = (await writeToken.fetchLatestValidSchema())?.latestValidVersion?.id;
|
|
|
|
if (!newVersionId) {
|
|
expect(newVersionId).toBeInstanceOf(String);
|
|
return;
|
|
}
|
|
|
|
const compareResult = await compareToPreviousVersion(newVersionId);
|
|
|
|
expect(compareResult?.target?.schemaVersion?.safeSchemaChanges?.nodes).toMatchInlineSnapshot(`
|
|
[
|
|
{
|
|
approval: null,
|
|
criticality: Dangerous,
|
|
criticalityReason: The registry service url has changed,
|
|
isSafeBasedOnUsage: false,
|
|
message: [foo] New service url: 'https://api.com/products' (previously: 'https://api.com/nah'),
|
|
path: null,
|
|
},
|
|
]
|
|
`);
|
|
expect(compareResult?.target?.schemaVersion?.breakingSchemaChanges?.nodes).toBeUndefined();
|
|
} finally {
|
|
await pool?.end();
|
|
}
|
|
},
|
|
);
|
|
|
|
test.concurrent(
|
|
'publishing schema with a deprecated "github: false" should be successful',
|
|
async ({ expect }) => {
|
|
const { createOrg } = await initSeed().createOwner();
|
|
const { createProject } = await createOrg();
|
|
const { createTargetAccessToken } = await createProject(ProjectType.Single);
|
|
const readWriteToken = await createTargetAccessToken({});
|
|
|
|
const result = await readWriteToken
|
|
.publishSchema({
|
|
sdl: /* GraphQL */ `
|
|
type Query {
|
|
ping: String
|
|
}
|
|
`,
|
|
github: false,
|
|
})
|
|
.then(r => r.expectNoGraphQLErrors());
|
|
|
|
expect(result.schemaPublish.__typename).toBe('SchemaPublishSuccess');
|
|
},
|
|
);
|
|
|
|
test.concurrent(
|
|
'publishing Federation schema results in tags stored on the schema version',
|
|
async ({ expect }) => {
|
|
const { createOrg } = await initSeed().createOwner();
|
|
const { createProject, setFeatureFlag } = await createOrg();
|
|
const { createTargetAccessToken, setNativeFederation } = await createProject(
|
|
ProjectType.Federation,
|
|
);
|
|
|
|
const readWriteToken = await createTargetAccessToken({});
|
|
|
|
const result = await readWriteToken
|
|
.publishSchema({
|
|
sdl: /* GraphQL */ `
|
|
extend schema
|
|
@link(url: "https://specs.apollo.dev/link/v1.0")
|
|
@link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@tag"])
|
|
|
|
type Query {
|
|
ping: String @tag(name: "atarashii")
|
|
}
|
|
`,
|
|
service: 'foo',
|
|
url: 'http://lol.de',
|
|
})
|
|
.then(r => r.expectNoGraphQLErrors());
|
|
|
|
expect(result.schemaPublish.__typename).toBe('SchemaPublishSuccess');
|
|
const latestValidSchema = await readWriteToken.fetchLatestValidSchema();
|
|
expect(latestValidSchema.latestValidVersion?.tags).toEqual(['atarashii']);
|
|
},
|
|
);
|
|
|
|
test.concurrent('CDN services are published in alphanumeric order', async ({ expect }) => {
|
|
const { createOrg } = await initSeed().createOwner();
|
|
const { createProject } = await createOrg();
|
|
const { createTargetAccessToken, createCdnAccess } = await createProject(ProjectType.Stitching);
|
|
const readWriteToken = await createTargetAccessToken({});
|
|
|
|
await readWriteToken
|
|
.publishSchema({
|
|
sdl: /* GraphQL */ `
|
|
type Query {
|
|
ping1: String
|
|
}
|
|
`,
|
|
service: 'z',
|
|
url: 'http://z.foo',
|
|
})
|
|
.then(r => r.expectNoGraphQLErrors());
|
|
|
|
await readWriteToken
|
|
.publishSchema({
|
|
sdl: /* GraphQL */ `
|
|
type Query {
|
|
ping2: String
|
|
}
|
|
`,
|
|
service: 'x',
|
|
url: 'http://x.foo',
|
|
})
|
|
.then(r => r.expectNoGraphQLErrors());
|
|
|
|
await readWriteToken
|
|
.publishSchema({
|
|
sdl: /* GraphQL */ `
|
|
type Query {
|
|
ping3: String
|
|
}
|
|
`,
|
|
service: 'y',
|
|
url: 'http://y.foo',
|
|
})
|
|
.then(r => r.expectNoGraphQLErrors());
|
|
|
|
const cdn = await createCdnAccess();
|
|
const res = await fetch(cdn.cdnUrl + '/services', {
|
|
method: 'GET',
|
|
headers: {
|
|
'X-Hive-CDN-Key': cdn.secretAccessToken,
|
|
},
|
|
});
|
|
|
|
expect(res.status).toBe(200);
|
|
const result = await res.json();
|
|
expect(result).toMatchObject([{ name: 'x' }, { name: 'y' }, { name: 'z' }]);
|
|
});
|
|
|
|
test.concurrent(
|
|
'Composite schema project publish without service name results in error',
|
|
async ({ expect }) => {
|
|
const { createOrg } = await initSeed().createOwner();
|
|
const { createProject } = await createOrg();
|
|
const { createTargetAccessToken } = await createProject(ProjectType.Federation);
|
|
const readWriteToken = await createTargetAccessToken({});
|
|
|
|
const result = await readWriteToken
|
|
.publishSchema({
|
|
sdl: /* GraphQL */ `
|
|
type Query {
|
|
ping: String
|
|
}
|
|
`,
|
|
url: 'http://example.localhost',
|
|
})
|
|
.then(r => r.expectNoGraphQLErrors());
|
|
|
|
expect(result).toEqual({
|
|
schemaPublish: {
|
|
__typename: 'SchemaPublishMissingServiceError',
|
|
},
|
|
});
|
|
},
|
|
);
|
|
|
|
test.concurrent(
|
|
'Composite schema project publish without service url results in error',
|
|
async ({ expect }) => {
|
|
const { createOrg } = await initSeed().createOwner();
|
|
const { createProject } = await createOrg();
|
|
const { createTargetAccessToken } = await createProject(ProjectType.Federation);
|
|
const readWriteToken = await createTargetAccessToken({});
|
|
|
|
const result = await readWriteToken
|
|
.publishSchema({
|
|
sdl: /* GraphQL */ `
|
|
type Query {
|
|
ping: String
|
|
}
|
|
`,
|
|
service: 'example',
|
|
})
|
|
.then(r => r.expectNoGraphQLErrors());
|
|
|
|
expect(result).toEqual({
|
|
schemaPublish: {
|
|
__typename: 'SchemaPublishMissingUrlError',
|
|
},
|
|
});
|
|
},
|
|
);
|
|
|
|
describe.concurrent(
|
|
'schema publish should be ignored due to unchanged input schema and being compared to latest schema version',
|
|
() => {
|
|
test.concurrent('native federation', async () => {
|
|
const { createOrg } = await initSeed().createOwner();
|
|
const { createProject, setFeatureFlag } = await createOrg();
|
|
const { createTargetAccessToken, setNativeFederation } = await createProject(
|
|
ProjectType.Federation,
|
|
);
|
|
|
|
const token = await createTargetAccessToken({});
|
|
|
|
const validSdl = /* GraphQL */ `
|
|
extend schema
|
|
@link(url: "https://specs.apollo.dev/link/v1.0")
|
|
@link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@tag"])
|
|
|
|
type Query {
|
|
ping: String
|
|
pong: String
|
|
foo: User
|
|
}
|
|
|
|
type User {
|
|
id: ID!
|
|
}
|
|
`;
|
|
|
|
// here we use @tag without an argument to trigger a validation/composition error
|
|
const invalidSdl = /* GraphQL */ `
|
|
extend schema
|
|
@link(url: "https://specs.apollo.dev/link/v1.0")
|
|
@link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@tag"])
|
|
|
|
type Query {
|
|
ping: String
|
|
pong: String
|
|
foo: User @tag
|
|
}
|
|
|
|
type User {
|
|
id: ID!
|
|
}
|
|
`;
|
|
|
|
// Publish schema with write rights
|
|
const validPublish = await token
|
|
.publishSchema({
|
|
sdl: validSdl,
|
|
service: 'serviceA',
|
|
url: 'http://localhost:4000',
|
|
})
|
|
.then(r => r.expectNoGraphQLErrors());
|
|
|
|
expect(validPublish.schemaPublish).toMatchObject({
|
|
valid: true,
|
|
linkToWebsite: expect.any(String),
|
|
});
|
|
|
|
const invalidPublish = await token
|
|
.publishSchema({
|
|
sdl: invalidSdl,
|
|
service: 'serviceA',
|
|
url: 'http://localhost:4000',
|
|
})
|
|
.then(r => r.expectNoGraphQLErrors());
|
|
|
|
expect(invalidPublish.schemaPublish).toMatchObject({
|
|
valid: false,
|
|
linkToWebsite: expect.any(String),
|
|
});
|
|
|
|
const invalidSdlCheck = await token
|
|
.checkSchema(invalidSdl, 'serviceA')
|
|
.then(r => r.expectNoGraphQLErrors());
|
|
|
|
expect(invalidSdlCheck.schemaCheck).toMatchObject({
|
|
valid: false,
|
|
__typename: 'SchemaCheckError',
|
|
changes: expect.objectContaining({
|
|
total: 0,
|
|
}),
|
|
errors: expect.objectContaining({
|
|
total: 1,
|
|
}),
|
|
});
|
|
|
|
const validSdlCheck = await token
|
|
.checkSchema(validSdl, 'serviceA')
|
|
.then(r => r.expectNoGraphQLErrors());
|
|
|
|
expect(validSdlCheck.schemaCheck).toMatchObject({
|
|
valid: true,
|
|
__typename: 'SchemaCheckSuccess',
|
|
changes: expect.objectContaining({
|
|
total: 0,
|
|
}),
|
|
});
|
|
|
|
const result = await token
|
|
.publishSchema({
|
|
sdl: validSdl,
|
|
service: 'serviceA',
|
|
url: 'http://localhost:4000',
|
|
})
|
|
.then(r => r.expectNoGraphQLErrors());
|
|
|
|
expect(result.schemaPublish).toMatchObject({
|
|
valid: true,
|
|
linkToWebsite: expect.any(String),
|
|
});
|
|
|
|
if (
|
|
!('linkToWebsite' in result.schemaPublish) ||
|
|
!('linkToWebsite' in invalidPublish.schemaPublish) ||
|
|
!('linkToWebsite' in validPublish.schemaPublish)
|
|
) {
|
|
throw new Error('linkToWebsite not found');
|
|
}
|
|
|
|
// If the linkToWebsite is the same as one of the previous versions,
|
|
// the schema publish was ignored due to unchanged input schemas.
|
|
// It shouldn't be the case.
|
|
// That's what we're checking here.
|
|
|
|
expect(result.schemaPublish.linkToWebsite).not.toEqual(
|
|
invalidPublish.schemaPublish.linkToWebsite,
|
|
);
|
|
|
|
expect(result.schemaPublish.linkToWebsite).not.toEqual(
|
|
validPublish.schemaPublish.linkToWebsite,
|
|
);
|
|
|
|
const ignoredResult = await token
|
|
.publishSchema({
|
|
sdl: validSdl,
|
|
service: 'serviceA',
|
|
url: 'http://localhost:4000',
|
|
})
|
|
.then(r => r.expectNoGraphQLErrors());
|
|
|
|
// This time the schema publish should be ignored
|
|
// and link to the previous version
|
|
expect(ignoredResult.schemaPublish).toMatchObject({
|
|
valid: true,
|
|
linkToWebsite: result.schemaPublish.linkToWebsite,
|
|
});
|
|
});
|
|
|
|
test.concurrent('legacy fed composition', async () => {
|
|
const { createOrg } = await initSeed().createOwner();
|
|
const { createProject, setFeatureFlag } = await createOrg();
|
|
const { createTargetAccessToken, setNativeFederation } = await createProject(
|
|
ProjectType.Federation,
|
|
);
|
|
|
|
const token = await createTargetAccessToken({});
|
|
|
|
const validSdl = /* GraphQL */ `
|
|
type Query {
|
|
ping: String
|
|
pong: String
|
|
foo: User
|
|
}
|
|
|
|
type User @key(fields: "id") {
|
|
id: ID!
|
|
}
|
|
`;
|
|
|
|
// @key(fields:) is invalid - should trigger a composition error
|
|
const invalidSdl = /* GraphQL */ `
|
|
type Query {
|
|
ping: String
|
|
pong: String
|
|
foo: User
|
|
}
|
|
|
|
type User @key(fields: "uuid") {
|
|
id: ID!
|
|
}
|
|
`;
|
|
|
|
// Publish schema with write rights
|
|
const validPublish = await token
|
|
.publishSchema({
|
|
sdl: validSdl,
|
|
service: 'serviceA',
|
|
url: 'http://localhost:4000',
|
|
})
|
|
.then(r => r.expectNoGraphQLErrors());
|
|
|
|
expect(validPublish.schemaPublish).toMatchObject({
|
|
valid: true,
|
|
linkToWebsite: expect.any(String),
|
|
});
|
|
|
|
const invalidPublish = await token
|
|
.publishSchema({
|
|
sdl: invalidSdl,
|
|
service: 'serviceA',
|
|
url: 'http://localhost:4000',
|
|
})
|
|
.then(r => r.expectNoGraphQLErrors());
|
|
|
|
expect(invalidPublish.schemaPublish).toMatchObject({
|
|
valid: false,
|
|
linkToWebsite: expect.any(String),
|
|
});
|
|
|
|
const invalidSdlCheck = await token
|
|
.checkSchema(invalidSdl, 'serviceA')
|
|
.then(r => r.expectNoGraphQLErrors());
|
|
|
|
expect(invalidSdlCheck.schemaCheck).toMatchObject({
|
|
valid: false,
|
|
__typename: 'SchemaCheckError',
|
|
changes: expect.objectContaining({
|
|
total: 0,
|
|
}),
|
|
errors: expect.objectContaining({
|
|
total: 1,
|
|
}),
|
|
});
|
|
|
|
const validSdlCheck = await token
|
|
.checkSchema(validSdl, 'serviceA')
|
|
.then(r => r.expectNoGraphQLErrors());
|
|
|
|
expect(validSdlCheck.schemaCheck).toMatchObject({
|
|
valid: true,
|
|
__typename: 'SchemaCheckSuccess',
|
|
changes: expect.objectContaining({
|
|
total: 0,
|
|
}),
|
|
});
|
|
|
|
const result = await token
|
|
.publishSchema({
|
|
sdl: validSdl,
|
|
service: 'serviceA',
|
|
url: 'http://localhost:4000',
|
|
})
|
|
.then(r => r.expectNoGraphQLErrors());
|
|
|
|
expect(result.schemaPublish).toMatchObject({
|
|
valid: true,
|
|
linkToWebsite: expect.any(String),
|
|
});
|
|
|
|
if (
|
|
!('linkToWebsite' in result.schemaPublish) ||
|
|
!('linkToWebsite' in invalidPublish.schemaPublish) ||
|
|
!('linkToWebsite' in validPublish.schemaPublish)
|
|
) {
|
|
throw new Error('linkToWebsite not found');
|
|
}
|
|
|
|
// If the linkToWebsite is the same as one of the previous versions,
|
|
// the schema publish was ignored due to unchanged input schemas.
|
|
// It shouldn't be the case.
|
|
// That's what we're checking here.
|
|
|
|
expect(result.schemaPublish.linkToWebsite).not.toEqual(
|
|
invalidPublish.schemaPublish.linkToWebsite,
|
|
);
|
|
|
|
expect(result.schemaPublish.linkToWebsite).not.toEqual(
|
|
validPublish.schemaPublish.linkToWebsite,
|
|
);
|
|
|
|
const ignoredResult = await token
|
|
.publishSchema({
|
|
sdl: validSdl,
|
|
service: 'serviceA',
|
|
url: 'http://localhost:4000',
|
|
})
|
|
.then(r => r.expectNoGraphQLErrors());
|
|
|
|
// This time the schema publish should be ignored
|
|
// and link to the previous version
|
|
expect(ignoredResult.schemaPublish).toMatchObject({
|
|
valid: true,
|
|
linkToWebsite: result.schemaPublish.linkToWebsite,
|
|
});
|
|
});
|
|
},
|
|
);
|
|
|
|
test.concurrent(
|
|
'publishing schema with deprecated non-nullable input field fails due to validation errors',
|
|
async () => {
|
|
const { createOrg } = await initSeed().createOwner();
|
|
const { createProject } = await createOrg();
|
|
const { createTargetAccessToken } = await createProject(ProjectType.Single);
|
|
const token = await createTargetAccessToken({});
|
|
|
|
const sdl = /* GraphQL */ `
|
|
type Query {
|
|
a(b: B!): String
|
|
}
|
|
|
|
input B {
|
|
a: String! @deprecated(reason: "This field is deprecated")
|
|
b: String!
|
|
}
|
|
`;
|
|
|
|
const result = await token
|
|
.publishSchema({
|
|
sdl,
|
|
})
|
|
.then(r => r.expectNoGraphQLErrors());
|
|
|
|
expect(result.schemaPublish).toEqual({
|
|
__typename: 'SchemaPublishError',
|
|
changes: {
|
|
nodes: [],
|
|
total: 0,
|
|
},
|
|
errors: {
|
|
nodes: [
|
|
{
|
|
message: 'Required input field B.a cannot be deprecated.',
|
|
},
|
|
],
|
|
total: 1,
|
|
},
|
|
linkToWebsite: null,
|
|
valid: false,
|
|
});
|
|
},
|
|
);
|
|
|
|
test.concurrent(
|
|
'publishing a valid schema onto a broken schema succeeds (prior schema has deprecated non-nullable input)',
|
|
async () => {
|
|
const { createOrg } = await initSeed().createOwner();
|
|
const { createProject, organization } = await createOrg();
|
|
const { createTargetAccessToken, project, target } = await createProject(ProjectType.Single);
|
|
const token = await createTargetAccessToken({});
|
|
|
|
const brokenSdl = /* GraphQL */ `
|
|
type Query {
|
|
a(b: B!): String
|
|
}
|
|
|
|
input B {
|
|
a: String! @deprecated(reason: "This field is deprecated")
|
|
b: String!
|
|
}
|
|
`;
|
|
|
|
// we need to manually insert a broken schema version into the database
|
|
// as we fixed the issue that allows publishing such a broken version
|
|
|
|
const conn = connectionString();
|
|
const storage = await createStorage(conn, 2);
|
|
await storage.createVersion({
|
|
schema: brokenSdl,
|
|
author: 'Jochen',
|
|
async actionFn() {},
|
|
base_schema: null,
|
|
commit: '123',
|
|
changes: [],
|
|
compositeSchemaSDL: null,
|
|
conditionalBreakingChangeMetadata: null,
|
|
contracts: null,
|
|
coordinatesDiff: null,
|
|
diffSchemaVersionId: null,
|
|
github: null,
|
|
metadata: null,
|
|
logIds: [],
|
|
projectId: project.id,
|
|
service: null,
|
|
organizationId: organization.id,
|
|
previousSchemaVersion: null,
|
|
valid: true,
|
|
schemaCompositionErrors: [],
|
|
supergraphSDL: null,
|
|
tags: null,
|
|
targetId: target.id,
|
|
url: null,
|
|
schemaMetadata: null,
|
|
metadataAttributes: null,
|
|
});
|
|
await storage.destroy();
|
|
|
|
const validSdl = /* GraphQL */ `
|
|
type Query {
|
|
a(b: B!): String
|
|
}
|
|
|
|
input B {
|
|
a: String @deprecated(reason: "This field is deprecated")
|
|
b: String!
|
|
}
|
|
`;
|
|
|
|
const result = await token
|
|
.publishSchema({
|
|
sdl: validSdl,
|
|
})
|
|
.then(r => r.expectNoGraphQLErrors());
|
|
|
|
expect(result.schemaPublish).toEqual({
|
|
__typename: 'SchemaPublishSuccess',
|
|
changes: null,
|
|
initial: false,
|
|
linkToWebsite: expect.any(String),
|
|
message: '',
|
|
valid: true,
|
|
});
|
|
},
|
|
);
|