Skip to content

Commit d9ce4ea

Browse files
alharris-atDane Pilcher
andauthored
feat: merge noinit-modelgen into main to support executing amplify codegen models without an initialized amplify app (#698)
* feat: use default directive definitions if outside amplify project (#686) * chore: refactor command/models.js into independent functions, add jsdoc type annotations (#693) * feat: support modelgen without an amplify backend (#694) * feat: support modelgen without an amplify backend * chore: add unit tests for new utils * chore: add tests to confirm that tests for detached projects return the right files * chore: add additional unit tests for uninitialized backend states * feat: add e2e tests for modelgen in an uninitialized project (#696) * feat: add e2e tests for modelgen in an uninitialized project * fix: expecting to reject failed in CI, trying to just use a try-catch instead --------- Co-authored-by: Dane Pilcher <[email protected]>
1 parent 74eefdd commit d9ce4ea

20 files changed

+962
-215
lines changed

.codebuild/e2e_workflow.yml

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,15 +120,26 @@ batch:
120120
CLI_REGION: ap-northeast-1
121121
depend-on:
122122
- publish_to_local_registry
123-
- identifier: build_app_ts
123+
- identifier: >-
124+
build_app_ts_uninitialized_project_modelgen_android_uninitialized_project_modelgen_flutter_uninitialized_project_modelgen_ios
124125
buildspec: .codebuild/run_e2e_tests.yml
125126
env:
126127
compute-type: BUILD_GENERAL1_LARGE
127128
variables:
128-
TEST_SUITE: src/__tests__/build-app-ts.test.ts
129+
TEST_SUITE: >-
130+
src/__tests__/build-app-ts.test.ts|src/__tests__/uninitialized-project-modelgen-android.test.ts|src/__tests__/uninitialized-project-modelgen-flutter.test.ts|src/__tests__/uninitialized-project-modelgen-ios.test.ts
129131
CLI_REGION: ap-southeast-1
130132
depend-on:
131133
- publish_to_local_registry
134+
- identifier: uninitialized_project_modelgen_js
135+
buildspec: .codebuild/run_e2e_tests.yml
136+
env:
137+
compute-type: BUILD_GENERAL1_LARGE
138+
variables:
139+
TEST_SUITE: src/__tests__/uninitialized-project-modelgen-js.test.ts
140+
CLI_REGION: ap-southeast-2
141+
depend-on:
142+
- publish_to_local_registry
132143
- identifier: cleanup_e2e_resources
133144
buildspec: .codebuild/cleanup_e2e_resources.yml
134145
env:

packages/amplify-codegen-e2e-core/src/categories/codegen.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,16 @@ export function generateModels(cwd: string, outputDir?: string, settings: { errM
2020
});
2121
}
2222

23+
export const generateModelsWithOptions = (cwd: string, options: Record<string, any>): Promise<void> => new Promise((resolve, reject) => {
24+
spawn(getCLIPath(), ['codegen', 'models', ...(Object.entries(options).flat())], { cwd, stripColors: true }).run((err: Error) => {
25+
if (!err) {
26+
resolve();
27+
} else {
28+
reject(err);
29+
}
30+
});
31+
});
32+
2333
export function generateStatementsAndTypes(cwd: string) : Promise<void> {
2434
return new Promise((resolve, reject) => {
2535
spawn(getCLIPath(), ['codegen'], { cwd, stripColors: true })

packages/amplify-codegen-e2e-core/src/utils/frontend-config-helper.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export enum AmplifyFrontend {
22
javascript = 'javascript',
3+
typescript = 'typescript',
34
ios = 'ios',
45
android = 'android',
56
flutter = 'flutter'
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { createNewProjectDir, DEFAULT_ANDROID_CONFIG, deleteProjectDir } from '@aws-amplify/amplify-codegen-e2e-core';
2+
import { testUninitializedCodegenModels } from '../codegen-tests-base';
3+
import * as path from 'path';
4+
5+
const schemaName = 'modelgen/model_gen_schema_with_aws_scalars.graphql';
6+
7+
describe('Uninitialized Project Modelgen tests - Android', () => {
8+
let projectRoot: string;
9+
10+
beforeEach(async () => {
11+
projectRoot = await createNewProjectDir('uninitializedProjectModelgenAndroid');
12+
});
13+
14+
afterEach(() => deleteProjectDir(projectRoot));
15+
16+
it(`should generate files at desired location and not delete src files`, async () => {
17+
await testUninitializedCodegenModels({
18+
config: DEFAULT_ANDROID_CONFIG,
19+
projectRoot,
20+
schemaName,
21+
outputDir: path.join('app', 'src', 'main', 'guava'),
22+
shouldSucceed: true,
23+
expectedFilenames: [
24+
'AmplifyModelProvider.java',
25+
'Attration.java',
26+
'Comment.java',
27+
'License.java',
28+
'Person.java',
29+
'Post.java',
30+
'Status.java',
31+
'User.java',
32+
],
33+
});
34+
});
35+
36+
it(`should not generate files at desired location and not delete src files if no output dir is specified`, async () => {
37+
await testUninitializedCodegenModels({
38+
config: DEFAULT_ANDROID_CONFIG,
39+
projectRoot,
40+
schemaName,
41+
shouldSucceed: false,
42+
});
43+
});
44+
});
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { createNewProjectDir, DEFAULT_FLUTTER_CONFIG, deleteProjectDir } from '@aws-amplify/amplify-codegen-e2e-core';
2+
import { testUninitializedCodegenModels } from '../codegen-tests-base';
3+
import * as path from 'path';
4+
5+
const schemaName = 'modelgen/model_gen_schema_with_aws_scalars.graphql';
6+
7+
describe('Uninitialized Project Modelgen tests - Flutter', () => {
8+
let projectRoot: string;
9+
10+
beforeEach(async () => {
11+
projectRoot = await createNewProjectDir('uninitializedProjectModelgenFlutter');
12+
});
13+
14+
afterEach(() => deleteProjectDir(projectRoot));
15+
16+
it(`should generate files at desired location and not delete src files`, async () => {
17+
await testUninitializedCodegenModels({
18+
config: DEFAULT_FLUTTER_CONFIG,
19+
projectRoot,
20+
schemaName,
21+
outputDir: path.join('lib', 'blueprints'),
22+
shouldSucceed: true,
23+
expectedFilenames: [
24+
'Attration.dart',
25+
'Comment.dart',
26+
'License.dart',
27+
'ModelProvider.dart',
28+
'Person.dart',
29+
'Post.dart',
30+
'Status.dart',
31+
'User.dart',
32+
],
33+
});
34+
});
35+
36+
it(`should not generate files at desired location and not delete src files if no output dir is specified`, async () => {
37+
await testUninitializedCodegenModels({
38+
config: DEFAULT_FLUTTER_CONFIG,
39+
projectRoot,
40+
schemaName,
41+
shouldSucceed: false,
42+
});
43+
});
44+
});
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { createNewProjectDir, DEFAULT_IOS_CONFIG, deleteProjectDir } from '@aws-amplify/amplify-codegen-e2e-core';
2+
import { testUninitializedCodegenModels } from '../codegen-tests-base';
3+
import * as path from 'path';
4+
5+
const schemaName = 'modelgen/model_gen_schema_with_aws_scalars.graphql';
6+
7+
describe('Uninitialized Project Modelgen tests - IOS', () => {
8+
let projectRoot: string;
9+
10+
beforeEach(async () => {
11+
projectRoot = await createNewProjectDir('uninitializedProjectModelgenIOS');
12+
});
13+
14+
afterEach(() => deleteProjectDir(projectRoot));
15+
16+
it(`should generate files at desired location and not delete src files`, async () => {
17+
await testUninitializedCodegenModels({
18+
config: DEFAULT_IOS_CONFIG,
19+
projectRoot,
20+
schemaName,
21+
outputDir: path.join('amplification', 'manufactured', 'models'),
22+
shouldSucceed: true,
23+
expectedFilenames: [
24+
'AmplifyModels.swift',
25+
'Attration+Schema.swift',
26+
'Attration.swift',
27+
'Comment+Schema.swift',
28+
'Comment.swift',
29+
'License+Schema.swift',
30+
'License.swift',
31+
'Person+Schema.swift',
32+
'Person.swift',
33+
'Post+Schema.swift',
34+
'Post.swift',
35+
'Status.swift',
36+
'User+Schema.swift',
37+
'User.swift',
38+
],
39+
});
40+
});
41+
42+
it(`should not generate files at desired location and not delete src files if no output dir is specified`, async () => {
43+
await testUninitializedCodegenModels({
44+
config: DEFAULT_IOS_CONFIG,
45+
projectRoot,
46+
schemaName,
47+
shouldSucceed: false,
48+
});
49+
});
50+
});
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { AmplifyFrontend, createNewProjectDir, DEFAULT_JS_CONFIG, deleteProjectDir } from '@aws-amplify/amplify-codegen-e2e-core';
2+
import { testUninitializedCodegenModels } from '../codegen-tests-base';
3+
import * as path from 'path';
4+
5+
const schemaName = 'modelgen/model_gen_schema_with_aws_scalars.graphql';
6+
7+
describe('Uninitialized Project Modelgen tests - JS', () => {
8+
let projectRoot: string;
9+
10+
beforeEach(async () => {
11+
projectRoot = await createNewProjectDir('uninitializedProjectModelgenJS');
12+
});
13+
14+
afterEach(() => deleteProjectDir(projectRoot));
15+
16+
it(`should generate files at desired location and not delete src files`, async () => {
17+
await testUninitializedCodegenModels({
18+
config: DEFAULT_JS_CONFIG,
19+
projectRoot,
20+
schemaName,
21+
outputDir: path.join('src', 'backmodels'),
22+
shouldSucceed: true,
23+
expectedFilenames: [
24+
'index.d.ts',
25+
'index.js',
26+
'schema.d.ts',
27+
'schema.js',
28+
],
29+
});
30+
});
31+
32+
it(`should generate files at desired location and not delete src files for typescript variant`, async () => {
33+
await testUninitializedCodegenModels({
34+
config: {
35+
...DEFAULT_JS_CONFIG,
36+
frontendType: AmplifyFrontend.typescript,
37+
},
38+
projectRoot,
39+
schemaName,
40+
outputDir: path.join('src', 'backmodels'),
41+
shouldSucceed: true,
42+
expectedFilenames: [
43+
'index.ts',
44+
'schema.ts',
45+
],
46+
});
47+
});
48+
49+
it(`should not generate files at desired location and not delete src files if no output dir is specified`, async () => {
50+
await testUninitializedCodegenModels({
51+
config: DEFAULT_JS_CONFIG,
52+
projectRoot,
53+
schemaName,
54+
shouldSucceed: false,
55+
});
56+
});
57+
});

packages/amplify-codegen-e2e-tests/src/codegen-tests-base/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@ export * from './configure-codegen';
55
export * from './remove-codegen';
66
export * from './datastore-modelgen';
77
export * from './push-codegen';
8+
export * from './uninitialized-modelgen';
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { generateModelsWithOptions, AmplifyFrontendConfig, AmplifyFrontend, getSchemaPath } from '@aws-amplify/amplify-codegen-e2e-core';
2+
import { existsSync, writeFileSync, readFileSync, readdirSync } from 'fs';
3+
import { isNotEmptyDir, generateSourceCode } from '../utils';
4+
import { createPubspecLockFile } from './datastore-modelgen';
5+
import path from 'path';
6+
7+
export type TestUninitializedCodegenModelsProps = {
8+
config: AmplifyFrontendConfig;
9+
projectRoot: string;
10+
schemaName: string;
11+
shouldSucceed: boolean;
12+
outputDir?: string;
13+
featureFlags?: Record<string, any>;
14+
expectedFilenames?: Array<string>;
15+
};
16+
17+
export const testUninitializedCodegenModels = async ({
18+
config,
19+
projectRoot,
20+
schemaName,
21+
outputDir,
22+
shouldSucceed,
23+
featureFlags,
24+
expectedFilenames,
25+
}: TestUninitializedCodegenModelsProps): Promise<void> => {
26+
// generate pre existing user file
27+
const userSourceCodePath = generateSourceCode(projectRoot, config.srcDir);
28+
29+
// Write Schema File to Schema Path
30+
const schemaPath = getSchemaPath(schemaName);
31+
const schema = readFileSync(schemaPath).toString();
32+
const modelSchemaPath = path.join(config.srcDir, 'schema.graphql');
33+
writeFileSync(path.join(projectRoot, modelSchemaPath), schema);
34+
35+
// For flutter frontend, we need to have a pubspec lock file with supported dart version
36+
if (config?.frontendType === AmplifyFrontend.flutter) {
37+
createPubspecLockFile(projectRoot);
38+
}
39+
40+
// generate models
41+
try {
42+
await generateModelsWithOptions(projectRoot, {
43+
'--target': config.frontendType,
44+
'--model-schema': modelSchemaPath,
45+
'--output-dir': outputDir,
46+
...(featureFlags ? Object.entries(featureFlags).map(([ffName, ffVal]) => [`--feature-flag:${ffName}`, ffVal]).flat() : []),
47+
});
48+
} catch (_) {
49+
// This is temporarily expected to throw, since the post-modelgen hook in amplify cli fails, even though modelgen succeeds.
50+
}
51+
52+
// pre-existing file should still exist
53+
expect(existsSync(userSourceCodePath)).toBe(true);
54+
55+
// datastore models are generated at correct location
56+
const partialDirToCheck = outputDir
57+
? path.join(projectRoot, outputDir)
58+
: path.join(projectRoot, config.modelgenDir);
59+
const dirToCheck = config.frontendType === AmplifyFrontend.android
60+
? path.join(partialDirToCheck, 'com', 'amplifyframework', 'datastore', 'generated', 'model')
61+
: partialDirToCheck;
62+
63+
expect(isNotEmptyDir(dirToCheck)).toBe(shouldSucceed);
64+
65+
if (expectedFilenames) {
66+
const foundFiles = new Set(readdirSync(dirToCheck));
67+
console.log(`Comparing written files: ${JSON.stringify(Array.from(foundFiles))} to expected files: ${JSON.stringify(expectedFilenames)}`);
68+
expectedFilenames.forEach((expectedFilename) => expect(foundFiles.has(expectedFilename)).toBe(true));
69+
}
70+
};

0 commit comments

Comments
 (0)