Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support serverless v4 #637

Merged
merged 1 commit into from
Aug 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 0 additions & 27 deletions src/__tests__/commands.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,33 +4,6 @@ import ServerlessError from 'serverless/lib/serverless-error';

jest.setTimeout(30000);

jest.mock('@serverless/utils/log', () => {
const dummyProgress = {
update: jest.fn(),
remove: jest.fn(),
};
const logger = {
error: jest.fn(),
warning: jest.fn(),
notice: jest.fn(),
info: jest.fn(),
debug: jest.fn(),
success: jest.fn(),
};
return {
writeText: jest.fn(),
progress: {
get: () => dummyProgress,
create: () => dummyProgress,
},
log: {
get: () => logger,
...logger,
},
getPluginWriters: jest.fn(),
};
});

const confirmSpy = jest.spyOn(utils, 'confirmAction');
const describeStackResources = jest.fn().mockResolvedValue({
StackResources: [
Expand Down
15 changes: 14 additions & 1 deletion src/__tests__/given.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,20 @@ export const plugin = () => {
stage: 'dev',
region: 'us-east-1',
};
return new ServerlessAppsyncPlugin(createServerless(), options);
return new ServerlessAppsyncPlugin(createServerless(), options, {
log: {
error: jest.fn(),
warning: jest.fn(),
info: jest.fn(),
success: jest.fn(),
},
progress: {
create: () => ({
remove: jest.fn(),
}),
},
writeText: jest.fn(),
});
};

export const appSyncConfig = (partial?: Partial<AppSyncConfig>) => {
Expand Down
94 changes: 58 additions & 36 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { writeText, log, progress } from '@serverless/utils/log';
import Serverless from 'serverless/lib/Serverless';
import Provider from 'serverless/lib/plugins/aws/provider.js';
import { forEach, last, merge } from 'lodash';
Expand Down Expand Up @@ -68,6 +67,23 @@ import terminalLink from 'terminal-link';

const CONSOLE_BASE_URL = 'https://console.aws.amazon.com';

type Progress = {
remove: () => void;
};

type ServerlessPluginUtils = {
log: {
success: (message: string) => void;
warning: (message: string) => void;
error: (message: string) => void;
info: (message: string) => void;
};
progress: {
create: (params: { name?: string; message: string }) => Progress;
};
writeText: (message: string) => void;
};

class ServerlessAppsyncPlugin {
private provider: Provider;
private gatheredData: {
Expand All @@ -90,6 +106,7 @@ class ServerlessAppsyncPlugin {
constructor(
public serverless: Serverless,
private options: Record<string, string>,
public utils: ServerlessPluginUtils,
) {
this.gatheredData = {
apis: [],
Expand All @@ -98,7 +115,7 @@ class ServerlessAppsyncPlugin {
this.serverless = serverless;
this.options = options;
this.provider = this.serverless.getProvider('aws');

this.utils = utils;
// We are using a newer version of AJV than Serverless Framework
// and some customizations (eg: custom errors, $merge, filter irrelevant errors)
// For SF, just validate the type of input to allow us to use a custom
Expand Down Expand Up @@ -304,7 +321,7 @@ class ServerlessAppsyncPlugin {
'appsync:validate-schema:run': () => {
this.loadConfig();
this.validateSchemas();
log.success('AppSync schema valid');
this.utils.log.success('AppSync schema valid');
},
'appsync:get-introspection:run': () => this.getIntrospection(),
'appsync:flush-cache:run': () => this.flushCache(),
Expand All @@ -327,7 +344,7 @@ class ServerlessAppsyncPlugin {
this.initDomainCommand(),
'appsync:domain:delete-record:run': async () => this.deleteRecord(),
finalize: () => {
writeText(
this.utils.writeText(
'\nLooking for a better AppSync development experience? Have you tried GraphBolt? https://graphbolt.dev',
);
},
Expand Down Expand Up @@ -431,20 +448,22 @@ class ServerlessAppsyncPlugin {
try {
const filePath = path.resolve(this.options.output);
fs.writeFileSync(filePath, schema.toString());
log.success(`Introspection schema exported to ${filePath}`);
this.utils.log.success(`Introspection schema exported to ${filePath}`);
} catch (error) {
log.error(`Could not save to file: ${(error as Error).message}`);
this.utils.log.error(
`Could not save to file: ${(error as Error).message}`,
);
}
return;
}

writeText(schema.toString());
this.utils.writeText(schema.toString());
}

async flushCache() {
const apiId = await this.getApiId();
await this.provider.request('AppSync', 'flushApiCache', { apiId });
log.success('Cache flushed successfully');
this.utils.log.success('Cache flushed successfully');
}

async openConsole() {
Expand Down Expand Up @@ -486,7 +505,7 @@ class ServerlessAppsyncPlugin {

events?.forEach((event) => {
const { timestamp, message } = event;
writeText(
this.utils.writeText(
`${chalk.gray(
DateTime.fromMillis(timestamp || 0).toISO(),
)}\t${message}`,
Expand All @@ -512,7 +531,7 @@ class ServerlessAppsyncPlugin {
const domain = this.getDomain();

if (domain.useCloudFormation !== false) {
log.warning(
this.utils.log.warning(
'You are using the CloudFormation integration for domain configuration.\n' +
'To avoid CloudFormation drifts, you should not use it in combination with this command.\n' +
'Set the `domain.useCloudFormation` attribute to false to use the CLI integration.\n' +
Expand Down Expand Up @@ -568,7 +587,7 @@ class ServerlessAppsyncPlugin {
({ DomainName }) => DomainName === match,
);
if (cert) {
log.info(
this.utils.log.info(
`Found matching certificate for ${match}: ${cert.CertificateArn}`,
);
return cert.CertificateArn;
Expand All @@ -595,13 +614,13 @@ class ServerlessAppsyncPlugin {
domainName: domain.name,
certificateArn,
});
log.success(`Domain '${domain.name}' created successfully`);
this.utils.log.success(`Domain '${domain.name}' created successfully`);
} catch (error) {
if (
error instanceof this.serverless.classes.Error &&
this.options.quiet
) {
log.error(error.message);
this.utils.log.error(error.message);
} else {
throw error;
}
Expand All @@ -611,7 +630,7 @@ class ServerlessAppsyncPlugin {
async deleteDomain() {
try {
const domain = this.getDomain();
log.warning(`The domain '${domain.name} will be deleted.`);
this.utils.log.warning(`The domain '${domain.name} will be deleted.`);
if (!this.options.yes && !(await confirmAction())) {
return;
}
Expand All @@ -621,13 +640,13 @@ class ServerlessAppsyncPlugin {
>('AppSync', 'deleteDomainName', {
domainName: domain.name,
});
log.success(`Domain '${domain.name}' deleted successfully`);
this.utils.log.success(`Domain '${domain.name}' deleted successfully`);
} catch (error) {
if (
error instanceof this.serverless.classes.Error &&
this.options.quiet
) {
log.error(error.message);
this.utils.log.error(error.message);
} else {
throw error;
}
Expand Down Expand Up @@ -663,8 +682,7 @@ class ServerlessAppsyncPlugin {
message: string;
desiredStatus: 'SUCCESS' | 'NOT_FOUND';
}) {
const progressInstance = progress.create({ message });

const progressInstance = this.utils.progress.create({ message });
let status: string;
do {
status =
Expand All @@ -683,14 +701,14 @@ class ServerlessAppsyncPlugin {
const assoc = await this.getApiAssocStatus(domain.name);

if (assoc?.associationStatus !== 'NOT_FOUND' && assoc?.apiId !== apiId) {
log.warning(
this.utils.log.warning(
`The domain ${domain.name} is currently associated to another API (${assoc?.apiId})`,
);
if (!this.options.yes && !(await confirmAction())) {
return;
}
} else if (assoc?.apiId === apiId) {
log.success('The domain is already associated to this API');
this.utils.log.success('The domain is already associated to this API');
return;
}

Expand All @@ -709,7 +727,9 @@ class ServerlessAppsyncPlugin {
message,
desiredStatus: 'SUCCESS',
});
log.success(`API successfully associated to domain '${domain.name}'`);
this.utils.log.success(
`API successfully associated to domain '${domain.name}'`,
);
}

async disassocDomain() {
Expand All @@ -718,7 +738,7 @@ class ServerlessAppsyncPlugin {
const assoc = await this.getApiAssocStatus(domain.name);

if (assoc?.associationStatus === 'NOT_FOUND') {
log.warning(
this.utils.log.warning(
`The domain ${domain.name} is currently not associated to any API`,
);
return;
Expand All @@ -730,7 +750,7 @@ class ServerlessAppsyncPlugin {
`Try running this command from that API's stack or stage, or use the --force / -f flag`,
);
}
log.warning(
this.utils.log.warning(
`The domain ${domain.name} will be disassociated from API '${apiId}'`,
);

Expand All @@ -752,7 +772,9 @@ class ServerlessAppsyncPlugin {
desiredStatus: 'NOT_FOUND',
});

log.success(`API successfully disassociated from domain '${domain.name}'`);
this.utils.log.success(
`API successfully disassociated from domain '${domain.name}'`,
);
}

async getHostedZoneId() {
Expand Down Expand Up @@ -798,7 +820,7 @@ class ServerlessAppsyncPlugin {
}

async createRecord() {
const progressInstance = progress.create({
const progressInstance = this.utils.progress.create({
message: 'Creating route53 record',
});

Expand All @@ -813,10 +835,10 @@ class ServerlessAppsyncPlugin {
if (changeId) {
await this.checkRoute53RecordStatus(changeId);
progressInstance.remove();
log.info(
this.utils.log.info(
`Alias record for '${domain.name}' was created in Hosted Zone '${hostedZoneId}'`,
);
log.success('Route53 record created successfuly');
this.utils.log.success('Route53 record created successfuly');
}
}

Expand All @@ -825,14 +847,14 @@ class ServerlessAppsyncPlugin {
const appsyncDomainName = await this.getAppSyncDomainName();
const hostedZoneId = await this.getHostedZoneId();

log.warning(
this.utils.log.warning(
`Alias record for '${domain.name}' will be deleted from Hosted Zone '${hostedZoneId}'`,
);
if (!this.options.yes && !(await confirmAction())) {
return;
}

const progressInstance = progress.create({
const progressInstance = this.utils.progress.create({
message: 'Deleting route53 record',
});

Expand All @@ -844,10 +866,10 @@ class ServerlessAppsyncPlugin {
if (changeId) {
await this.checkRoute53RecordStatus(changeId);
progressInstance.remove();
log.info(
this.utils.log.info(
`Alias record for '${domain.name}' was deleted from Hosted Zone '${hostedZoneId}'`,
);
log.success('Route53 record deleted successfuly');
this.utils.log.success('Route53 record deleted successfuly');
}
}

Expand Down Expand Up @@ -905,7 +927,7 @@ class ServerlessAppsyncPlugin {
error instanceof this.serverless.classes.Error &&
this.options.quiet
) {
log.error(error.message);
this.utils.log.error(error.message);
} else {
throw error;
}
Expand Down Expand Up @@ -949,7 +971,7 @@ class ServerlessAppsyncPlugin {
}

loadConfig() {
log.info('Loading AppSync config');
this.utils.log.info('Loading AppSync config');

const { appSync } = this.serverless.configurationInput;

Expand All @@ -969,15 +991,15 @@ class ServerlessAppsyncPlugin {

validateSchemas() {
try {
log.info('Validating AppSync schema');
this.utils.log.info('Validating AppSync schema');
if (!this.api) {
throw new this.serverless.classes.Error(
'Could not load the API. This should not happen.',
);
}
this.api.compileSchema();
} catch (error) {
log.info('Error');
this.utils.log.info('Error');
if (error instanceof GraphQLError) {
this.handleError(error.message);
}
Expand Down Expand Up @@ -1057,7 +1079,7 @@ class ServerlessAppsyncPlugin {
if (configValidationMode === 'error') {
throw new this.serverless.classes.Error(message);
} else if (configValidationMode === 'warn') {
log.warning(message);
this.utils.log.warning(message);
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/resources/Schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import { Api } from './Api';
import { flatten } from 'lodash';
import { parse, print } from 'graphql';
import ServerlessError from 'serverless/lib/serverless-error';

Check warning on line 8 in src/resources/Schema.ts

View workflow job for this annotation

GitHub Actions / tests (16)

'ServerlessError' is defined but never used

Check warning on line 8 in src/resources/Schema.ts

View workflow job for this annotation

GitHub Actions / tests (18)

'ServerlessError' is defined but never used
import { validateSDL } from 'graphql/validation/validate';
import { mergeTypeDefs } from '@graphql-tools/merge';

Expand Down Expand Up @@ -50,7 +50,7 @@
valdiateSchema(schema: string) {
const errors = validateSDL(parse(schema));
if (errors.length > 0) {
throw new ServerlessError(
throw new this.api.plugin.serverless.classes.Error(
'Invalid GraphQL schema:\n' +
errors.map((error) => ` ${error.message}`).join('\n'),
);
Expand Down
1 change: 1 addition & 0 deletions src/types/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export type WafRuleCustom = {
action?: WafRuleAction;
statement: CfnWafRuleStatement;
visibilityConfig?: VisibilityConfig;
overrideAction?: Record<string, unknown>;
};

export type WafRuleDisableIntrospection = {
Expand Down
Loading