Skip to content
Draft
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
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[]
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"AWSTemplateFormatVersion": "2010-09-09",
"Description": "{\"stackType\":\"auth-Cognito\"}",
"Resources": {
"UserPool": {
"Type": "AWS::Cognito::UserPool",
"Properties": {}
}
},
"Outputs": {},
"Parameters": {
"hostedUIProviderMeta": {
"Type": "String"
},
"hostedUIProviderCreds": {
"Type": "String"
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,18 @@ jest.mock('../../../../../../commands/gen2-migration/generate/_infra/ts', () =>

const mockMkdir = jest.fn().mockResolvedValue(undefined);
const mockWriteFile = jest.fn().mockResolvedValue(undefined);
const mockCopyFile = jest.fn().mockResolvedValue(undefined);
jest.mock('node:fs/promises', () => ({
mkdir: (...args: unknown[]) => mockMkdir(...args),
writeFile: (...args: unknown[]) => mockWriteFile(...args),
copyFile: (...args: unknown[]) => mockCopyFile(...args),
}));

const mockExistsSync = jest.fn().mockReturnValue(false);
const mockReaddirSync = jest.fn().mockReturnValue([]);
jest.mock('node:fs', () => ({
existsSync: (...args: unknown[]) => mockExistsSync(...args),
readdirSync: (...args: unknown[]) => mockReaddirSync(...args),
}));

function createMockGen1App(overrides?: Record<string, unknown>): Gen1App {
Expand Down Expand Up @@ -324,5 +333,125 @@ describe('DataGenerator', () => {

expect(addStatementSpy).not.toHaveBeenCalled();
});

describe('resolver overrides', () => {
it('does not add resolver operations when no resolvers directory exists', async () => {
mockExistsSync.mockReturnValue(false);

const generator = new DataGenerator(gen1App, backendGenerator, outputDir, {
category: 'api',
resourceName: 'testApi',
service: 'AppSync',
key: 'api:AppSync',
});
const ops = await generator.plan();

expect(ops).toHaveLength(1);
const addStatementSpy = jest.spyOn(backendGenerator, 'addStatement');
await ops[0].execute();
expect(addStatementSpy).not.toHaveBeenCalled();
});

it('does not add resolver operations when resolvers directory has no vtl files', async () => {
mockExistsSync.mockReturnValue(true);
mockReaddirSync.mockReturnValue(['readme.txt', 'notes.md']);

const generator = new DataGenerator(gen1App, backendGenerator, outputDir, {
category: 'api',
resourceName: 'testApi',
service: 'AppSync',
key: 'api:AppSync',
});
const ops = await generator.plan();

expect(ops).toHaveLength(1);
});

it('adds a copy operation when vtl files exist', async () => {
mockExistsSync.mockReturnValue(true);
mockReaddirSync.mockReturnValue(['Query.listProducts.res.vtl']);

const generator = new DataGenerator(gen1App, backendGenerator, outputDir, {
category: 'api',
resourceName: 'testApi',
service: 'AppSync',
key: 'api:AppSync',
});
const ops = await generator.plan();

expect(ops).toHaveLength(2);
const descriptions = await ops[1].describe();
expect(descriptions[0]).toContain('1 VTL resolver file(s)');
});

it('copies vtl files to amplify/data/resolvers/', async () => {
mockExistsSync.mockReturnValue(true);
mockReaddirSync.mockReturnValue(['Query.listProducts.res.vtl', 'Mutation.createProduct.req.vtl']);

const generator = new DataGenerator(gen1App, backendGenerator, outputDir, {
category: 'api',
resourceName: 'testApi',
service: 'AppSync',
key: 'api:AppSync',
});
const ops = await generator.plan();
await ops[1].execute();

expect(mockMkdir).toHaveBeenCalledWith(expect.stringContaining('resolvers'), { recursive: true });
expect(mockCopyFile).toHaveBeenCalledTimes(2);
expect(mockCopyFile).toHaveBeenCalledWith(
expect.stringContaining('Query.listProducts.res.vtl'),
expect.stringContaining('Query.listProducts.res.vtl'),
);
expect(mockCopyFile).toHaveBeenCalledWith(
expect.stringContaining('Mutation.createProduct.req.vtl'),
expect.stringContaining('Mutation.createProduct.req.vtl'),
);
});

it('contributes resolver override imports and statements to backendGenerator', async () => {
mockExistsSync.mockReturnValue(true);
mockReaddirSync.mockReturnValue(['Query.listProducts.res.vtl']);

const addImportSpy = jest.spyOn(backendGenerator, 'addImport');
const addNamespaceImportSpy = jest.spyOn(backendGenerator, 'addNamespaceImport');
const addStatementSpy = jest.spyOn(backendGenerator, 'addStatement');

const generator = new DataGenerator(gen1App, backendGenerator, outputDir, {
category: 'api',
resourceName: 'testApi',
service: 'AppSync',
key: 'api:AppSync',
});
const ops = await generator.plan();
await ops[0].execute();

// Resolver imports: fs (readdirSync only), path, url
expect(addImportSpy).toHaveBeenCalledWith('fs', ['readdirSync']);
expect(addImportSpy).toHaveBeenCalledWith('path', ['join', 'dirname']);
expect(addImportSpy).toHaveBeenCalledWith('url', ['fileURLToPath']);
expect(addNamespaceImportSpy).toHaveBeenCalledWith('aws-cdk-lib/aws-s3-assets', 'assets');

// 4 statements: __dirname, resolversDir, resolverFiles, for-of loop
expect(addStatementSpy).toHaveBeenCalledTimes(4);
});

it('handles multiple vtl files of both req and res types', async () => {
mockExistsSync.mockReturnValue(true);
mockReaddirSync.mockReturnValue(['Query.listProducts.res.vtl', 'Query.listProducts.req.vtl', 'Mutation.createProduct.res.vtl']);

const generator = new DataGenerator(gen1App, backendGenerator, outputDir, {
category: 'api',
resourceName: 'testApi',
service: 'AppSync',
key: 'api:AppSync',
});
const ops = await generator.plan();

expect(ops).toHaveLength(2);
const descriptions = await ops[1].describe();
expect(descriptions[0]).toContain('3 VTL resolver file(s)');
});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const factory = ts.factory;
*/
export class BackendGenerator implements Planner {
private readonly imports: Array<{ readonly source: string; readonly identifiers: string[] }> = [];
private readonly namespaceImports: Array<{ readonly source: string; readonly alias: string }> = [];
private readonly defineBackendProperties: ts.ObjectLiteralElementLike[] = [];
private readonly postDefineStatements: ts.Statement[] = [];
private readonly earlyStatements: ts.Statement[] = [];
Expand Down Expand Up @@ -46,6 +47,15 @@ export class BackendGenerator implements Planner {
}
}

/**
* Adds a namespace import (`import * as alias from 'source'`) to backend.ts.
*/
public addNamespaceImport(source: string, alias: string): void {
if (!this.namespaceImports.some((i) => i.source === source)) {
this.namespaceImports.push({ source, alias });
}
}

/**
* Adds a property to the `defineBackend({ ... })` call.
*/
Expand Down Expand Up @@ -122,6 +132,12 @@ export class BackendGenerator implements Planner {
nodes.push(createImportDeclaration(imp.source, imp.identifiers));
}

// Namespace imports (import * as X from 'source'), sorted by importOrder
const sortedNamespaceImports = [...this.namespaceImports].sort((a, b) => importOrder(a.source) - importOrder(b.source));
for (const ns of sortedNamespaceImports) {
nodes.push(createNamespaceImportDeclaration(ns.source, ns.alias));
}

// Sort defineBackend properties: auth first, then data, storage, then functions
const sortedProperties = [...this.defineBackendProperties].sort((a, b) => {
const getName = (prop: ts.ObjectLiteralElementLike): string => {
Expand Down Expand Up @@ -186,6 +202,14 @@ function createImportDeclaration(source: string, identifiers: string[]): ts.Impo
);
}

function createNamespaceImportDeclaration(source: string, alias: string): ts.ImportDeclaration {
return factory.createImportDeclaration(
undefined,
factory.createImportClause(false, undefined, factory.createNamespaceImport(factory.createIdentifier(alias))),
factory.createStringLiteral(source),
);
}

/**
* Returns a numeric sort key for import source paths.
*
Expand Down
Loading
Loading