From 968a7550e2b3299b7d589d52ea263b327f1af57b Mon Sep 17 00:00:00 2001 From: Tyler Roach Date: Wed, 16 Aug 2023 16:17:23 -0400 Subject: [PATCH] Android CSS and LL support --- .../src/configs/java-config.ts | 33 +++- .../src/languages/java-declaration-block.ts | 3 +- .../appsync-modelgen-plugin/src/preset.ts | 77 +++++++-- .../src/visitors/appsync-java-visitor.ts | 153 +++++++++++++++++- 4 files changed, 243 insertions(+), 23 deletions(-) diff --git a/packages/appsync-modelgen-plugin/src/configs/java-config.ts b/packages/appsync-modelgen-plugin/src/configs/java-config.ts index be19d1313..ef6a585a0 100644 --- a/packages/appsync-modelgen-plugin/src/configs/java-config.ts +++ b/packages/appsync-modelgen-plugin/src/configs/java-config.ts @@ -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'; @@ -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', @@ -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(); diff --git a/packages/appsync-modelgen-plugin/src/languages/java-declaration-block.ts b/packages/appsync-modelgen-plugin/src/languages/java-declaration-block.ts index dc140e7fc..9659d5c8a 100644 --- a/packages/appsync-modelgen-plugin/src/languages/java-declaration-block.ts +++ b/packages/appsync-modelgen-plugin/src/languages/java-declaration-block.ts @@ -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' | ''; export type Kind = 'class' | 'interface' | 'enum'; export type MemberFlags = { transient?: boolean; @@ -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, diff --git a/packages/appsync-modelgen-plugin/src/preset.ts b/packages/appsync-modelgen-plugin/src/preset.ts index 8d1f69d52..e41a1efab 100644 --- a/packages/appsync-modelgen-plugin/src/preset.ts +++ b/packages/appsync-modelgen-plugin/src/preset.ts @@ -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']; @@ -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; diff --git a/packages/appsync-modelgen-plugin/src/visitors/appsync-java-visitor.ts b/packages/appsync-modelgen-plugin/src/visitors/appsync-java-visitor.ts index d9d5541ee..c4d5a69c0 100644 --- a/packages/appsync-modelgen-plugin/src/visitors/appsync-java-visitor.ts +++ b/packages/appsync-modelgen-plugin/src/visitors/appsync-java-visitor.ts @@ -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'; @@ -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()) { @@ -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() @@ -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); @@ -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); + } } /** @@ -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[] = []; @@ -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, ''); + } + +} \ No newline at end of file