Skip to content

Commit

Permalink
feat: support serverless v4 (#637)
Browse files Browse the repository at this point in the history
  • Loading branch information
Mmarzex authored Aug 12, 2024
1 parent e99c51a commit 3b56bf2
Show file tree
Hide file tree
Showing 5 changed files with 74 additions and 65 deletions.
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 @@ -50,7 +50,7 @@ export class Schema {
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

0 comments on commit 3b56bf2

Please sign in to comment.