Skip to content

Commit

Permalink
Android CSS and LL support
Browse files Browse the repository at this point in the history
  • Loading branch information
tylerjroach committed Aug 16, 2023
1 parent e8d63f0 commit 968a755
Show file tree
Hide file tree
Showing 4 changed files with 243 additions and 23 deletions.
33 changes: 31 additions & 2 deletions packages/appsync-modelgen-plugin/src/configs/java-config.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { CodeGenConnectionType } from '../utils/process-connections';

// Name of the Generated Java Package
export const GENERATED_PACKAGE_NAME = 'com.amplifyframework.datastore.generated.model';
// Name of the Generated Java Package for DataStore category
export const GENERATED_DATASTORE_PACKAGE_NAME = 'com.amplifyframework.datastore.generated.model';

// Name of the Generated Java Package for API category
export const GENERATED_API_PACKAGE_NAME = 'com.amplifyframework.api.generated.model';

// Name of the Class Loader package
export const LOADER_CLASS_NAME = 'AmplifyModelProvider';
Expand All @@ -10,6 +13,8 @@ const JAVA_UTIL_PACKAGES = ['java.util.List', 'java.util.UUID', 'java.util.Objec

const ANDROIDX_CORE_PACKAGES = ['androidx.core.util.ObjectsCompat'];

const ANDROIDX_ANNOTATION_PACKAGES = ['androidx.annotation.NonNull', 'androidx.annotation.Nullable']

const AMPLIFY_FRAMEWORK_PACKAGES = [
'com.amplifyframework.core.model.Model',
'com.amplifyframework.core.model.annotations.Index',
Expand Down Expand Up @@ -83,3 +88,27 @@ export const CONNECTION_RELATIONSHIP_IMPORTS: { [key in CodeGenConnectionType]:
};

export const CUSTOM_PRIMARY_KEY_IMPORT_PACKAGE = 'com.amplifyframework.core.model.ModelIdentifier';

export const LAZY_MODEL_IMPORT_PACKAGE = 'com.amplifyframework.core.model.LazyModel'

export const CONNECTION_RELATIONSHIP_LAZY_LOAD_IMPORTS: { [key in CodeGenConnectionType]: string } = {
BELONGS_TO: LAZY_MODEL_IMPORT_PACKAGE,
HAS_MANY: 'com.amplifyframework.core.model.LazyList',
HAS_ONE: LAZY_MODEL_IMPORT_PACKAGE,
};

export const MODEL_PATH_IMPORT_PACKAGES = [
'com.amplifyframework.core.model.ModelPath',
'com.amplifyframework.core.model.PropertyPath'
]

export function getModelPathClassImports(): string[] {
return [
...ANDROIDX_ANNOTATION_PACKAGES,
'',
...MODEL_PATH_IMPORT_PACKAGES,
'',
];
}

export const MODEL_PATH_CLASS_IMPORT_PACKAGES = getModelPathClassImports();
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { StringValueNode, NameNode } from 'graphql';
import stripIndent from 'strip-indent';

// Todo: PR to @graphql-codegen/java-common to support method exceptions and comment
export type Access = 'private' | 'public' | 'protected';
export type Access = 'private' | 'public' | 'protected' | '';

This comment has been minimized.

Copy link
@lawmicha

lawmicha Sep 15, 2023

Contributor

is this needed? how is the empty case used? a search on Access doesn't show any changes

export type Kind = 'class' | 'interface' | 'enum';
export type MemberFlags = {
transient?: boolean;
Expand Down Expand Up @@ -143,6 +143,7 @@ export class JavaDeclarationBlock {
method.flags.final ? 'final' : null,
method.flags.transient ? 'transient' : null,
method.flags.volatile ? 'volatile' : null,
method.flags.synchronized ? 'synchronized' : null,
...(method.returnTypeAnnotations || []).map(annotation => `@${annotation}`),
method.returnType,
method.name,
Expand Down
77 changes: 64 additions & 13 deletions packages/appsync-modelgen-plugin/src/preset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Types } from '@graphql-codegen/plugin-helpers';
import { Kind, TypeDefinitionNode } from 'graphql';
import { join } from 'path';
import { JAVA_SCALAR_MAP, SWIFT_SCALAR_MAP, TYPESCRIPT_SCALAR_MAP, DART_SCALAR_MAP, METADATA_SCALAR_MAP } from './scalars';
import { LOADER_CLASS_NAME, GENERATED_PACKAGE_NAME } from './configs/java-config';
import { LOADER_CLASS_NAME, GENERATED_API_PACKAGE_NAME, GENERATED_DATASTORE_PACKAGE_NAME } from './configs/java-config';
import { graphqlName, toUpper } from 'graphql-transformer-common';

const APPSYNC_DATA_STORE_CODEGEN_TARGETS = ['java', 'swift', 'javascript', 'typescript', 'dart'];
Expand Down Expand Up @@ -33,29 +33,80 @@ const generateJavaPreset = (
models: TypeDefinitionNode[],
): Types.GenerateOptions[] => {
const config: Types.GenerateOptions[] = [];
const modelFolder = options.config.overrideOutputDir ? [options.config.overrideOutputDir] : [options.baseOutputDir, ...GENERATED_PACKAGE_NAME.split('.')];
const dataStoreModelFolder = options.config.overrideOutputDir ? [options.config.overrideOutputDir] : [options.baseOutputDir, ...GENERATED_DATASTORE_PACKAGE_NAME.split('.')];
models.forEach(model => {

// Android splits models into 2 different packages.. 1 for DataStore and 1 for API
// generateModelsForLazyLoadAndCustomSelectionSet is used to decide whether or not we create the codegen for the API category

// Model
const modelName = model.name.value;
config.push({
...options,
filename: join(...modelFolder, `${modelName}.java`),
filename: join(...dataStoreModelFolder, `${modelName}.java`),
config: {
...options.config,
generateModelsForLazyLoadAndCustomSelectionSet: false, // override value to output DataStore models
scalars: { ...JAVA_SCALAR_MAP, ...options.config.scalars },
selectedType: modelName,
},
});
});

// Class loader
config.push({
...options,
filename: join(...modelFolder, `${LOADER_CLASS_NAME}.java`),
config: {
...options.config,
scalars: { ...JAVA_SCALAR_MAP, ...options.config.scalars },
generate: 'loader',
},
// Class loader
config.push({
...options,
filename: join(...dataStoreModelFolder, `${LOADER_CLASS_NAME}.java`),
config: {
...options.config,
generateModelsForLazyLoadAndCustomSelectionSet: false, // override value to output DataStore models
scalars: { ...JAVA_SCALAR_MAP, ...options.config.scalars },
generate: 'loader',
},
});

if (options.config.generateModelsForLazyLoadAndCustomSelectionSet) {

// If an overrideOutputDir is provided, we palce all API codegen in an 'api' folder to prevent collisions with DataStore models
const apiModelFolder = options.config.overrideOutputDir ?
[options.config.overrideOutputDir, "api"] :
[options.baseOutputDir, ...GENERATED_API_PACKAGE_NAME.split('.')];

// Model
const modelName = model.name.value;
config.push({
...options,
filename: join(...apiModelFolder, `${modelName}.java`),
config: {
...options.config,
scalars: { ...JAVA_SCALAR_MAP, ...options.config.scalars },
selectedType: modelName,
},
});

// ModelPath
const modelPathName = modelName + "Path";
config.push({
...options,
filename: join(...apiModelFolder, `${modelPathName}.java`),
config: {
...options.config,
scalars: { ...JAVA_SCALAR_MAP, ...options.config.scalars },
generate: 'metadata',
selectedType: modelName,
},
});

// Class loader
config.push({
...options,
filename: join(...apiModelFolder, `${LOADER_CLASS_NAME}.java`),
config: {
...options.config,
scalars: { ...JAVA_SCALAR_MAP, ...options.config.scalars },
generate: 'loader',
},
});
}
});

return config;
Expand Down
153 changes: 146 additions & 7 deletions packages/appsync-modelgen-plugin/src/visitors/appsync-java-visitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,28 @@ import { camelCase, constantCase, pascalCase } from 'change-case';
import dedent from 'ts-dedent';
import {
MODEL_CLASS_IMPORT_PACKAGES,
GENERATED_PACKAGE_NAME,
MODEL_PATH_CLASS_IMPORT_PACKAGES,
GENERATED_API_PACKAGE_NAME,
GENERATED_DATASTORE_PACKAGE_NAME,
LOADER_CLASS_NAME,
LOADER_IMPORT_PACKAGES,
CONNECTION_RELATIONSHIP_IMPORTS,
CONNECTION_RELATIONSHIP_LAZY_LOAD_IMPORTS,
NON_MODEL_CLASS_IMPORT_PACKAGES,
MODEL_AUTH_CLASS_IMPORT_PACKAGES,
CUSTOM_PRIMARY_KEY_IMPORT_PACKAGE,
} from '../configs/java-config';
import { JAVA_TYPE_IMPORT_MAP } from '../scalars';
import { JavaDeclarationBlock } from '../languages/java-declaration-block';
import { AppSyncModelVisitor, CodeGenField, CodeGenModel, CodeGenPrimaryKeyType, ParsedAppSyncModelConfig, RawAppSyncModelConfig } from './appsync-visitor';
import {
AppSyncModelVisitor,
CodeGenField,
CodeGenGenerateEnum,
CodeGenModel,
CodeGenPrimaryKeyType,
ParsedAppSyncModelConfig,
RawAppSyncModelConfig
} from './appsync-visitor';
import { CodeGenConnectionType } from '../utils/process-connections';
import { AuthDirective, AuthStrategy } from '../utils/process-auth';
import { printWarning } from '../utils/warn';
Expand All @@ -36,8 +47,10 @@ export class AppSyncModelJavaVisitor<
shouldImputeKeyForUniDirectionalHasMany
);

if (this._parsedConfig.generate === 'loader') {
if (this._parsedConfig.generate === CodeGenGenerateEnum.loader) {
return this.generateClassLoader();
} else if (this._parsedConfig.generate === CodeGenGenerateEnum.metadata) {
return this.generateModelPathClasses();
}
validateFieldName({ ...this.getSelectedModels(), ...this.getSelectedNonModels() });
if (this.selectedTypeIsEnum()) {
Expand Down Expand Up @@ -175,7 +188,11 @@ export class AppSyncModelJavaVisitor<
}

generatePackageName(): string {
return `package ${GENERATED_PACKAGE_NAME};`;
if (this.isGenerateModelsForLazyLoadAndCustomSelectionSet()) {
return `package ${GENERATED_API_PACKAGE_NAME};`;
} else {
return `package ${GENERATED_DATASTORE_PACKAGE_NAME};`;
}
}
generateModelClass(model: CodeGenModel): string {
const classDeclarationBlock = new JavaDeclarationBlock()
Expand All @@ -189,6 +206,11 @@ export class AppSyncModelJavaVisitor<
const annotations = this.generateModelAnnotations(model);
classDeclarationBlock.annotate(annotations);

// generate rootPath for models with Custom Selection Set enabled
if (this.isGenerateModelsForLazyLoadAndCustomSelectionSet()) {
this.generateRootPath(model, classDeclarationBlock);
}

const queryFields = this.getWritableFields(model);
queryFields.forEach(field => this.generateQueryFields(model, field, classDeclarationBlock));
const nonConnectedFields = this.getNonConnectedField(model);
Expand Down Expand Up @@ -735,7 +757,25 @@ export class AppSyncModelJavaVisitor<
if (Object.keys(JAVA_TYPE_IMPORT_MAP).includes(nativeType)) {
this.additionalPackages.add(JAVA_TYPE_IMPORT_MAP[nativeType]);
}
return nativeType;
if(this.isGenerateModelsForLazyLoadAndCustomSelectionSet() && field.connectionInfo?.kind) {
switch (field.connectionInfo?.kind) {
case CodeGenConnectionType.BELONGS_TO:
case CodeGenConnectionType.HAS_ONE:
return `LazyModel<${nativeType}>`;
default:
return nativeType;
}
} else {
return nativeType;
}
}

protected getListType(typeStr: string, field: CodeGenField): string {
if(this.isGenerateModelsForLazyLoadAndCustomSelectionSet()) {
return `LazyList<${typeStr}>`
} else {
return super.getListType(typeStr, field);
}
}

/**
Expand Down Expand Up @@ -956,7 +996,9 @@ export class AppSyncModelJavaVisitor<
const { connectionInfo } = field;
// Add annotation to import
this.additionalPackages.add(CONNECTION_RELATIONSHIP_IMPORTS[connectionInfo.kind]);

if(this.isGenerateModelsForLazyLoadAndCustomSelectionSet()) {
this.additionalPackages.add(CONNECTION_RELATIONSHIP_LAZY_LOAD_IMPORTS[connectionInfo.kind]);
}
let connectionDirectiveName: string = '';
const connectionArguments: string[] = [];

Expand Down Expand Up @@ -1023,4 +1065,101 @@ export class AppSyncModelJavaVisitor<
protected getWritableFields(model: CodeGenModel): CodeGenField[] {
return this.getNonConnectedField(model).filter(f => !f.isReadOnly);
}
}

protected getConnectedFields(model: CodeGenModel): CodeGenField[] {
return model.fields.filter(f => {
if (f.connectionInfo) return true;
})
}

protected generateRootPath(model: CodeGenModel, classDeclarationBlock: JavaDeclarationBlock) {
const modelPathName = this.generateModelPathName(model);
classDeclarationBlock.addClassMember(
"rootPath",
modelPathName,
`new ${modelPathName}("root", false, null)`,
[],
'public',
{
final: true,
static: true,
},
);
}

protected generateModelPathClasses(): string {
const result: string[] = [];
Object.entries(this.getSelectedModels()).forEach(([name, model]) => {
const modelPathDeclaration = this.generateModelPathClass(model);
result.push(...[modelPathDeclaration]);
});
const packageDeclaration = this.generateModelPathPackageHeader();
return [packageDeclaration, ...result].join('\n');
}

protected generateModelPathClass(model: CodeGenModel): string {
const classDeclarationBlock = new JavaDeclarationBlock()
.asKind('class')
.access('public')
.withName(this.generateModelPathName(model))
.extends([`ModelPath<${this.getModelName(model)}>`])
.withComment(`This is an auto generated class representing the ModelPath for the ${model.name} type in your schema.`)
.final();

// constructor
this.generateModelPathConstructor(model, classDeclarationBlock);

// fields and getters
const connectedFields = this.getConnectedFields(model);
connectedFields.forEach(field => this.generateModelPathField(field, classDeclarationBlock));

return classDeclarationBlock.string;
}

protected generateModelPathName(model: CodeGenModel): string {
return this.getModelName(model) + "Path";
}

protected generateModelPathField(field: CodeGenField, classDeclarationBlock: JavaDeclarationBlock): void {

const fieldName = this.getFieldName(field);
const fieldType = this.generateModelPathName(this.modelMap[field.type])
classDeclarationBlock.addClassMember(fieldName, fieldType, '', [], 'private');

const methodName = this.getFieldGetterName(field);
const modelPathFieldGetterBody = dedent`
if (${fieldName} == null) {
${fieldName} = new ${fieldType}("${fieldName}", ${field.isList}, this);
}
return ${fieldName};`;

classDeclarationBlock.addClassMethod(methodName, fieldType, modelPathFieldGetterBody, undefined, undefined, 'public', {
synchronized: true,
});
};

protected generateModelPathPackageHeader(): string {
const imports = this.generateImportStatements(MODEL_PATH_CLASS_IMPORT_PACKAGES);
return [this.generatePackageName(), '', imports].join('\n');
}

/**
* Generate constructor for the extended ModelPath class
* @param model CodeGenModel
* @param declarationsBlock Class Declaration block to which constructor will be added
*/
protected generateModelPathConstructor(model: CodeGenModel, declarationsBlock: JavaDeclarationBlock): void {
const modelPathName = this.generateModelPathName(model)

const constructorArguments = [
{ name: "name", type: "@NonNull String" },
{ name: "isCollection", type: "@NonNull Boolean" },
{ name: "parent", type: "@Nullable PropertyPath" }
]

const body = `super(name, isCollection, parent, ${this.getModelName(model)}.class);`

declarationsBlock.addClassMethod(modelPathName, null, body, constructorArguments, undefined, '');
}

}

0 comments on commit 968a755

Please sign in to comment.