Skip to content

Commit 3b56bf2

Browse files
authored
feat: support serverless v4 (#637)
1 parent e99c51a commit 3b56bf2

File tree

5 files changed

+74
-65
lines changed

5 files changed

+74
-65
lines changed

src/__tests__/commands.test.ts

Lines changed: 0 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -4,33 +4,6 @@ import ServerlessError from 'serverless/lib/serverless-error';
44

55
jest.setTimeout(30000);
66

7-
jest.mock('@serverless/utils/log', () => {
8-
const dummyProgress = {
9-
update: jest.fn(),
10-
remove: jest.fn(),
11-
};
12-
const logger = {
13-
error: jest.fn(),
14-
warning: jest.fn(),
15-
notice: jest.fn(),
16-
info: jest.fn(),
17-
debug: jest.fn(),
18-
success: jest.fn(),
19-
};
20-
return {
21-
writeText: jest.fn(),
22-
progress: {
23-
get: () => dummyProgress,
24-
create: () => dummyProgress,
25-
},
26-
log: {
27-
get: () => logger,
28-
...logger,
29-
},
30-
getPluginWriters: jest.fn(),
31-
};
32-
});
33-
347
const confirmSpy = jest.spyOn(utils, 'confirmAction');
358
const describeStackResources = jest.fn().mockResolvedValue({
369
StackResources: [

src/__tests__/given.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,20 @@ export const plugin = () => {
2222
stage: 'dev',
2323
region: 'us-east-1',
2424
};
25-
return new ServerlessAppsyncPlugin(createServerless(), options);
25+
return new ServerlessAppsyncPlugin(createServerless(), options, {
26+
log: {
27+
error: jest.fn(),
28+
warning: jest.fn(),
29+
info: jest.fn(),
30+
success: jest.fn(),
31+
},
32+
progress: {
33+
create: () => ({
34+
remove: jest.fn(),
35+
}),
36+
},
37+
writeText: jest.fn(),
38+
});
2639
};
2740

2841
export const appSyncConfig = (partial?: Partial<AppSyncConfig>) => {

src/index.ts

Lines changed: 58 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { writeText, log, progress } from '@serverless/utils/log';
21
import Serverless from 'serverless/lib/Serverless';
32
import Provider from 'serverless/lib/plugins/aws/provider.js';
43
import { forEach, last, merge } from 'lodash';
@@ -68,6 +67,23 @@ import terminalLink from 'terminal-link';
6867

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

70+
type Progress = {
71+
remove: () => void;
72+
};
73+
74+
type ServerlessPluginUtils = {
75+
log: {
76+
success: (message: string) => void;
77+
warning: (message: string) => void;
78+
error: (message: string) => void;
79+
info: (message: string) => void;
80+
};
81+
progress: {
82+
create: (params: { name?: string; message: string }) => Progress;
83+
};
84+
writeText: (message: string) => void;
85+
};
86+
7187
class ServerlessAppsyncPlugin {
7288
private provider: Provider;
7389
private gatheredData: {
@@ -90,6 +106,7 @@ class ServerlessAppsyncPlugin {
90106
constructor(
91107
public serverless: Serverless,
92108
private options: Record<string, string>,
109+
public utils: ServerlessPluginUtils,
93110
) {
94111
this.gatheredData = {
95112
apis: [],
@@ -98,7 +115,7 @@ class ServerlessAppsyncPlugin {
98115
this.serverless = serverless;
99116
this.options = options;
100117
this.provider = this.serverless.getProvider('aws');
101-
118+
this.utils = utils;
102119
// We are using a newer version of AJV than Serverless Framework
103120
// and some customizations (eg: custom errors, $merge, filter irrelevant errors)
104121
// For SF, just validate the type of input to allow us to use a custom
@@ -304,7 +321,7 @@ class ServerlessAppsyncPlugin {
304321
'appsync:validate-schema:run': () => {
305322
this.loadConfig();
306323
this.validateSchemas();
307-
log.success('AppSync schema valid');
324+
this.utils.log.success('AppSync schema valid');
308325
},
309326
'appsync:get-introspection:run': () => this.getIntrospection(),
310327
'appsync:flush-cache:run': () => this.flushCache(),
@@ -327,7 +344,7 @@ class ServerlessAppsyncPlugin {
327344
this.initDomainCommand(),
328345
'appsync:domain:delete-record:run': async () => this.deleteRecord(),
329346
finalize: () => {
330-
writeText(
347+
this.utils.writeText(
331348
'\nLooking for a better AppSync development experience? Have you tried GraphBolt? https://graphbolt.dev',
332349
);
333350
},
@@ -431,20 +448,22 @@ class ServerlessAppsyncPlugin {
431448
try {
432449
const filePath = path.resolve(this.options.output);
433450
fs.writeFileSync(filePath, schema.toString());
434-
log.success(`Introspection schema exported to ${filePath}`);
451+
this.utils.log.success(`Introspection schema exported to ${filePath}`);
435452
} catch (error) {
436-
log.error(`Could not save to file: ${(error as Error).message}`);
453+
this.utils.log.error(
454+
`Could not save to file: ${(error as Error).message}`,
455+
);
437456
}
438457
return;
439458
}
440459

441-
writeText(schema.toString());
460+
this.utils.writeText(schema.toString());
442461
}
443462

444463
async flushCache() {
445464
const apiId = await this.getApiId();
446465
await this.provider.request('AppSync', 'flushApiCache', { apiId });
447-
log.success('Cache flushed successfully');
466+
this.utils.log.success('Cache flushed successfully');
448467
}
449468

450469
async openConsole() {
@@ -486,7 +505,7 @@ class ServerlessAppsyncPlugin {
486505

487506
events?.forEach((event) => {
488507
const { timestamp, message } = event;
489-
writeText(
508+
this.utils.writeText(
490509
`${chalk.gray(
491510
DateTime.fromMillis(timestamp || 0).toISO(),
492511
)}\t${message}`,
@@ -512,7 +531,7 @@ class ServerlessAppsyncPlugin {
512531
const domain = this.getDomain();
513532

514533
if (domain.useCloudFormation !== false) {
515-
log.warning(
534+
this.utils.log.warning(
516535
'You are using the CloudFormation integration for domain configuration.\n' +
517536
'To avoid CloudFormation drifts, you should not use it in combination with this command.\n' +
518537
'Set the `domain.useCloudFormation` attribute to false to use the CLI integration.\n' +
@@ -568,7 +587,7 @@ class ServerlessAppsyncPlugin {
568587
({ DomainName }) => DomainName === match,
569588
);
570589
if (cert) {
571-
log.info(
590+
this.utils.log.info(
572591
`Found matching certificate for ${match}: ${cert.CertificateArn}`,
573592
);
574593
return cert.CertificateArn;
@@ -595,13 +614,13 @@ class ServerlessAppsyncPlugin {
595614
domainName: domain.name,
596615
certificateArn,
597616
});
598-
log.success(`Domain '${domain.name}' created successfully`);
617+
this.utils.log.success(`Domain '${domain.name}' created successfully`);
599618
} catch (error) {
600619
if (
601620
error instanceof this.serverless.classes.Error &&
602621
this.options.quiet
603622
) {
604-
log.error(error.message);
623+
this.utils.log.error(error.message);
605624
} else {
606625
throw error;
607626
}
@@ -611,7 +630,7 @@ class ServerlessAppsyncPlugin {
611630
async deleteDomain() {
612631
try {
613632
const domain = this.getDomain();
614-
log.warning(`The domain '${domain.name} will be deleted.`);
633+
this.utils.log.warning(`The domain '${domain.name} will be deleted.`);
615634
if (!this.options.yes && !(await confirmAction())) {
616635
return;
617636
}
@@ -621,13 +640,13 @@ class ServerlessAppsyncPlugin {
621640
>('AppSync', 'deleteDomainName', {
622641
domainName: domain.name,
623642
});
624-
log.success(`Domain '${domain.name}' deleted successfully`);
643+
this.utils.log.success(`Domain '${domain.name}' deleted successfully`);
625644
} catch (error) {
626645
if (
627646
error instanceof this.serverless.classes.Error &&
628647
this.options.quiet
629648
) {
630-
log.error(error.message);
649+
this.utils.log.error(error.message);
631650
} else {
632651
throw error;
633652
}
@@ -663,8 +682,7 @@ class ServerlessAppsyncPlugin {
663682
message: string;
664683
desiredStatus: 'SUCCESS' | 'NOT_FOUND';
665684
}) {
666-
const progressInstance = progress.create({ message });
667-
685+
const progressInstance = this.utils.progress.create({ message });
668686
let status: string;
669687
do {
670688
status =
@@ -683,14 +701,14 @@ class ServerlessAppsyncPlugin {
683701
const assoc = await this.getApiAssocStatus(domain.name);
684702

685703
if (assoc?.associationStatus !== 'NOT_FOUND' && assoc?.apiId !== apiId) {
686-
log.warning(
704+
this.utils.log.warning(
687705
`The domain ${domain.name} is currently associated to another API (${assoc?.apiId})`,
688706
);
689707
if (!this.options.yes && !(await confirmAction())) {
690708
return;
691709
}
692710
} else if (assoc?.apiId === apiId) {
693-
log.success('The domain is already associated to this API');
711+
this.utils.log.success('The domain is already associated to this API');
694712
return;
695713
}
696714

@@ -709,7 +727,9 @@ class ServerlessAppsyncPlugin {
709727
message,
710728
desiredStatus: 'SUCCESS',
711729
});
712-
log.success(`API successfully associated to domain '${domain.name}'`);
730+
this.utils.log.success(
731+
`API successfully associated to domain '${domain.name}'`,
732+
);
713733
}
714734

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

720740
if (assoc?.associationStatus === 'NOT_FOUND') {
721-
log.warning(
741+
this.utils.log.warning(
722742
`The domain ${domain.name} is currently not associated to any API`,
723743
);
724744
return;
@@ -730,7 +750,7 @@ class ServerlessAppsyncPlugin {
730750
`Try running this command from that API's stack or stage, or use the --force / -f flag`,
731751
);
732752
}
733-
log.warning(
753+
this.utils.log.warning(
734754
`The domain ${domain.name} will be disassociated from API '${apiId}'`,
735755
);
736756

@@ -752,7 +772,9 @@ class ServerlessAppsyncPlugin {
752772
desiredStatus: 'NOT_FOUND',
753773
});
754774

755-
log.success(`API successfully disassociated from domain '${domain.name}'`);
775+
this.utils.log.success(
776+
`API successfully disassociated from domain '${domain.name}'`,
777+
);
756778
}
757779

758780
async getHostedZoneId() {
@@ -798,7 +820,7 @@ class ServerlessAppsyncPlugin {
798820
}
799821

800822
async createRecord() {
801-
const progressInstance = progress.create({
823+
const progressInstance = this.utils.progress.create({
802824
message: 'Creating route53 record',
803825
});
804826

@@ -813,10 +835,10 @@ class ServerlessAppsyncPlugin {
813835
if (changeId) {
814836
await this.checkRoute53RecordStatus(changeId);
815837
progressInstance.remove();
816-
log.info(
838+
this.utils.log.info(
817839
`Alias record for '${domain.name}' was created in Hosted Zone '${hostedZoneId}'`,
818840
);
819-
log.success('Route53 record created successfuly');
841+
this.utils.log.success('Route53 record created successfuly');
820842
}
821843
}
822844

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

828-
log.warning(
850+
this.utils.log.warning(
829851
`Alias record for '${domain.name}' will be deleted from Hosted Zone '${hostedZoneId}'`,
830852
);
831853
if (!this.options.yes && !(await confirmAction())) {
832854
return;
833855
}
834856

835-
const progressInstance = progress.create({
857+
const progressInstance = this.utils.progress.create({
836858
message: 'Deleting route53 record',
837859
});
838860

@@ -844,10 +866,10 @@ class ServerlessAppsyncPlugin {
844866
if (changeId) {
845867
await this.checkRoute53RecordStatus(changeId);
846868
progressInstance.remove();
847-
log.info(
869+
this.utils.log.info(
848870
`Alias record for '${domain.name}' was deleted from Hosted Zone '${hostedZoneId}'`,
849871
);
850-
log.success('Route53 record deleted successfuly');
872+
this.utils.log.success('Route53 record deleted successfuly');
851873
}
852874
}
853875

@@ -905,7 +927,7 @@ class ServerlessAppsyncPlugin {
905927
error instanceof this.serverless.classes.Error &&
906928
this.options.quiet
907929
) {
908-
log.error(error.message);
930+
this.utils.log.error(error.message);
909931
} else {
910932
throw error;
911933
}
@@ -949,7 +971,7 @@ class ServerlessAppsyncPlugin {
949971
}
950972

951973
loadConfig() {
952-
log.info('Loading AppSync config');
974+
this.utils.log.info('Loading AppSync config');
953975

954976
const { appSync } = this.serverless.configurationInput;
955977

@@ -969,15 +991,15 @@ class ServerlessAppsyncPlugin {
969991

970992
validateSchemas() {
971993
try {
972-
log.info('Validating AppSync schema');
994+
this.utils.log.info('Validating AppSync schema');
973995
if (!this.api) {
974996
throw new this.serverless.classes.Error(
975997
'Could not load the API. This should not happen.',
976998
);
977999
}
9781000
this.api.compileSchema();
9791001
} catch (error) {
980-
log.info('Error');
1002+
this.utils.log.info('Error');
9811003
if (error instanceof GraphQLError) {
9821004
this.handleError(error.message);
9831005
}
@@ -1057,7 +1079,7 @@ class ServerlessAppsyncPlugin {
10571079
if (configValidationMode === 'error') {
10581080
throw new this.serverless.classes.Error(message);
10591081
} else if (configValidationMode === 'warn') {
1060-
log.warning(message);
1082+
this.utils.log.warning(message);
10611083
}
10621084
}
10631085
}

src/resources/Schema.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ export class Schema {
5050
valdiateSchema(schema: string) {
5151
const errors = validateSDL(parse(schema));
5252
if (errors.length > 0) {
53-
throw new ServerlessError(
53+
throw new this.api.plugin.serverless.classes.Error(
5454
'Invalid GraphQL schema:\n' +
5555
errors.map((error) => ` ${error.message}`).join('\n'),
5656
);

src/types/common.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ export type WafRuleCustom = {
5151
action?: WafRuleAction;
5252
statement: CfnWafRuleStatement;
5353
visibilityConfig?: VisibilityConfig;
54+
overrideAction?: Record<string, unknown>;
5455
};
5556

5657
export type WafRuleDisableIntrospection = {

0 commit comments

Comments
 (0)