From cbcf8ab32b8d3dca0a2adebe8cba274e38152bd3 Mon Sep 17 00:00:00 2001 From: Tom Gasson Date: Wed, 4 Oct 2023 13:38:13 +1100 Subject: [PATCH 1/4] Atlassian: Output query text as files --- compiler/Cargo.lock | 27 ++++++++++ compiler/crates/persisted-mocks/Cargo.toml | 25 ++++++++++ compiler/crates/persisted-mocks/README.md | 8 +++ compiler/crates/persisted-mocks/src/lib.rs | 57 ++++++++++++++++++++++ compiler/crates/relay-bin/Cargo.toml | 5 ++ compiler/crates/relay-bin/src/main.rs | 3 ++ 6 files changed, 125 insertions(+) create mode 100644 compiler/crates/persisted-mocks/Cargo.toml create mode 100644 compiler/crates/persisted-mocks/README.md create mode 100644 compiler/crates/persisted-mocks/src/lib.rs diff --git a/compiler/Cargo.lock b/compiler/Cargo.lock index ff3a73f2f4f98..92cbe398c7a9d 100644 --- a/compiler/Cargo.lock +++ b/compiler/Cargo.lock @@ -1428,6 +1428,28 @@ dependencies = [ "url", ] +[[package]] +name = "persisted-mocks" +version = "0.0.0" +dependencies = [ + "clap", + "common", + "intern", + "log", + "relay-codegen", + "relay-compiler", + "relay-lsp", + "relay-transforms", + "schema", + "schema-documentation", + "serde", + "serde_json", + "signedsource", + "simplelog", + "thiserror", + "tokio", +] + [[package]] name = "pin-project-lite" version = "0.2.10" @@ -1617,10 +1639,15 @@ dependencies = [ "common", "intern", "log", + "persisted-mocks", + "relay-codegen", "relay-compiler", "relay-lsp", "schema", "schema-documentation", + "serde", + "serde_json", + "signedsource", "simplelog", "thiserror", "tokio", diff --git a/compiler/crates/persisted-mocks/Cargo.toml b/compiler/crates/persisted-mocks/Cargo.toml new file mode 100644 index 0000000000000..3a40f3153d3d6 --- /dev/null +++ b/compiler/crates/persisted-mocks/Cargo.toml @@ -0,0 +1,25 @@ +# @generated by autocargo from //relay/oss/crates/relay-docblock:[relay-docblock,relay-docblock_parse_test,relay-to_schema_test] +[package] +name = "persisted-mocks" +version = "0.0.0" +authors = ["Atlassian"] +edition = "2021" +license = "MIT" + +[dependencies] +clap = { version = "3.2.25", features = ["derive", "env", "regex", "unicode", "wrap_help"] } +common = { path = "../common" } +intern = { path = "../intern" } +log = { version = "0.4.17", features = ["kv_unstable", "kv_unstable_std"] } +relay-compiler = { path = "../relay-compiler" } +relay-codegen = { path = "../relay-codegen" } +relay-transforms = { path = "../relay-transforms" } +signedsource = {path = "../signedsource"} +serde_json = { version = "1.0.100", features = ["float_roundtrip", "unbounded_depth"] } +serde = { version = "1.0.185", features = ["derive", "rc"] } +relay-lsp = { path = "../relay-lsp" } +schema = { path = "../schema" } +schema-documentation = { path = "../schema-documentation" } +simplelog = "0.10.0" +thiserror = "1.0.43" +tokio = { version = "1.29.1", features = ["full", "test-util", "tracing"] } diff --git a/compiler/crates/persisted-mocks/README.md b/compiler/crates/persisted-mocks/README.md new file mode 100644 index 0000000000000..6479fc917ae6d --- /dev/null +++ b/compiler/crates/persisted-mocks/README.md @@ -0,0 +1,8 @@ +# relay_docblock + +Extract and valdiate Relay-specific information from Docblocks. + +## Development + +Note that snapshots can be updated by running tests with the +`UPDATE_SNAPSHOTS=1` environment variable set. diff --git a/compiler/crates/persisted-mocks/src/lib.rs b/compiler/crates/persisted-mocks/src/lib.rs new file mode 100644 index 0000000000000..f34563a184629 --- /dev/null +++ b/compiler/crates/persisted-mocks/src/lib.rs @@ -0,0 +1,57 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +use relay_codegen::QueryID; +use relay_compiler::Artifact; +use relay_compiler::ArtifactContent; +use relay_compiler::ProjectConfig; +use relay_transforms::Programs; +use schema::SDLSchema; + +#[derive(serde::Serialize)] +struct QueryForMock { + signature: &'static str, + operation: String, +} + +pub fn generate_persisted_mocks( + project_config: &ProjectConfig, + _schema: &SDLSchema, + _programs: &Programs, + artifacts: &[Artifact], +) -> Vec { + let mut extra = vec![]; + if let Some(folder) = &project_config.extra_artifacts_output { + for artifact in artifacts { + if let ArtifactContent::Operation { + text: Some(text), + id_and_text_hash: Some(QueryID::Persisted { id, .. }), + .. + } = &artifact.content + { + let full_path = folder.join(format!("{id}.json")); + let query = QueryForMock { + signature: signedsource::SIGNING_TOKEN, + operation: text.clone(), + }; + let str = serde_json::to_string_pretty(&query).unwrap(); + let str = signedsource::sign_file(str.as_str()); + + let new_artifact = Artifact { + content: ArtifactContent::Generic { + content: str.as_bytes().to_vec(), + }, + artifact_source_keys: artifact.artifact_source_keys.clone(), + path: full_path, + source_file: artifact.source_file, + }; + extra.push(new_artifact); + } + } + } + extra +} diff --git a/compiler/crates/relay-bin/Cargo.toml b/compiler/crates/relay-bin/Cargo.toml index 64143a4d79644..b6a552055e404 100644 --- a/compiler/crates/relay-bin/Cargo.toml +++ b/compiler/crates/relay-bin/Cargo.toml @@ -12,7 +12,12 @@ common = { path = "../common" } intern = { path = "../intern" } log = { version = "0.4.17", features = ["kv_unstable", "kv_unstable_std"] } relay-compiler = { path = "../relay-compiler" } +relay-codegen = { path = "../relay-codegen" } +signedsource = {path = "../signedsource"} +serde_json = { version = "1.0.100", features = ["float_roundtrip", "unbounded_depth"] } +serde = { version = "1.0.185", features = ["derive", "rc"] } relay-lsp = { path = "../relay-lsp" } +persisted-mocks = { path = "../persisted-mocks" } schema = { path = "../schema" } schema-documentation = { path = "../schema-documentation" } simplelog = "0.10.0" diff --git a/compiler/crates/relay-bin/src/main.rs b/compiler/crates/relay-bin/src/main.rs index f832244070dd7..f8c930ce1a4f6 100644 --- a/compiler/crates/relay-bin/src/main.rs +++ b/compiler/crates/relay-bin/src/main.rs @@ -283,6 +283,9 @@ async fn handle_compiler_command(command: CompileCommand) -> Result<(), Error> { ) })); + // Atlassian: Output id -> query text for testing / mocking usage + config.generate_extra_artifacts = Some(Box::new(persisted_mocks::generate_persisted_mocks)); + config.file_source_config = if should_use_watchman() { FileSourceKind::Watchman } else { From 349b3c90023e316e551c38f27e4c89aeba82e222 Mon Sep 17 00:00:00 2001 From: yamadapc Date: Wed, 27 Jul 2022 14:28:31 +1000 Subject: [PATCH 2/4] Update with publishing from main-atl This commit sets-up publishing into an internal Atlassian testing package set. All packages are prefixed with `atl-...` --- .github/workflows/ci.yml | 9 +++++---- gulpfile.js | 30 ++++++++++++++++++++++++++++-- 2 files changed, 33 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 93a548e599ea3..24229901ff9e3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -217,7 +217,7 @@ jobs: main-release: name: Publish to NPM runs-on: ubuntu-latest - if: github.event_name == 'push' && github.repository == 'facebook/relay' + if: github.event_name == 'push' && github.repository == 'atlassian-forks/relay' needs: [js-tests, js-lint, typecheck, build-tests, build-compiler] steps: - uses: actions/checkout@v2 @@ -262,7 +262,7 @@ jobs: chmod +x macos-arm64/relay - name: Build latest (main) version - if: github.ref == 'refs/heads/main' + if: github.ref == 'refs/heads/main-atl' run: yarn gulp mainrelease env: RELEASE_COMMIT_SHA: ${{ github.sha }} @@ -272,11 +272,12 @@ jobs: run: yarn gulp release - name: Publish to npm - if: github.ref == 'refs/heads/main' || github.ref_type == 'tag' && startsWith(github.ref_name, 'v') + if: github.ref == 'refs/heads/main-atl' || github.ref_type == 'tag' && startsWith(github.ref_name, 'v') run: | for pkg in dist/*; do npm publish "$pkg" ${TAG} done env: - TAG: ${{ github.ref == 'refs/heads/main' && '--tag=main' || ((contains(github.ref_name, '-rc.') && '--tag=dev') || '' )}} + TAG: ${{ github.ref == 'refs/heads/main-atl' && '--tag=main-atl' || ((contains(github.ref_name, '-rc.') && '--tag=dev') || '' )}} + NPM_REGISTRY: ${{secrets.REGISTRY}} NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} diff --git a/gulpfile.js b/gulpfile.js index 2db8032804755..dc4f420e2fa8d 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -272,11 +272,24 @@ builds.forEach(build => { .pipe(gulp.dest(path.join(DIST, build.package, 'lib'))); }, function copyPackageJSON() { - return gulp + const stream = gulp .src(['package.json'], { cwd: path.join(PACKAGES, build.package), }) .pipe(gulp.dest(path.join(DIST, build.package))); + stream.on('end', () => { + const pkgJson = JSON.parse( + fs + .readFileSync(path.join(DIST, build.package, 'package.json')) + .toString(), + ); + pkgJson.name = 'atl-' + pkgJson.name; + fs.writeFileSync( + path.join(DIST, build.package, 'package.json'), + JSON.stringify(pkgJson, null, 2), + ); + }); + return stream; }, ); }); @@ -362,11 +375,24 @@ const relayCompiler = gulp.parallel( .pipe(gulp.dest(path.join(DIST, 'relay-compiler'))); }, function copyPackageFiles() { - return gulp + const stream = gulp .src(['README.md', 'package.json', 'cli.js', 'index.js'], { cwd: path.join(PACKAGES, 'relay-compiler'), }) .pipe(gulp.dest(path.join(DIST, 'relay-compiler'))); + stream.on('end', () => { + const pkg = JSON.parse( + fs + .readFileSync(path.join(DIST, 'relay-compiler', 'package.json')) + .toString(), + ); + pkg.name = 'atl-' + pkg.name; + fs.writeFileSync( + path.join(DIST, 'relay-compiler', 'package.json'), + JSON.stringify(pkg, null, 2), + ); + }); + return stream; }, function copyCompilerBins() { return gulp From a41dab2f7a73ab4979fd7028b123f89e6fbb1c71 Mon Sep 17 00:00:00 2001 From: Tom Gasson Date: Wed, 11 Oct 2023 11:19:36 +1100 Subject: [PATCH 3/4] Atlassian: Use name instead of id for mock file --- compiler/crates/persisted-mocks/src/lib.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/compiler/crates/persisted-mocks/src/lib.rs b/compiler/crates/persisted-mocks/src/lib.rs index f34563a184629..cc8608e96cd6a 100644 --- a/compiler/crates/persisted-mocks/src/lib.rs +++ b/compiler/crates/persisted-mocks/src/lib.rs @@ -15,6 +15,7 @@ use schema::SDLSchema; #[derive(serde::Serialize)] struct QueryForMock { signature: &'static str, + id: String, operation: String, } @@ -30,12 +31,15 @@ pub fn generate_persisted_mocks( if let ArtifactContent::Operation { text: Some(text), id_and_text_hash: Some(QueryID::Persisted { id, .. }), + normalization_operation, .. } = &artifact.content { - let full_path = folder.join(format!("{id}.json")); + let name = normalization_operation.name.item; + let full_path = folder.join(format!("{name}.json")); let query = QueryForMock { signature: signedsource::SIGNING_TOKEN, + id: id.clone(), operation: text.clone(), }; let str = serde_json::to_string_pretty(&query).unwrap(); From 2d817fa20a900a235f37479a2fbdf5f40e9d6564 Mon Sep 17 00:00:00 2001 From: Kyle Painter Date: Thu, 7 Mar 2024 09:48:08 +1100 Subject: [PATCH 4/4] Use task scheduler to process optimistic responses and execute error rollback. --- .../relay-runtime/store/OperationExecutor.js | 30 +-- ...yModernEnvironment-ExecuteMutation-test.js | 203 ++++++++++++++++++ 2 files changed, 220 insertions(+), 13 deletions(-) diff --git a/packages/relay-runtime/store/OperationExecutor.js b/packages/relay-runtime/store/OperationExecutor.js index 805a4816a6f19..588ecbc30328f 100644 --- a/packages/relay-runtime/store/OperationExecutor.js +++ b/packages/relay-runtime/store/OperationExecutor.js @@ -234,13 +234,15 @@ class Executor { }); if (optimisticConfig != null) { - this._processOptimisticResponse( - optimisticConfig.response != null - ? {data: optimisticConfig.response} - : null, - optimisticConfig.updater, - false, - ); + this._schedule(() => { + this._processOptimisticResponse( + optimisticConfig.response != null + ? {data: optimisticConfig.response} + : null, + optimisticConfig.updater, + false, + ); + }); } } @@ -342,12 +344,14 @@ class Executor { } _error(error: Error): void { - this.cancel(); - this._sink.error(error); - this._log({ - name: 'execute.error', - executeId: this._executeId, - error, + this._schedule(() => { + this.cancel(); + this._sink.error(error); + this._log({ + name: 'execute.error', + executeId: this._executeId, + error, + }); }); } diff --git a/packages/relay-runtime/store/__tests__/RelayModernEnvironment-ExecuteMutation-test.js b/packages/relay-runtime/store/__tests__/RelayModernEnvironment-ExecuteMutation-test.js index 2542b8cc1fa9d..aba228b580953 100644 --- a/packages/relay-runtime/store/__tests__/RelayModernEnvironment-ExecuteMutation-test.js +++ b/packages/relay-runtime/store/__tests__/RelayModernEnvironment-ExecuteMutation-test.js @@ -546,6 +546,209 @@ describe.each(['RelayModernEnvironment', 'MultiActorEnvironment'])( // and thus the snapshot has missing data expect(callback.mock.calls[0][0].isMissingData).toEqual(true); }); + + describe('when using a scheduler', () => { + let taskID; + let tasks; + let scheduler; + let runTask; + + beforeEach(() => { + taskID = 0; + tasks = new Map void>(); + scheduler = { + cancel: (id: string) => { + tasks.delete(id); + }, + schedule: (task: () => void) => { + const id = String(taskID++); + tasks.set(id, task); + return id; + }, + }; + runTask = () => { + for (const [id, task] of tasks) { + tasks.delete(id); + task(); + break; + } + }; + + const multiActorEnvironment = new MultiActorEnvironment({ + // $FlowFixMe[invalid-tuple-arity] Error found while enabling LTI on this file + createNetworkForActor: _actorID => RelayNetwork.create(fetch), + createStoreForActor: _actorID => store, + scheduler, + }); + environment = + environmentType === 'MultiActorEnvironment' + ? multiActorEnvironment.forActor(getActorIdentifier('actor:1234')) + : new RelayModernEnvironment({ + // $FlowFixMe[invalid-tuple-arity] Error found while enabling LTI on this file + network: RelayNetwork.create(fetch), + store, + scheduler, + }); + }); + + it('applies the optimistic update', () => { + const selector = createReaderSelector( + CommentFragment, + commentID, + {}, + queryOperation.request, + ); + const snapshot = environment.lookup(selector); + const callback = jest.fn<[Snapshot], void>(); + environment.subscribe(snapshot, callback); + + environment + .executeMutation({ + operation, + optimisticUpdater: _store => { + const comment = _store.create(commentID, 'Comment'); + comment.setValue(commentID, 'id'); + const body = _store.create(commentID + '.text', 'Text'); + comment.setLinkedRecord(body, 'body'); + body.setValue('Give Relay', 'text'); + }, + }) + .subscribe(callbacks); + + // Verify task was scheduled and run it + expect(tasks.size).toBe(1); + runTask(); + + // Update is applied after scheduler runs scheduled task + expect(complete).not.toBeCalled(); + expect(error).not.toBeCalled(); + expect(callback.mock.calls.length).toBe(1); + expect(callback.mock.calls[0][0].data).toEqual({ + id: commentID, + body: { + text: 'Give Relay', + }, + }); + }); + + it('reverts the optimistic update and commits the server payload', () => { + const selector = createReaderSelector( + CommentFragment, + commentID, + {}, + queryOperation.request, + ); + const snapshot = environment.lookup(selector); + const callback = jest.fn<[Snapshot], void>(); + environment.subscribe(snapshot, callback); + + environment + .executeMutation({ + operation, + optimisticUpdater: _store => { + const comment = _store.create(commentID, 'Comment'); + comment.setValue(commentID, 'id'); + const body = _store.create(commentID + '.text', 'Text'); + comment.setLinkedRecord(body, 'body'); + body.setValue('Give Relay', 'text'); + }, + }) + .subscribe(callbacks); + + // Verify optimistic update task was scheduled and run it + expect(tasks.size).toBe(1); + runTask(); + + // Update is applied after scheduler runs scheduled task + expect(callback.mock.calls.length).toBe(1); + expect(callback.mock.calls[0][0].data).toEqual({ + id: commentID, + body: { + text: 'Give Relay', + }, + }); + + callback.mockClear(); + subject.next({ + data: { + commentCreate: { + comment: { + id: commentID, + body: { + text: 'Gave Relay', + }, + }, + }, + }, + }); + subject.complete(); + + // Verify update task was scheduled and run it + expect(tasks.size).toBe(1); + runTask(); + + // Update is applied after scheduler runs scheduled task + expect(complete).toBeCalled(); + expect(error).not.toBeCalled(); + expect(callback.mock.calls.length).toBe(1); + expect(callback.mock.calls[0][0].data).toEqual({ + id: commentID, + body: { + text: 'Gave Relay', + }, + }); + }); + + it('reverts the optimistic update if the fetch is rejected', () => { + const selector = createReaderSelector( + CommentFragment, + commentID, + {}, + queryOperation.request, + ); + const snapshot = environment.lookup(selector); + const callback = jest.fn<[Snapshot], void>(); + environment.subscribe(snapshot, callback); + + environment + .executeMutation({ + operation, + optimisticUpdater: _store => { + const comment = _store.create(commentID, 'Comment'); + comment.setValue(commentID, 'id'); + const body = _store.create(commentID + '.text', 'Text'); + comment.setLinkedRecord(body, 'body'); + body.setValue('Give Relay', 'text'); + }, + }) + .subscribe(callbacks); + + // Verify optimistic update task was scheduled and run it + expect(tasks.size).toBe(1); + runTask(); + + // Update is applied after scheduler runs scheduled task + expect(callback.mock.calls.length).toBe(1); + expect(callback.mock.calls[0][0].data).toEqual({ + id: commentID, + body: { + text: 'Give Relay', + }, + }); + + callback.mockClear(); + subject.error(new Error('wtf')); + + // Verify rollback task was scheduled and run it + expect(tasks.size).toBe(1); + runTask(); + + expect(complete).not.toBeCalled(); + expect(error).toBeCalled(); + expect(callback.mock.calls.length).toBe(1); + expect(callback.mock.calls[0][0].data).toEqual(undefined); + }); + }); }); }, );