From 7adb34bafe5dcf2043486ee20ec69d2a16e5bb15 Mon Sep 17 00:00:00 2001 From: Al Harris Date: Mon, 25 Sep 2023 10:01:37 -0700 Subject: [PATCH] chore: flip to use real generator, not mocked api --- package-lock.json | 1 + packages/cli/package.json | 1 + .../generate/generate_command_factory.ts | 6 +- .../generate_api_code_adapter.ts | 33 ++++ ...nerate_graphql_client_code_command.test.ts | 186 +++++++++++++----- .../generate_graphql_client_code_command.ts | 44 +++-- .../mock_code_generator.ts | 110 ----------- packages/cli/tsconfig.json | 1 + 8 files changed, 202 insertions(+), 180 deletions(-) create mode 100644 packages/cli/src/commands/generate/graphql-client-code/generate_api_code_adapter.ts delete mode 100644 packages/cli/src/commands/generate/graphql-client-code/mock_code_generator.ts diff --git a/package-lock.json b/package-lock.json index 54048ebda3..1ed6952ba0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18505,6 +18505,7 @@ "dependencies": { "@aws-amplify/client-config": "^0.2.0-alpha.5", "@aws-amplify/deployed-backend-client": "^0.1.0", + "@aws-amplify/model-generator": "^0.2.0-alpha.1", "@aws-amplify/sandbox": "^0.1.1-alpha.5", "@aws-sdk/credential-providers": "^3.360.0", "@inquirer/prompts": "^3.0.0", diff --git a/packages/cli/package.json b/packages/cli/package.json index 3841f6d1c2..cfa379969b 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -29,6 +29,7 @@ "dependencies": { "@aws-amplify/client-config": "^0.2.0-alpha.5", "@aws-amplify/deployed-backend-client": "^0.1.0", + "@aws-amplify/model-generator": "^0.2.0-alpha.1", "@aws-amplify/sandbox": "^0.1.1-alpha.5", "@aws-sdk/credential-providers": "^3.360.0", "@inquirer/prompts": "^3.0.0", diff --git a/packages/cli/src/commands/generate/generate_command_factory.ts b/packages/cli/src/commands/generate/generate_command_factory.ts index ad8da52346..2e439936d0 100644 --- a/packages/cli/src/commands/generate/generate_command_factory.ts +++ b/packages/cli/src/commands/generate/generate_command_factory.ts @@ -7,7 +7,7 @@ import { CwdPackageJsonLoader } from '../../cwd_package_json_loader.js'; import { GenerateGraphqlClientCodeCommand } from './graphql-client-code/generate_graphql_client_code_command.js'; import { LocalAppNameResolver } from '../../backend-identifier/local_app_name_resolver.js'; import { BackendIdentifierResolver } from '../../backend-identifier/backend_identifier_resolver.js'; -import { ApiCodeGenerator } from './graphql-client-code/mock_code_generator.js'; +import { GenerateApiCodeAdapter } from './graphql-client-code/generate_api_code_adapter.js'; /** * Creates wired generate command. @@ -30,10 +30,10 @@ export const createGenerateCommand = (): CommandModule => { backendIdentifierResolver ); - const apiCodeGenerator = new ApiCodeGenerator(credentialProvider); + const generateApiCodeAdapter = new GenerateApiCodeAdapter(credentialProvider); const generateGraphqlClientCodeCommand = new GenerateGraphqlClientCodeCommand( - apiCodeGenerator, + generateApiCodeAdapter, backendIdentifierResolver ); diff --git a/packages/cli/src/commands/generate/graphql-client-code/generate_api_code_adapter.ts b/packages/cli/src/commands/generate/graphql-client-code/generate_api_code_adapter.ts new file mode 100644 index 0000000000..5a7a4a23c5 --- /dev/null +++ b/packages/cli/src/commands/generate/graphql-client-code/generate_api_code_adapter.ts @@ -0,0 +1,33 @@ +import { BackendIdentifier } from '@aws-amplify/deployed-backend-client'; +import { + GenerateOptions, + GenerationResult, + generateApiCode, +} from '@aws-amplify/model-generator'; +import { AwsCredentialIdentityProvider } from '@aws-sdk/types'; + +// For some reason using `omit` is causing type errors, so reconstructing without the credentialProvider. +export type InvokeGenerateApiCodeProps = GenerateOptions & BackendIdentifier; + +/** + * Class to wrap static generateApiCode method for testability. + */ +export class GenerateApiCodeAdapter { + /** + * Creates graphql api code adapter. + */ + constructor( + private readonly credentialProvider: AwsCredentialIdentityProvider + ) {} + + /** + * Invoke the generateApiCode method, using the constructor injected credentialProvider, and remaining props. + */ + invokeGenerateApiCode = ( + props: InvokeGenerateApiCodeProps + ): Promise => + generateApiCode({ + ...props, + credentialProvider: this.credentialProvider, + }); +} diff --git a/packages/cli/src/commands/generate/graphql-client-code/generate_graphql_client_code_command.test.ts b/packages/cli/src/commands/generate/graphql-client-code/generate_graphql_client_code_command.test.ts index 02d22fe25b..b4be9dc344 100644 --- a/packages/cli/src/commands/generate/graphql-client-code/generate_graphql_client_code_command.test.ts +++ b/packages/cli/src/commands/generate/graphql-client-code/generate_graphql_client_code_command.test.ts @@ -9,22 +9,28 @@ import { import assert from 'node:assert'; import path from 'path'; import { BackendIdentifierResolver } from '../../../backend-identifier/backend_identifier_resolver.js'; -import { ApiCodeGenerator } from './mock_code_generator.js'; +import { GenerateApiCodeAdapter } from './generate_api_code_adapter.js'; describe('generate graphql-client-code command', () => { - const apiCodeGenerator = new ApiCodeGenerator(fromNodeProviderChain()); + const generateApiCodeAdapter = new GenerateApiCodeAdapter( + fromNodeProviderChain() + ); - const codeGeneratorMock = mock.method( - apiCodeGenerator, - 'generateAPICodeToFile', - () => Promise.resolve() + const writeToDirectoryMock = mock.fn(); + const invokeGenerateApiCodeMock = mock.method( + generateApiCodeAdapter, + 'invokeGenerateApiCode', + () => + Promise.resolve({ + writeToDirectory: writeToDirectoryMock, + }) ); const backendIdentifierResolver = new BackendIdentifierResolver({ resolve: () => Promise.resolve('testAppName'), }); const generateGraphqlClientCodeCommand = new GenerateGraphqlClientCodeCommand( - apiCodeGenerator, + generateApiCodeAdapter, backendIdentifierResolver ); const parser = yargs().command( @@ -33,65 +39,74 @@ describe('generate graphql-client-code command', () => { const commandRunner = new TestCommandRunner(parser); beforeEach(() => { - codeGeneratorMock.mock.resetCalls(); + invokeGenerateApiCodeMock.mock.resetCalls(); + writeToDirectoryMock.mock.resetCalls(); }); it('generates and writes graphql client code for stack', async () => { await commandRunner.runCommand('graphql-client-code --stack stack_name'); - assert.equal(codeGeneratorMock.mock.callCount(), 1); - assert.deepEqual(codeGeneratorMock.mock.calls[0].arguments[0], { - backendIdentifier: { - stackName: 'stack_name', - }, + assert.equal(invokeGenerateApiCodeMock.mock.callCount(), 1); + assert.deepEqual(invokeGenerateApiCodeMock.mock.calls[0].arguments[0], { + stackName: 'stack_name', format: 'graphql-codegen', - out: process.cwd(), statementTarget: 'javascript', }); + assert.equal(writeToDirectoryMock.mock.callCount(), 1); + assert.equal( + writeToDirectoryMock.mock.calls[0].arguments[0], + process.cwd() + ); }); it('generates and writes graphql client code for branch', async () => { await commandRunner.runCommand('graphql-client-code --branch branch_name'); - assert.equal(codeGeneratorMock.mock.callCount(), 1); - assert.deepEqual(codeGeneratorMock.mock.calls[0].arguments[0], { - backendIdentifier: { - appName: 'testAppName', - branchName: 'branch_name', - }, - out: process.cwd(), + assert.equal(invokeGenerateApiCodeMock.mock.callCount(), 1); + assert.deepEqual(invokeGenerateApiCodeMock.mock.calls[0].arguments[0], { + appName: 'testAppName', + branchName: 'branch_name', format: 'graphql-codegen', statementTarget: 'javascript', }); + assert.equal(writeToDirectoryMock.mock.callCount(), 1); + assert.equal( + writeToDirectoryMock.mock.calls[0].arguments[0], + process.cwd() + ); }); it('generates and writes graphql client code for appID and branch', async () => { await commandRunner.runCommand( 'graphql-client-code --branch branch_name --appId app_id' ); - assert.equal(codeGeneratorMock.mock.callCount(), 1); - assert.deepEqual(codeGeneratorMock.mock.calls[0].arguments[0], { - backendIdentifier: { - backendId: 'app_id', - branchName: 'branch_name', - }, - out: process.cwd(), + assert.equal(invokeGenerateApiCodeMock.mock.callCount(), 1); + assert.deepEqual(invokeGenerateApiCodeMock.mock.calls[0].arguments[0], { + backendId: 'app_id', + branchName: 'branch_name', format: 'graphql-codegen', statementTarget: 'javascript', }); + assert.equal(writeToDirectoryMock.mock.callCount(), 1); + assert.equal( + writeToDirectoryMock.mock.calls[0].arguments[0], + process.cwd() + ); }); it('can generate to custom relative path', async () => { await commandRunner.runCommand( 'graphql-client-code --stack stack_name --out foo/bar' ); - assert.equal(codeGeneratorMock.mock.callCount(), 1); - assert.deepEqual(codeGeneratorMock.mock.calls[0].arguments[0], { - backendIdentifier: { - stackName: 'stack_name', - }, + assert.equal(invokeGenerateApiCodeMock.mock.callCount(), 1); + assert.deepEqual(invokeGenerateApiCodeMock.mock.calls[0].arguments[0], { + stackName: 'stack_name', format: 'graphql-codegen', statementTarget: 'javascript', - out: path.join(process.cwd(), 'foo', 'bar'), }); + assert.equal(writeToDirectoryMock.mock.callCount(), 1); + assert.equal( + writeToDirectoryMock.mock.calls[0].arguments[0], + path.join(process.cwd(), 'foo', 'bar') + ); }); it('shows available options in help output', async () => { @@ -104,50 +119,117 @@ describe('generate graphql-client-code command', () => { assert.match(output, /--typeTarget/); assert.match(output, /--modelTarget/); assert.match(output, /--out/); + assert.match(output, /--all/); + }); + + it('shows all available options in help output', async () => { + const output = await commandRunner.runCommand( + 'graphql-client-code --help --all' + ); + assert.match(output, /--stack/); + assert.match(output, /--appId/); + assert.match(output, /--branch/); + assert.match(output, /--format/); + assert.match(output, /--statementTarget/); + assert.match(output, /--typeTarget/); + assert.match(output, /--modelTarget/); + assert.match(output, /--out/); + assert.match(output, /--all/); + assert.match(output, /--modelGenerateIndexRules/); + assert.match(output, /--modelEmitAuthProvider/); + assert.match(output, /--modelAddTimestampFields/); + assert.match(output, /--statementMaxDepth/); + assert.match(output, /--statementTypenameIntrospection/); + assert.match(output, /--typeMultipleSwiftFiles/); }); it('can be invoked explicitly with graphql-codegen format', async () => { await commandRunner.runCommand( 'graphql-client-code --stack stack_name --format graphql-codegen' ); - assert.equal(codeGeneratorMock.mock.callCount(), 1); - assert.deepEqual(codeGeneratorMock.mock.calls[0].arguments[0], { - backendIdentifier: { - stackName: 'stack_name', - }, + assert.equal(invokeGenerateApiCodeMock.mock.callCount(), 1); + assert.deepEqual(invokeGenerateApiCodeMock.mock.calls[0].arguments[0], { + stackName: 'stack_name', format: 'graphql-codegen', statementTarget: 'javascript', - out: process.cwd(), }); + assert.equal(writeToDirectoryMock.mock.callCount(), 1); + assert.equal( + writeToDirectoryMock.mock.calls[0].arguments[0], + process.cwd() + ); }); it('can be invoked explicitly with modelgen format', async () => { await commandRunner.runCommand( 'graphql-client-code --stack stack_name --format modelgen' ); - assert.equal(codeGeneratorMock.mock.callCount(), 1); - assert.deepEqual(codeGeneratorMock.mock.calls[0].arguments[0], { - backendIdentifier: { - stackName: 'stack_name', - }, + assert.equal(invokeGenerateApiCodeMock.mock.callCount(), 1); + assert.deepEqual(invokeGenerateApiCodeMock.mock.calls[0].arguments[0], { + stackName: 'stack_name', format: 'modelgen', modelTarget: 'javascript', - out: process.cwd(), }); + assert.equal(writeToDirectoryMock.mock.callCount(), 1); + assert.equal( + writeToDirectoryMock.mock.calls[0].arguments[0], + process.cwd() + ); }); it('can be invoked explicitly with introspection format', async () => { await commandRunner.runCommand( 'graphql-client-code --stack stack_name --format introspection' ); - assert.equal(codeGeneratorMock.mock.callCount(), 1); - assert.deepEqual(codeGeneratorMock.mock.calls[0].arguments[0], { - backendIdentifier: { - stackName: 'stack_name', - }, + assert.equal(invokeGenerateApiCodeMock.mock.callCount(), 1); + assert.deepEqual(invokeGenerateApiCodeMock.mock.calls[0].arguments[0], { + stackName: 'stack_name', format: 'introspection', - out: process.cwd(), }); + assert.equal(writeToDirectoryMock.mock.callCount(), 1); + assert.equal( + writeToDirectoryMock.mock.calls[0].arguments[0], + process.cwd() + ); + }); + + it('passes in feature flags on modelgen', async () => { + await commandRunner.runCommand( + 'graphql-client-code --stack stack_name --format modelgen --modelGenerateIndexRules true --modelEmitAuthProvider true --modelGenerateModelsForLazyLoadAndCustomSelectionSet false' + ); + assert.equal(invokeGenerateApiCodeMock.mock.callCount(), 1); + assert.deepEqual(invokeGenerateApiCodeMock.mock.calls[0].arguments[0], { + stackName: 'stack_name', + format: 'modelgen', + modelTarget: 'javascript', + generateIndexRules: true, + emitAuthProvider: true, + generateModelsForLazyLoadAndCustomSelectionSet: false, + }); + assert.equal(writeToDirectoryMock.mock.callCount(), 1); + assert.equal( + writeToDirectoryMock.mock.calls[0].arguments[0], + process.cwd() + ); + }); + + it('passes in feature flags on graphql-codegen', async () => { + await commandRunner.runCommand( + 'graphql-client-code --stack stack_name --format graphql-codegen --statementTarget typescript --statementMaxDepth 3 --statementTypenameIntrospection true' + ); + assert.equal(invokeGenerateApiCodeMock.mock.callCount(), 1); + assert.deepEqual(invokeGenerateApiCodeMock.mock.calls[0].arguments[0], { + stackName: 'stack_name', + format: 'graphql-codegen', + statementTarget: 'typescript', + maxDepth: 3, + typenameIntrospection: true, + }); + assert.equal(writeToDirectoryMock.mock.callCount(), 1); + assert.equal( + writeToDirectoryMock.mock.calls[0].arguments[0], + process.cwd() + ); }); // Note: after this test, future tests seem to be in a weird state, leaving this at the diff --git a/packages/cli/src/commands/generate/graphql-client-code/generate_graphql_client_code_command.ts b/packages/cli/src/commands/generate/graphql-client-code/generate_graphql_client_code_command.ts index ec90834069..164bb3ee15 100644 --- a/packages/cli/src/commands/generate/graphql-client-code/generate_graphql_client_code_command.ts +++ b/packages/cli/src/commands/generate/graphql-client-code/generate_graphql_client_code_command.ts @@ -1,7 +1,7 @@ import { ArgumentsCamelCase, Argv, CommandModule } from 'yargs'; import { BackendIdentifierResolver } from '../../../backend-identifier/backend_identifier_resolver.js'; -import { ApiCodeGenerator } from './mock_code_generator.js'; import { isAbsolute, resolve } from 'path'; +import { GenerateApiCodeAdapter } from './generate_api_code_adapter.js'; export const formatChoices = ['graphql-codegen', 'introspection', 'modelgen']; export const modelgenTargetChoices = [ @@ -73,7 +73,7 @@ export class GenerateGraphqlClientCodeCommand * Creates graphql client code generation command. */ constructor( - private readonly apiCodeGenerator: ApiCodeGenerator, + private readonly generateApiCodeAdapter: GenerateApiCodeAdapter, private readonly backendIdentifierResolver: BackendIdentifierResolver ) { this.command = 'graphql-client-code'; @@ -88,42 +88,42 @@ export class GenerateGraphqlClientCodeCommand case 'graphql-codegen': return { statementTarget: args.statementTarget ?? 'javascript', - ...(args.typeTarget ? { typeTarget: args.typeTarget } : {}), - ...(args.statementMaxDepth + ...('typeTarget' in args ? { typeTarget: args.typeTarget } : {}), + ...('statementMaxDepth' in args ? { maxDepth: args.statementMaxDepth } : {}), - ...(args.statementTypenameIntrospection + ...('statementTypenameIntrospection' in args ? { typenameIntrospection: args.statementTypenameIntrospection } : {}), - ...(args.typeMultipleSwiftFiles + ...('typeMultipleSwiftFiles' in args ? { multipleSwiftFiles: args.typeMultipleSwiftFiles } : {}), }; case 'modelgen': return { modelTarget: args.modelTarget ?? 'javascript', - ...(args.modelGenerateIndexRules + ...('modelGenerateIndexRules' in args ? { generateIndexRules: args.modelGenerateIndexRules } : {}), - ...(args.modelEmitAuthProvider + ...('modelEmitAuthProvider' in args ? { emitAuthProvider: args.modelEmitAuthProvider } : {}), - ...(args.modelRespectPrimaryKeyAttributesOnConnectionField + ...('modelRespectPrimaryKeyAttributesOnConnectionField' in args ? { respectPrimaryKeyAttributesOnConnectionField: args.modelRespectPrimaryKeyAttributesOnConnectionField, } : {}), - ...(args.modelGenerateModelsForLazyLoadAndCustomSelectionSet + ...('modelGenerateModelsForLazyLoadAndCustomSelectionSet' in args ? { generateModelsForLazyLoadAndCustomSelectionSet: args.modelGenerateModelsForLazyLoadAndCustomSelectionSet, } : {}), - ...(args.modelAddTimestampFields + ...('modelAddTimestampFields' in args ? { addTimestampFields: args.modelAddTimestampFields } : {}), - ...(args.modelHandleListNullabilityTransparently + ...('modelHandleListNullabilityTransparently' in args ? { handleListNullabilityTransparently: args.modelHandleListNullabilityTransparently, @@ -160,12 +160,13 @@ export class GenerateGraphqlClientCodeCommand const format = args.format ?? ('graphql-codegen' as unknown as any); const formatParams = this.getFormatParams(format, args); - await this.apiCodeGenerator.generateAPICodeToFile({ - backendIdentifier, - out, + const result = await this.generateApiCodeAdapter.invokeGenerateApiCode({ + ...backendIdentifier, format, ...formatParams, }); + + await result.writeToDirectory(out); }; /** @@ -230,31 +231,40 @@ export class GenerateGraphqlClientCodeCommand choices: typesTargetChoice, }) .option('modelGenerateIndexRules', { + description: 'Adds key/index details to iOS models', type: 'boolean', array: false, hidden: true, }) .option('modelEmitAuthProvider', { + description: 'Adds auth provider details to iOS models', type: 'boolean', array: false, hidden: true, }) .option('modelRespectPrimaryKeyAttributesOnConnectionField', { + description: + 'If enabled, datastore queries will respect the primary + sort key fields, rather than a defaut id field', type: 'boolean', array: false, hidden: true, }) .option('modelGenerateModelsForLazyLoadAndCustomSelectionSet', { + description: + 'Generates lazy model type definitions, required or amplify-js v5+', type: 'boolean', array: false, hidden: true, }) .option('modelAddTimestampFields', { + description: 'Add read-only timestamp fields in modelgen.', type: 'boolean', array: false, hidden: true, }) .option('modelHandleListNullabilityTransparently', { + description: + 'Configure the nullability of the List and List components in Datastore Models generation', type: 'boolean', array: false, hidden: true, @@ -267,11 +277,15 @@ export class GenerateGraphqlClientCodeCommand hidden: true, }) .option('statementTypenameIntrospection', { + description: + 'Determines whether to include default __typename for all generated statements', type: 'boolean', array: false, hidden: true, }) .option('typeMultipleSwiftFiles', { + description: + 'Determines whether or not to generate a single API.swift, or multiple per-model swift files.', type: 'boolean', array: false, hidden: true, diff --git a/packages/cli/src/commands/generate/graphql-client-code/mock_code_generator.ts b/packages/cli/src/commands/generate/graphql-client-code/mock_code_generator.ts deleted file mode 100644 index f7747b5ee7..0000000000 --- a/packages/cli/src/commands/generate/graphql-client-code/mock_code_generator.ts +++ /dev/null @@ -1,110 +0,0 @@ -import { BackendIdentifier } from '@aws-amplify/deployed-backend-client'; -import path from 'path'; -import fs from 'fs'; -import { AwsCredentialIdentityProvider } from '@aws-sdk/types'; - -export type GeneratedOutput = { [filename: string]: string }; - -export type GenerateModelsOptions = { - format: 'modelgen'; - modelTarget: 'java' | 'swift' | 'javascript' | 'typescript' | 'dart'; - generateIndexRules?: boolean; - emitAuthProvider?: boolean; - respectPrimaryKeyAttributesOnConnectionField?: boolean; - generateModelsForLazyLoadAndCustomSelectionSet?: boolean; - addTimestampFields?: boolean; - handleListNullabilityTransparently?: boolean; -}; - -export type GenerateGraphqlCodegenOptions = { - format: 'graphql-codegen'; - statementTarget: 'javascript' | 'graphql' | 'flow' | 'typescript' | 'angular'; - maxDepth?: number; - typenameIntrospection?: boolean; - typeTarget?: - | 'json' - | 'swift' - | 'typescript' - | 'flow' - | 'scala' - | 'flow-modern' - | 'angular'; - multipleSwiftFiles?: boolean; -}; - -export type GenerateIntrospectionOptions = { - format: 'introspection'; -}; - -export type GenerateOptions = - | GenerateGraphqlCodegenOptions - | GenerateModelsOptions - | GenerateIntrospectionOptions; - -export type GenerateAPICodeProps = GenerateOptions & { - backendIdentifier: BackendIdentifier; -}; - -export type GenerateAPICodeToFileProps = GenerateAPICodeProps & { - out: string; -}; - -/** - * API Code Generator mock. - */ -export class ApiCodeGenerator { - /** - * Constructor - * @param awsCredentialProvider credential provider - */ - constructor( - private readonly awsCredentialProvider: AwsCredentialIdentityProvider - ) {} - - /** - * Mock generateApiCode command. - */ - generateAPICode = async ( - props: GenerateAPICodeProps - ): Promise => { - switch (props.format) { - case 'graphql-codegen': - return { - [path.join('src', 'graphql', 'mutations.js')]: 'type Mutations {}', - [path.join('src', 'graphql', 'queries.js')]: 'type Queries {}', - [path.join('src', 'graphql', 'subscriptions.js')]: - 'type Subscriptions {}', - }; - case 'modelgen': - return { - [path.join('src', 'models', 'index.js')]: 'export me', - [path.join('src', 'models', 'models.js')]: 'im a models', - }; - case 'introspection': - return { - 'model-introspection-schema.json': JSON.stringify( - { version: 1, models: [], nonModels: [] }, - null, - 4 - ), - }; - } - }; - - /** - * Generates the platform-specific graphql client code for a given backend, and write the outputs to the specified target. - */ - generateAPICodeToFile = async ( - props: GenerateAPICodeToFileProps - ): Promise => { - const { out, ...rest } = props; - - const generatedCode = await this.generateAPICode({ ...rest }); - - Object.entries(generatedCode).forEach(([filePathSuffix, fileContents]) => { - const filePath = path.join(out, filePathSuffix); - fs.mkdirSync(path.dirname(filePath), { recursive: true }); - fs.writeFileSync(filePath, fileContents); - }); - }; -} diff --git a/packages/cli/tsconfig.json b/packages/cli/tsconfig.json index ca7ebacb55..ea291bde69 100644 --- a/packages/cli/tsconfig.json +++ b/packages/cli/tsconfig.json @@ -4,6 +4,7 @@ "references": [ { "path": "../client-config" }, { "path": "../deployed-backend-client" }, + { "path": "../model-generator" }, { "path": "../sandbox" } ] }