From 9685dfa682d088ea6c6e695059be11d14cc643d0 Mon Sep 17 00:00:00 2001 From: Krystan HuffMenne Date: Tue, 11 Feb 2025 17:45:51 -0500 Subject: [PATCH] chore: generate latest delta --- .github/actions/prepare-build/action.yml | 2 +- .github/actions/setup/action.yml | 2 +- .github/renovate.json | 2 +- .github/workflows/asset-size-check.yml | 4 +- .github/workflows/compat-tests.yml | 41 +- .github/workflows/deprecations-check.yml | 8 +- .../workflows/docs-and-blueprint-tests.yml | 2 +- .github/workflows/main.yml | 10 +- .github/workflows/perf-check.yml | 18 +- .github/workflows/perf-over-release.yml | 18 +- .github/workflows/release_promote-lts.yml | 2 +- .github/workflows/release_publish-beta.yml | 2 +- .github/workflows/release_publish-canary.yml | 2 +- .github/workflows/release_publish-lts.yml | 2 +- .github/workflows/release_publish-stable.yml | 2 +- CHANGELOG.md | 497 +-- config/package.json | 5 +- config/rollup/external.js | 2 +- config/vite/fix-module-output-plugin.js | 3 +- config/vite/keep-assets.js | 3 +- guides/community-resources.md | 1 - guides/index.md | 1 - guides/requests/examples/0-basic-usage.md | 3 +- guides/requests/index.md | 23 +- guides/typescript/0-installation.md | 59 +- guides/typescript/1-configuration.md | 28 +- guides/typescript/index.md | 6 +- package.json | 11 +- .../-ember-data/app/adapters/-json-api.js | 1 + .../app/initializers/ember-data.js | 9 +- .../app/instance-initializers/ember-data.js | 5 + .../-ember-data/app/serializers/-default.js | 1 + .../-ember-data/app/serializers/-json-api.js | 1 + packages/-ember-data/app/serializers/-rest.js | 1 + packages/-ember-data/app/services/store.js | 3 +- .../-ember-data/app/transforms/boolean.js | 4 +- packages/-ember-data/app/transforms/date.js | 4 +- packages/-ember-data/app/transforms/number.js | 4 +- packages/-ember-data/app/transforms/string.js | 4 +- packages/-ember-data/package.json | 8 +- packages/-ember-data/src/-private/index.ts | 24 +- packages/-ember-data/src/adapter.ts | 4 +- packages/-ember-data/src/adapters/errors.ts | 6 +- packages/-ember-data/src/adapters/json-api.ts | 4 +- packages/-ember-data/src/adapters/rest.ts | 4 +- packages/-ember-data/src/attr.ts | 24 +- packages/-ember-data/src/index.ts | 3 +- packages/-ember-data/src/model.ts | 24 +- packages/-ember-data/src/relationships.ts | 4 +- packages/-ember-data/src/serializer.ts | 4 +- .../src/serializers/embedded-records-mixin.ts | 4 +- .../-ember-data/src/serializers/json-api.ts | 4 +- packages/-ember-data/src/serializers/json.ts | 4 +- packages/-ember-data/src/serializers/rest.ts | 4 +- packages/-ember-data/src/setup-container.ts | 24 +- packages/-ember-data/src/store.ts | 6 + packages/-ember-data/src/transform.ts | 4 +- packages/active-record/package.json | 4 +- packages/adapter/README.md | 10 +- packages/adapter/package.json | 6 +- packages/adapter/src/error.js | 161 + packages/build-config/package.json | 8 +- .../src/-private/utils/deprecations.ts | 8 +- packages/build-config/src/debugging.ts | 8 - .../build-config/src/deprecation-versions.ts | 684 +++- packages/build-config/src/deprecations.ts | 24 +- packages/core-types/package.json | 4 +- packages/core-types/src/identifier.ts | 8 +- packages/core-types/src/request.ts | 2 - packages/core-types/src/schema/fields.ts | 68 - packages/core-types/src/spec/document.ts | 4 +- packages/debug/package.json | 6 +- packages/diagnostic/README.md | 2 +- packages/diagnostic/package.json | 9 +- packages/graph/eslint.config.mjs | 3 +- packages/graph/package.json | 4 +- packages/graph/src/-private/-diff.ts | 36 +- .../graph/src/-private/-edge-definition.ts | 31 +- packages/graph/src/-private/-utils.ts | 14 +- packages/graph/src/-private/coerce-id.ts | 4 +- .../-private/debug/assert-polymorphic-type.ts | 226 +- .../graph/src/-private/edges/collection.ts | 20 +- packages/graph/src/-private/edges/resource.ts | 3 - packages/graph/src/-private/graph.ts | 10 +- .../operations/add-to-related-records.ts | 9 +- .../-private/operations/merge-identifier.ts | 4 +- .../operations/remove-from-related-records.ts | 2 +- .../operations/replace-related-record.ts | 17 +- .../operations/replace-related-records.ts | 89 +- .../operations/update-relationship.ts | 2 +- packages/graph/vite.config.mjs | 1 + packages/holodeck/README.md | 8 +- packages/holodeck/package.json | 5 +- packages/json-api/package.json | 4 +- packages/json-api/src/-private/cache.ts | 166 +- packages/legacy-compat/package.json | 4 +- packages/legacy-compat/src/builders/utils.ts | 4 +- packages/legacy-compat/src/index.ts | 34 +- .../legacy-network-handler/fetch-manager.ts | 23 +- .../legacy-data-fetch.ts | 83 +- .../snapshot-record-array.ts | 31 + .../src/legacy-network-handler/snapshot.ts | 33 +- .../src/legacy-network-handler/utils.ts | 57 + packages/model/eslint.config.mjs | 13 +- packages/model/package.json | 6 +- packages/model/src/-private/belongs-to.ts | 139 +- .../-private/debug/assert-polymorphic-type.ts | 84 +- .../src/-private/deprecated-promise-proxy.ts | 73 + packages/model/src/-private/has-many.ts | 91 +- .../-private/legacy-relationships-support.ts | 90 +- packages/model/src/-private/many-array.ts | 40 +- packages/model/src/-private/model-methods.ts | 12 + packages/model/src/-private/model.ts | 649 +++- packages/model/src/-private/notify-changes.ts | 47 +- .../model/src/-private/promise-many-array.ts | 204 +- .../src/-private/references/belongs-to.ts | 30 +- .../model/src/-private/references/has-many.ts | 55 +- .../model/src/-private/relationship-meta.ts | 93 + .../model/src/-private/schema-provider.ts | 118 +- packages/model/src/-private/util.ts | 4 +- packages/model/src/migration-support.ts | 7 - packages/model/vite.config.mjs | 1 + packages/request-utils/package.json | 4 +- .../request-utils/src/deprecation-support.ts | 18 +- packages/request-utils/src/index.ts | 5 +- packages/request/README.md | 27 +- packages/request/package.json | 4 +- packages/request/src/-private/manager.ts | 27 +- packages/request/src/-private/types.ts | 5 +- packages/rest/package.json | 4 +- packages/schema-record/package.json | 4 +- packages/schema-record/src/record.ts | 8 - packages/serializer/README.md | 10 +- packages/serializer/package.json | 6 +- packages/store/README.md | 9 +- packages/store/package.json | 6 +- packages/store/src/-private.ts | 35 +- .../src/-private/cache-handler/handler.ts | 2 +- .../src/-private/caches/identifier-cache.ts | 7 +- .../src/-private/caches/instance-cache.ts | 9 +- .../managers/cache-capabilities-manager.ts | 8 +- .../-private/managers/notification-manager.ts | 59 +- .../src/-private/proxies/promise-proxies.ts | 225 ++ .../-private/proxies/promise-proxy-base.d.ts | 65 + .../-private/proxies/promise-proxy-base.js | 9 + .../record-arrays/identifier-array.ts | 542 +++- packages/store/src/-private/store-service.ts | 235 +- .../src/-private/store-service.type-test.ts | 20 - .../store/src/-private/utils/coerce-id.ts | 4 +- .../-private/utils/normalize-model-name.ts | 4 +- .../-types/q/cache-capabilities-manager.ts | 8 +- packages/store/src/index.ts | 8 +- packages/store/vite.config.mjs | 16 +- packages/tracking/README.md | 11 + packages/tracking/package.json | 4 +- .../unpublished-eslint-rules/package.json | 2 +- packages/unpublished-test-infra/package.json | 8 +- .../publish/steps/generate-mirror-tarballs.ts | 7 - .../core/publish/steps/generate-tarballs.ts | 27 - release/strategy.json | 10 +- release/utils/package.ts | 1 - tests/blueprints/package.json | 6 +- tests/builders/package.json | 8 +- tests/docs/fixtures/expected.js | 30 +- tests/docs/package.json | 2 +- .../ember-data__adapter/app/services/store.ts | 7 +- tests/ember-data__adapter/package.json | 8 +- tests/ember-data__graph/ember-cli-build.js | 3 + tests/ember-data__graph/package.json | 10 +- .../graph/diff-preservation-test.ts | 159 - tests/ember-data__graph/tests/test-helper.ts | 1 - tests/ember-data__json-api/package.json | 10 +- tests/ember-data__model/package.json | 10 +- tests/ember-data__request/package.json | 10 +- tests/ember-data__serializer/package.json | 6 +- tests/embroider-basic-compat/package.json | 12 +- tests/fastboot/package.json | 6 +- tests/full-data-asset-size-app/package.json | 4 +- tests/main/ember-cli-build.js | 12 +- tests/main/package.json | 10 +- .../acceptance/relationships/has-many-test.js | 223 +- .../acceptance/tracking-promise-flags-test.js | 69 + .../deprecate-early-static-test.js | 63 + .../deprecations/deprecate-helpers-test.js | 48 + .../deprecate-reopen-class-test.js | 30 + .../deprecations/deprecate-reopen-test.js | 30 + .../cache/spec-cache-errors-test.ts | 12 +- .../cache/spec-cache-state-test.ts | 16 +- .../integration/cache/spec-cache-test.ts | 6 +- tests/main/tests/integration/inverse-test.js | 312 ++ .../adapter-populated-record-array-test.js | 16 + .../records/relationship-changes-test.js | 111 + .../tests/integration/records/save-test.js | 28 +- .../references/autotracking-test.js | 96 +- .../integration/references/belongs-to-test.js | 56 +- .../integration/references/has-many-test.js | 56 +- .../relationships/belongs-to-test.js | 72 +- .../collection/mutating-has-many-test.ts | 425 +++ .../relationships/has-many-test.js | 473 ++- .../inverse-relationship-load-test.js | 2867 ++++++++++++++++- .../inverse-relationships-test.js | 659 +++- .../relationships/one-to-many-test.js | 47 + .../relationships/one-to-one-test.js | 187 +- .../polymorphic-mixins-belongs-to-test.js | 5 +- .../polymorphic-mixins-has-many-test.js | 20 +- .../relationships/promise-many-array-test.js | 154 + .../relationships/rollback-test.ts | 37 +- tests/main/tests/integration/snapshot-test.js | 40 + .../integration/store/adapter-for-test.js | 55 + .../tests/integration/store/query-test.js | 49 + tests/main/tests/unit/adapter-errors-test.js | 80 + .../custom-class-model-test.ts | 4 +- .../tests/unit/model/relationships-test.js | 33 + .../unit/model/relationships/has-many-test.js | 707 ++-- .../adapter-populated-record-array-test.js | 12 + .../unit/record-arrays/record-array-test.js | 227 ++ .../tests/unit/store/adapter-interop-test.js | 14 + .../tests/unit/store/serializer-for-test.js | 9 + .../unit/system/snapshot-record-array-test.js | 40 + tests/main/tsconfig.json | 5 - .../relationship-materialization-complex.js | 2 - tests/performance/ember-cli-build.js | 17 +- tests/performance/package.json | 15 +- tests/vite-basic-compat/package.json | 10 +- tsconfig.json | 4 +- 225 files changed, 11225 insertions(+), 2647 deletions(-) create mode 100644 packages/-ember-data/app/adapters/-json-api.js create mode 100644 packages/-ember-data/app/instance-initializers/ember-data.js create mode 100644 packages/-ember-data/app/serializers/-default.js create mode 100644 packages/-ember-data/app/serializers/-json-api.js create mode 100644 packages/-ember-data/app/serializers/-rest.js create mode 100644 packages/legacy-compat/src/legacy-network-handler/utils.ts create mode 100644 packages/model/src/-private/deprecated-promise-proxy.ts create mode 100644 packages/model/src/-private/relationship-meta.ts create mode 100644 packages/store/src/-private/proxies/promise-proxies.ts create mode 100644 packages/store/src/-private/proxies/promise-proxy-base.d.ts create mode 100644 packages/store/src/-private/proxies/promise-proxy-base.js create mode 100644 tests/main/tests/acceptance/tracking-promise-flags-test.js create mode 100644 tests/main/tests/deprecations/deprecate-early-static-test.js create mode 100644 tests/main/tests/deprecations/deprecate-helpers-test.js create mode 100644 tests/main/tests/deprecations/deprecate-reopen-class-test.js create mode 100644 tests/main/tests/deprecations/deprecate-reopen-test.js create mode 100644 tests/main/tests/integration/inverse-test.js create mode 100644 tests/main/tests/integration/relationships/collection/mutating-has-many-test.ts create mode 100644 tests/main/tests/integration/relationships/promise-many-array-test.js create mode 100644 tests/main/tests/integration/store/query-test.js diff --git a/.github/actions/prepare-build/action.yml b/.github/actions/prepare-build/action.yml index ab13d7528c5..6a86d914c9e 100644 --- a/.github/actions/prepare-build/action.yml +++ b/.github/actions/prepare-build/action.yml @@ -29,7 +29,7 @@ runs: - if: ${{ steps.restore-ref-artifact.outputs.cache-hit != 'true' }} name: Build ${{ inputs.name }} - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4 with: ref: ${{ inputs.ref }} fetch-depth: 1 diff --git a/.github/actions/setup/action.yml b/.github/actions/setup/action.yml index bf0b8c9c177..075d445f1dc 100644 --- a/.github/actions/setup/action.yml +++ b/.github/actions/setup/action.yml @@ -63,7 +63,7 @@ inputs: runs: using: composite steps: - - uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0 + - uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2 # v4.0.0 - uses: actions/setup-node@v4 with: registry-url: 'https://registry.npmjs.org' diff --git a/.github/renovate.json b/.github/renovate.json index 3676d30e1f4..0d72e8ebd16 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -95,5 +95,5 @@ "assignees": ["@runspired"], "enabled": true }, - "ignorePaths": ["node_modules/**", "**/node_modules/**", "tests/smoke-tests/**"] + "ignorePaths": ["node_modules/**", "**/node_modules/**"] } diff --git a/.github/workflows/asset-size-check.yml b/.github/workflows/asset-size-check.yml index 81c6ae9bd68..026c1c53d43 100644 --- a/.github/workflows/asset-size-check.yml +++ b/.github/workflows/asset-size-check.yml @@ -24,11 +24,11 @@ jobs: if: contains(github.event.pull_request.labels.*.name, 'ci-assetsize') runs-on: ubuntu-latest steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4 with: fetch-depth: 3 - run: git fetch origin main --depth=1 - - uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0 + - uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2 # v4.0.0 - uses: actions/setup-node@v4 with: node-version: 19.x diff --git a/.github/workflows/compat-tests.yml b/.github/workflows/compat-tests.yml index 19620590cd8..4109f4bb655 100644 --- a/.github/workflows/compat-tests.yml +++ b/.github/workflows/compat-tests.yml @@ -20,7 +20,7 @@ jobs: timeout-minutes: 7 runs-on: ubuntu-latest steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4 - uses: ./.github/actions/setup with: restore-broccoli-cache: true @@ -32,7 +32,7 @@ jobs: timeout-minutes: 7 runs-on: ubuntu-latest steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4 - uses: ./.github/actions/setup with: restore-broccoli-cache: true @@ -46,7 +46,7 @@ jobs: timeout-minutes: 7 runs-on: ubuntu-latest steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4 - uses: ./.github/actions/setup with: restore-broccoli-cache: true @@ -58,7 +58,7 @@ jobs: timeout-minutes: 9 runs-on: ubuntu-latest steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4 - uses: ./.github/actions/setup with: repo-token: ${{ secrets.GITHUB_TOKEN }} @@ -74,7 +74,7 @@ jobs: matrix: node-version: [16.x, 18.x] steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4 - uses: ./.github/actions/setup with: node-version: ${{ matrix.node-version }} @@ -83,34 +83,3 @@ jobs: repo-token: ${{ secrets.GITHUB_TOKEN }} - name: Basic Tests run: pnpm test - - smoke-tests: - name: Smoke ${{ matrix.scenario.name }} w/ ${{ matrix.packageManager }} - timeout-minutes: 10 - runs-on: ubuntu-latest - # TODO: - # needs: [embroider, vite] - - strategy: - matrix: - packageManager: - - npm - # - yarn # yarn@1 has not been reliable, if yarn@4 were easy to setup, we could test against that - - pnpm - scenario: - - { dir: "dt-types", name: "DT Types" } - - { dir: "native-types", name: "Native Types" } - - steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - - uses: ./.github/actions/setup - with: - restore-broccoli-cache: true - install: true - repo-token: ${{ secrets.GITHUB_TOKEN }} - - name: "Run a basic smoke test with ${{ matrix.packageManager }} and ${{ matrix.kind }} tagging" - run: | - bun ./tests/smoke-tests/run.ts \ - "${{ matrix.scenario.dir }}" "${{ matrix.packageManager }}" - - diff --git a/.github/workflows/deprecations-check.yml b/.github/workflows/deprecations-check.yml index 67777beb910..1f5d674ecb5 100644 --- a/.github/workflows/deprecations-check.yml +++ b/.github/workflows/deprecations-check.yml @@ -12,8 +12,8 @@ jobs: test-all-deprecations: runs-on: ubuntu-latest steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - - uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0 + - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4 + - uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2 # v4.0.0 - uses: actions/setup-node@v4 with: node-version: 19.x @@ -33,8 +33,8 @@ jobs: scenario: [ember-beta, ember-canary] runs-on: ubuntu-latest steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - - uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0 + - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4 + - uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2 # v4.0.0 - uses: actions/setup-node@v4 with: node-version: 19.x diff --git a/.github/workflows/docs-and-blueprint-tests.yml b/.github/workflows/docs-and-blueprint-tests.yml index 7f004d6fb08..cccb7c87741 100644 --- a/.github/workflows/docs-and-blueprint-tests.yml +++ b/.github/workflows/docs-and-blueprint-tests.yml @@ -20,7 +20,7 @@ jobs: timeout-minutes: 5 runs-on: ubuntu-latest steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4 - uses: ./.github/actions/setup with: install: true diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 8a66fd1cdca..d1f746be123 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -27,7 +27,7 @@ jobs: timeout-minutes: 8 runs-on: ubuntu-latest steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4 - uses: ./.github/actions/setup with: restore-lint-caches: ${{ secrets.ACTIONS_RUNNER_DEBUG != 'true' }} @@ -52,7 +52,7 @@ jobs: timeout-minutes: 20 runs-on: ubuntu-latest steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4 - uses: ./.github/actions/setup with: restore-broccoli-cache: true @@ -93,7 +93,7 @@ jobs: runs-on: ubuntu-latest name: Test ${{matrix.launcher}} steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4 - uses: ./.github/actions/setup with: github-token: ${{ secrets.GH_PACKAGES_ACCESS_TOKEN }} @@ -163,7 +163,7 @@ jobs: scenario: [ember-lts-4.12, ember-lts-4.8, ember-lts-4.4, ember-lts-3.28] runs-on: ubuntu-latest steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4 - uses: ./.github/actions/setup with: restore-broccoli-cache: true @@ -191,7 +191,7 @@ jobs: release: [ember-canary, ember-beta] runs-on: ubuntu-latest steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4 - uses: ./.github/actions/setup with: restore-broccoli-cache: true diff --git a/.github/workflows/perf-check.yml b/.github/workflows/perf-check.yml index 72d7cd3dd25..005c7e05ece 100644 --- a/.github/workflows/perf-check.yml +++ b/.github/workflows/perf-check.yml @@ -25,7 +25,7 @@ jobs: name: 'Performance Checks' runs-on: ubuntu-latest steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4 with: fetch-depth: 3 - run: git fetch origin main --depth=1 @@ -39,21 +39,12 @@ jobs: originSha=$(git rev-parse HEAD^2) echo $originSha > tmp/sha-for-commit.txt git show --format=short --no-patch $originSha - - uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0 + - uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2 # v4.0.0 - uses: actions/setup-node@v4 with: - registry-url: 'https://registry.npmjs.org' - node-version-file: 'package.json' + node-version: 19.x cache: 'pnpm' - - uses: oven-sh/setup-bun@v1 - with: - bun-version: latest - - name: Get Browser Flags - id: browser-flags - run: | - BROWSER_FLAGS=$(node ./scripts/perf-tracking/browser-flags.mjs) - echo "BROWSER_FLAGS=$BROWSER_FLAGS" >> $GITHUB_OUTPUT - - uses: tracerbench/tracerbench-compare-action@35f3ab44b512fd2caffbe81adf875ab47272b5b5 + - uses: tracerbench/tracerbench-compare-action@master with: experiment-build-command: pnpm install && pnpm --filter performance-test-app exec ember build -e production --output-path dist-experiment --suppress-sizes experiment-serve-command: pnpm --filter performance-test-app exec ember s --path dist-experiment --port 4201 @@ -62,7 +53,6 @@ jobs: control-sha: origin/main sample-timeout: 60 use-pnpm: true - browser-args: ${{ steps.browser-flags.outputs.BROWSER_FLAGS }} scenarios: | { "basic-record-materialization": { diff --git a/.github/workflows/perf-over-release.yml b/.github/workflows/perf-over-release.yml index 10883e21622..9dffa251153 100644 --- a/.github/workflows/perf-over-release.yml +++ b/.github/workflows/perf-over-release.yml @@ -25,7 +25,7 @@ jobs: name: 'Performance Check Against Release' runs-on: ubuntu-latest steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4 with: fetch-depth: 3 - run: git fetch origin release --depth=1 @@ -39,21 +39,12 @@ jobs: originSha=$(git rev-parse HEAD^2) echo $originSha > tmp/sha-for-commit.txt git show --format=short --no-patch $originSha - - uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0 + - uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2 # v4.0.0 - uses: actions/setup-node@v4 with: - registry-url: 'https://registry.npmjs.org' - node-version-file: 'package.json' + node-version: 19.x cache: 'pnpm' - - uses: oven-sh/setup-bun@v1 - with: - bun-version: latest - - name: Get Browser Flags - id: browser-flags - run: | - BROWSER_FLAGS=$(node ./scripts/perf-tracking/browser-flags.mjs) - echo "BROWSER_FLAGS=$BROWSER_FLAGS" >> $GITHUB_OUTPUT - - uses: tracerbench/tracerbench-compare-action@35f3ab44b512fd2caffbe81adf875ab47272b5b5 + - uses: tracerbench/tracerbench-compare-action@master with: experiment-build-command: pnpm install && pnpm --filter performance-test-app exec ember build -e production --output-path dist-experiment --suppress-sizes experiment-serve-command: pnpm --filter performance-test-app exec ember s --path dist-experiment --port 4201 @@ -61,7 +52,6 @@ jobs: control-serve-command: pnpm --filter performance-test-app exec ember s --path dist-control sample-timeout: 60 use-pnpm: true - browser-args: ${{ steps.browser-flags.outputs.BROWSER_FLAGS }} scenarios: | { "basic-record-materialization": { diff --git a/.github/workflows/release_promote-lts.yml b/.github/workflows/release_promote-lts.yml index f1f84329835..6cb8aa6f8ae 100644 --- a/.github/workflows/release_promote-lts.yml +++ b/.github/workflows/release_promote-lts.yml @@ -42,7 +42,7 @@ jobs: run: | echo "Releases may only be performed from the main branch." exit 1 - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4 with: fetch-depth: 1 fetch-tags: true diff --git a/.github/workflows/release_publish-beta.yml b/.github/workflows/release_publish-beta.yml index f8e1e7debf3..6d47c59c56e 100644 --- a/.github/workflows/release_publish-beta.yml +++ b/.github/workflows/release_publish-beta.yml @@ -75,7 +75,7 @@ jobs: else echo "DESIRED_BRANCH=beta" >> "$GITHUB_OUTPUT" fi - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4 with: fetch-tags: true show-progress: false diff --git a/.github/workflows/release_publish-canary.yml b/.github/workflows/release_publish-canary.yml index a0a47bdd42a..5ff92de05b3 100644 --- a/.github/workflows/release_publish-canary.yml +++ b/.github/workflows/release_publish-canary.yml @@ -64,7 +64,7 @@ jobs: else echo "DESIRED_BRANCH=main" >> "$GITHUB_OUTPUT" fi - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4 with: fetch-depth: 1 fetch-tags: true diff --git a/.github/workflows/release_publish-lts.yml b/.github/workflows/release_publish-lts.yml index dd6471b56e9..07f79cd53ed 100644 --- a/.github/workflows/release_publish-lts.yml +++ b/.github/workflows/release_publish-lts.yml @@ -44,7 +44,7 @@ jobs: run: | echo "Releases may only be performed from the main branch." exit 1 - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4 with: fetch-tags: true show-progress: false diff --git a/.github/workflows/release_publish-stable.yml b/.github/workflows/release_publish-stable.yml index ee6b9c06d8d..33ae0807183 100644 --- a/.github/workflows/release_publish-stable.yml +++ b/.github/workflows/release_publish-stable.yml @@ -87,7 +87,7 @@ jobs: else echo "DESIRED_BRANCH=release" >> "$GITHUB_OUTPUT" fi - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4 with: fetch-tags: true show-progress: false diff --git a/CHANGELOG.md b/CHANGELOG.md index 608ffc4feea..f612bd9e544 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,443 +1,4 @@ -# EmberData Changelog - -## v5.3.4 (2024-06-15) - -#### :evergreen_tree: New Deprecation - -* [#9479](https://github.com/emberjs/data/pull/9479) feat: support migration path for ember-inflector usage ([@runspired](https://github.com/runspired)) -* [#9403](https://github.com/emberjs/data/pull/9403) feat: deprecate store extending EmberObject ([@runspired](https://github.com/runspired)) - -#### :memo: Documentation - -* [#9394](https://github.com/emberjs/data/pull/9394) Add cookbook page about model names convention ([@Baltazore](https://github.com/Baltazore)) -* [#9393](https://github.com/emberjs/data/pull/9393) Update types on typescript guide part 4 ([@Baltazore](https://github.com/Baltazore)) -* [#9390](https://github.com/emberjs/data/pull/9390) docs: fix readme links in @warp-drive/ember ([@runspired](https://github.com/runspired)) -* [#9379](https://github.com/emberjs/data/pull/9379) fix: Automate uninstall process ([@MichalBryxi](https://github.com/MichalBryxi)) -* [#9378](https://github.com/emberjs/data/pull/9378) Update some docs to string ids ([@wagenet](https://github.com/wagenet)) -* [#9300](https://github.com/emberjs/data/pull/9300) doc: remove reference to unexisting ESA auth handler ([@sly7-7](https://github.com/sly7-7)) -* [#9332](https://github.com/emberjs/data/pull/9332) docs: add typescript guide ([@runspired](https://github.com/runspired)) -* [#9328](https://github.com/emberjs/data/pull/9328) chore: update READMEs with status and dist tag info ([@runspired](https://github.com/runspired)) -* [#9329](https://github.com/emberjs/data/pull/9329) chore: update compat chart in README ([@runspired](https://github.com/runspired)) -* [#9299](https://github.com/emberjs/data/pull/9299) doc: use store for save-record docs ([@Yelinz](https://github.com/Yelinz)) -* [#9298](https://github.com/emberjs/data/pull/9298) docs(request): remove duplicate line in readme ([@Yelinz](https://github.com/Yelinz)) -* [#9063](https://github.com/emberjs/data/pull/9063) docs: add requests guide ([@runspired](https://github.com/runspired)) -* [#9215](https://github.com/emberjs/data/pull/9215) Docs: Add guide for incremental adoption ([@Baltazore](https://github.com/Baltazore)) -* [#9275](https://github.com/emberjs/data/pull/9275) doc: don't mention unexisting ESA auth handler ([@sly7-7](https://github.com/sly7-7)) - -#### :rocket: Enhancement - -* [#9474](https://github.com/emberjs/data/pull/9474) Improve query types for legacy-compat/builders ([@gitKrystan](https://github.com/gitKrystan)) -* [#9473](https://github.com/emberjs/data/pull/9473) npx: warp-drive retrofit types@canary 🪄 ([@runspired](https://github.com/runspired)) -* [#9471](https://github.com/emberjs/data/pull/9471) feat: npx warp-drive ([@runspired](https://github.com/runspired)) -* [#9467](https://github.com/emberjs/data/pull/9467) feat: implement schema-object for schema-record ([@richgt](https://github.com/richgt)) -* [#9468](https://github.com/emberjs/data/pull/9468) feat: string utils 🌌 ([@runspired](https://github.com/runspired)) -* [#9466](https://github.com/emberjs/data/pull/9466) feat: make @id editable and reactive ([@runspired](https://github.com/runspired)) -* [#9465](https://github.com/emberjs/data/pull/9465) feat: implement edit & create cases for legacy relationships ([@runspired](https://github.com/runspired)) -* [#9464](https://github.com/emberjs/data/pull/9464) feat: implement support for legacy hasMany and belongsTo relationship reads ([@runspired](https://github.com/runspired)) -* [#9407](https://github.com/emberjs/data/pull/9407) feat: v2 addons ([@runspired](https://github.com/runspired)) -* [#9453](https://github.com/emberjs/data/pull/9453) feat: update SchemaService to reflect RFC updates ([@runspired](https://github.com/runspired)) -* [#9448](https://github.com/emberjs/data/pull/9448) feat: impl SchemaService RFC ([@runspired](https://github.com/runspired)) -* [#9450](https://github.com/emberjs/data/pull/9450) feat: improve typing around Model and createRecord ([@runspired](https://github.com/runspired)) -* [#9444](https://github.com/emberjs/data/pull/9444) feat: rename LifetimesService => CachePolicy for clarity ([@runspired](https://github.com/runspired)) -* [#9443](https://github.com/emberjs/data/pull/9443) feat: universal consts ([@runspired](https://github.com/runspired)) -* [#9396](https://github.com/emberjs/data/pull/9396) fix: Resolve promise types for props passed to `store.createRecord()` ([@seanCodes](https://github.com/seanCodes)) -* [#9401](https://github.com/emberjs/data/pull/9401) feat: preserve lids returned by the API in legacy normalization ([@runspired](https://github.com/runspired)) -* [#9400](https://github.com/emberjs/data/pull/9400) feat: add expectId util ([@runspired](https://github.com/runspired)) -* [#9343](https://github.com/emberjs/data/pull/9343) @ember-data/codemods package ([@gitKrystan](https://github.com/gitKrystan)) -* [#9387](https://github.com/emberjs/data/pull/9387) feat: better types for legacy store methods ([@runspired](https://github.com/runspired)) -* [#8957](https://github.com/emberjs/data/pull/8957) feat(private): schema CLI ([@runspired](https://github.com/runspired)) -* [#9366](https://github.com/emberjs/data/pull/9366) feat: typed Model ([@runspired](https://github.com/runspired)) -* [#9363](https://github.com/emberjs/data/pull/9363) feat: autoRefresh ([@runspired](https://github.com/runspired)) -* [#9359](https://github.com/emberjs/data/pull/9359) feat: type checked builders and inferred request types from builders ([@runspired](https://github.com/runspired)) -* [#9353](https://github.com/emberjs/data/pull/9353) feat: utilies for migrating to stricter type and id usage ([@runspired](https://github.com/runspired)) -* [#9352](https://github.com/emberjs/data/pull/9352) feat: make setKeyInfoForResource public ([@runspired](https://github.com/runspired)) -* [#9277](https://github.com/emberjs/data/pull/9277) feat: implement managed object for schemaRecord ([@richgt](https://github.com/richgt)) -* [#9319](https://github.com/emberjs/data/pull/9319) Add @ember-data/legacy-compat/builders ([@gitKrystan](https://github.com/gitKrystan)) -* [#9314](https://github.com/emberjs/data/pull/9314) feat: improve lifetime handling of ad-hoc createRecord requests ([@runspired](https://github.com/runspired)) -* [#9317](https://github.com/emberjs/data/pull/9317) feat: ensure data utils work well with legacy relationship proxies ([@runspired](https://github.com/runspired)) -* [#9260](https://github.com/emberjs/data/pull/9260) feat: ember specific data utils ([@runspired](https://github.com/runspired)) -* [#9240](https://github.com/emberjs/data/pull/9240) feat: implement managed array for schemaRecord ([@richgt](https://github.com/richgt)) -* [#9256](https://github.com/emberjs/data/pull/9256) feat: improve alpha types support ([@runspired](https://github.com/runspired)) -* [#9250](https://github.com/emberjs/data/pull/9250) feat: fix types for legacy decorator syntax ([@runspired](https://github.com/runspired)) -* [#9249](https://github.com/emberjs/data/pull/9249) chore: handle declare statements in module rewriting ([@runspired](https://github.com/runspired)) -* [#9248](https://github.com/emberjs/data/pull/9248) feat: publish types as module defs ([@runspired](https://github.com/runspired)) -* [#9245](https://github.com/emberjs/data/pull/9245) feat: add consumer types for Model APIs ([@runspired](https://github.com/runspired)) -* [#9246](https://github.com/emberjs/data/pull/9246) normalization in json-api serializer preserves lid #7956 ([@sly7-7](https://github.com/sly7-7)) -* [#9244](https://github.com/emberjs/data/pull/9244) feat: improves consumer-facing store types ([@runspired](https://github.com/runspired)) - -#### :bug: Bug Fix - -* [#9475](https://github.com/emberjs/data/pull/9475) fix: dont install optional peers if not already present ([@runspired](https://github.com/runspired)) -* [#9469](https://github.com/emberjs/data/pull/9469) Fix exports for 'ember-data' ([@NullVoxPopuli](https://github.com/NullVoxPopuli)) -* [#9459](https://github.com/emberjs/data/pull/9459) fix: ensure cachehandler responses are cast to documents ([@runspired](https://github.com/runspired)) -* [#9456](https://github.com/emberjs/data/pull/9456) fix: visibilitychange => hidden should update unavailableStart ([@runspired](https://github.com/runspired)) -* [#9454](https://github.com/emberjs/data/pull/9454) Allow RequestState.abort to be used with on modifier ([@gitKrystan](https://github.com/gitKrystan)) -* [#9455](https://github.com/emberjs/data/pull/9455) fix: config version lookup needs to be project location aware ([@runspired](https://github.com/runspired)) -* [#9355](https://github.com/emberjs/data/pull/9355) Fix: @attr defaultValue() results should persist after initialization ([@christophersansone](https://github.com/christophersansone)) -* [#9391](https://github.com/emberjs/data/pull/9391) fix: dont fall-through after shouldAttempt on refresh ([@runspired](https://github.com/runspired)) -* [#9383](https://github.com/emberjs/data/pull/9383) fix: ensure cache-handler clones full errors ([@runspired](https://github.com/runspired)) -* [#9369](https://github.com/emberjs/data/pull/9369) fix: @warp-drive-ember, dont leak empty slot ([@runspired](https://github.com/runspired)) -* [#9364](https://github.com/emberjs/data/pull/9364) fix: restore old behavior in deprecation ([@enspandi](https://github.com/enspandi)) -* [#9360](https://github.com/emberjs/data/pull/9360) fix: Make IS_MAYBE_MIRAGE work in Firefox ([@MichalBryxi](https://github.com/MichalBryxi)) -* [#9318](https://github.com/emberjs/data/pull/9318) fix: be more specific in files in case .npmignore is ignored ([@runspired](https://github.com/runspired)) -* [#9307](https://github.com/emberjs/data/pull/9307) fix: mirage does not support anything ([@runspired](https://github.com/runspired)) -* [#9265](https://github.com/emberjs/data/pull/9265) feat: Improve config handling for polyfillUUID ([@MehulKChaudhari](https://github.com/MehulKChaudhari)) -* [#9263](https://github.com/emberjs/data/pull/9263) fix: set localState to latest identifier in belongsTo when merging identifiers ([@runspired](https://github.com/runspired)) -* [#9254](https://github.com/emberjs/data/pull/9254) Update IS_MAYBE_MIRAGE function to check for Mirage in development mode ([@Baltazore](https://github.com/Baltazore)) -* [#9257](https://github.com/emberjs/data/pull/9257) fix: use npm pack instead of pnpm pack to respect .npmignore rules ([@runspired](https://github.com/runspired)) -* [#9252](https://github.com/emberjs/data/pull/9252) fix: update line when removing declare statements ([@runspired](https://github.com/runspired)) -* [#9251](https://github.com/emberjs/data/pull/9251) fix: notify during replace if existing localState never previously calculated ([@runspired](https://github.com/runspired)) - -#### :house: Internal - -* [#9477](https://github.com/emberjs/data/pull/9477) fix: add deprecation and avoid breaking configs ([@runspired](https://github.com/runspired)) -* [#9476](https://github.com/emberjs/data/pull/9476) chore: cleanup symbol usage ([@runspired](https://github.com/runspired)) -* [#9463](https://github.com/emberjs/data/pull/9463) types: ManyArray => HasMany ([@runspired](https://github.com/runspired)) -* [#9457](https://github.com/emberjs/data/pull/9457) feat: the big list of versions ([@runspired](https://github.com/runspired)) -* [#9292](https://github.com/emberjs/data/pull/9292) feat: add new build-config package ([@runspired](https://github.com/runspired)) -* [#9399](https://github.com/emberjs/data/pull/9399) types: limit traversal depth on include path generation ([@runspired](https://github.com/runspired)) -* [#9398](https://github.com/emberjs/data/pull/9398) chore: dont --compile during prepack ([@runspired](https://github.com/runspired)) -* [#9397](https://github.com/emberjs/data/pull/9397) chore: fixup publish for @ember-data/codemods ([@runspired](https://github.com/runspired)) -* [#9395](https://github.com/emberjs/data/pull/9395) Update strategy.json to mirror publish @warp-drive/schema-record ([@runspired](https://github.com/runspired)) -* [#9385](https://github.com/emberjs/data/pull/9385) fix: Make IS_MAYBE_MIRAGE simplified ([@MichalBryxi](https://github.com/MichalBryxi)) -* [#9392](https://github.com/emberjs/data/pull/9392) Fix some typos after reading code ([@Baltazore](https://github.com/Baltazore)) -* [#9370](https://github.com/emberjs/data/pull/9370) chore: rename macros ([@runspired](https://github.com/runspired)) -* [#9368](https://github.com/emberjs/data/pull/9368) docs: Update ISSUE_TEMPLATE.md to follow latest pnpm ([@MichalBryxi](https://github.com/MichalBryxi)) -* [#9365](https://github.com/emberjs/data/pull/9365) chore: remove unneeded infra tests ([@runspired](https://github.com/runspired)) -* [#9349](https://github.com/emberjs/data/pull/9349) chore: fix CI installs ([@runspired](https://github.com/runspired)) -* [#9330](https://github.com/emberjs/data/pull/9330) chore: ensure latest tag is canary/beta tag for early stage packages ([@runspired](https://github.com/runspired)) -* [#9303](https://github.com/emberjs/data/pull/9303) infra: setup mirror and types publishing ([@runspired](https://github.com/runspired)) -* [#9291](https://github.com/emberjs/data/pull/9291) chore: remove unused scripts ([@runspired](https://github.com/runspired)) -* [#9289](https://github.com/emberjs/data/pull/9289) chore: bump timeout for floating dep check in CI ([@runspired](https://github.com/runspired)) -* [#9287](https://github.com/emberjs/data/pull/9287) chore: bump deps for example-api app ([@runspired](https://github.com/runspired)) -* [#9279](https://github.com/emberjs/data/pull/9279) types: branded transforms and improve types needed for serializers ([@runspired](https://github.com/runspired)) -* [#9280](https://github.com/emberjs/data/pull/9280) chore: handle dynamic imports with relative paths ([@runspired](https://github.com/runspired)) -* [#9259](https://github.com/emberjs/data/pull/9259) Update setting-up-the-project.md ([@MehulKChaudhari](https://github.com/MehulKChaudhari)) -* [#9258](https://github.com/emberjs/data/pull/9258) fix: remove unused turbo key ([@runspired](https://github.com/runspired)) - -#### Committers: (13) - -Chris Thoburn ([@runspired](https://github.com/runspired)) -Kirill Shaplyko ([@Baltazore](https://github.com/Baltazore)) -Michal Bryxí ([@MichalBryxi](https://github.com/MichalBryxi)) -Peter Wagenet ([@wagenet](https://github.com/wagenet)) -Sylvain Mina ([@sly7-7](https://github.com/sly7-7)) -Yelin Zhang ([@Yelinz](https://github.com/Yelinz)) -Krystan HuffMenne ([@gitKrystan](https://github.com/gitKrystan)) -Rich Glazerman ([@richgt](https://github.com/richgt)) -Sean Juarez ([@seanCodes](https://github.com/seanCodes)) -[@NullVoxPopuli](https://github.com/NullVoxPopuli) -Christopher Sansone ([@christophersansone](https://github.com/christophersansone)) -Andreas Minnich ([@enspandi](https://github.com/enspandi)) -Mehul Kiran Chaudhari ([@MehulKChaudhari](https://github.com/MehulKChaudhari)) - -## v5.3.3 (2024-03-02) - -#### :bug: Bug Fix - -* [#9243](https://github.com/emberjs/data/pull/9243) fix: keep core-type peer-deps ([@runspired](https://github.com/runspired)) - -#### Committers: (1) - -Chris Thoburn ([@runspired](https://github.com/runspired)) - -## v5.3.2 (2024-02-29) - -#### :house: Internal - -* [#9241](https://github.com/emberjs/data/pull/9241) chore: manually run prepack ([@runspired](https://github.com/runspired)) -* [#9238](https://github.com/emberjs/data/pull/9238) chore: better "from" version default value population ([@runspired](https://github.com/runspired)) -* [#9237](https://github.com/emberjs/data/pull/9237) chore: fix publishing of core-types when strategy is private ([@runspired](https://github.com/runspired)) - -#### Committers: (1) - -Chris Thoburn ([@runspired](https://github.com/runspired)) - -## v5.3.1 (2024-02-24) - -#### :evergreen_tree: New Deprecation - -* [#9189](https://github.com/emberjs/data/pull/9189) fix: mutating ManyArray should handle duplicates gracefully (with deprecation) ([@gitKrystan](https://github.com/gitKrystan)) - -#### :memo: Documentation - -* [#9132](https://github.com/emberjs/data/pull/9132) Add auth handler guides ([@Baltazore](https://github.com/Baltazore)) -* [#9071](https://github.com/emberjs/data/pull/9071) chore: refactor relationships guide ([@runspired](https://github.com/runspired)) -* [#9059](https://github.com/emberjs/data/pull/9059) docs: The comprehensive guide to relationships ([@runspired](https://github.com/runspired)) -* [#9018](https://github.com/emberjs/data/pull/9018) doc(README): remove typo ([@omimakhare](https://github.com/omimakhare)) -* [#8966](https://github.com/emberjs/data/pull/8966) feat: Add links to the CODE_OF_CONDUCT.md ([@Agnik7](https://github.com/Agnik7)) -* [#8963](https://github.com/emberjs/data/pull/8963) chore: scaffold additional contributing materials ([@runspired](https://github.com/runspired)) -* [#9162](https://github.com/emberjs/data/pull/9162) feat: improve store.request documentation ([@runspired](https://github.com/runspired)) -* [#9161](https://github.com/emberjs/data/pull/9161) docs: fix return signature of peekRequest ([@runspired](https://github.com/runspired)) -* [#9159](https://github.com/emberjs/data/pull/9159) fix: support full range of json:api for references, update docs ([@runspired](https://github.com/runspired)) -* [#9160](https://github.com/emberjs/data/pull/9160) docs: update links ([@runspired](https://github.com/runspired)) -* [#8954](https://github.com/emberjs/data/pull/8954) docs: typo in hasChangedRelationships description ([@BoussonKarel](https://github.com/BoussonKarel)) -* [#9072](https://github.com/emberjs/data/pull/9072) feat: advanced JSON:API queries & basic request example ([@runspired](https://github.com/runspired)) -* [#9070](https://github.com/emberjs/data/pull/9070) docs: fix note notation to make use of github formatting ([@runspired](https://github.com/runspired)) -* [#9068](https://github.com/emberjs/data/pull/9068) docs: unroll details sections ([@runspired](https://github.com/runspired)) - -#### :rocket: Enhancement - -* [#9220](https://github.com/emberjs/data/pull/9220) feat: request infra improvements ([@runspired](https://github.com/runspired)) -* [#9163](https://github.com/emberjs/data/pull/9163) feat: improved lifetimes-service capabilities ([@runspired](https://github.com/runspired)) -* [#9159](https://github.com/emberjs/data/pull/9159) fix: support full range of json:api for references, update docs ([@runspired](https://github.com/runspired)) -* [#9094](https://github.com/emberjs/data/pull/9094) feat: support legacy attribute behaviors in SchemaRecord ([@gitKrystan](https://github.com/gitKrystan)) -* [#9095](https://github.com/emberjs/data/pull/9095) feat (internal): support legacy model behaviors in SchemaRecord legacy mode ([@runspired](https://github.com/runspired)) -* [#9072](https://github.com/emberjs/data/pull/9072) feat: advanced JSON:API queries & basic request example ([@runspired](https://github.com/runspired)) -* [#9069](https://github.com/emberjs/data/pull/9069) feat: Improve extensibility ([@runspired](https://github.com/runspired)) -* [#8955](https://github.com/emberjs/data/pull/8955) feat(private): scaffold packages for schema parser ([@runspired](https://github.com/runspired)) -* [#8949](https://github.com/emberjs/data/pull/8949) feat:prepare for universal reactivity ([@runspired](https://github.com/runspired)) -* [#8948](https://github.com/emberjs/data/pull/8948) feat(private): reactive simple fields ([@runspired](https://github.com/runspired)) -* [#8946](https://github.com/emberjs/data/pull/8946) feat (private): implement resource relationships for SchemaRecord ([@runspired](https://github.com/runspired)) -* [#8939](https://github.com/emberjs/data/pull/8939) feat (private): implement support for derivations in schema-record ([@runspired](https://github.com/runspired)) -* [#8935](https://github.com/emberjs/data/pull/8935) feat: (private) implement basic field support for schema-record ([@runspired](https://github.com/runspired)) -* [#8925](https://github.com/emberjs/data/pull/8925) feat: implement postQuery builder ([@runspired](https://github.com/runspired)) -* [#8921](https://github.com/emberjs/data/pull/8921) feat: Improved Fetch Errors ([@runspired](https://github.com/runspired)) - -#### :bug: Bug Fix - -* [#9221](https://github.com/emberjs/data/pull/9221) fix: prevent rollbackRelationships from setting remoteState and localState to the same array reference ([@runspired](https://github.com/runspired)) -* [#9203](https://github.com/emberjs/data/pull/9203) fix: Fetch handler hacks for Mirage (canary) ([@gitKrystan](https://github.com/gitKrystan)) -* [#9189](https://github.com/emberjs/data/pull/9189) fix: mutating ManyArray should handle duplicates gracefully (with deprecation) ([@gitKrystan](https://github.com/gitKrystan)) -* [#9183](https://github.com/emberjs/data/pull/9183) fix: keep a backreference for previously merged identifiers ([@runspired](https://github.com/runspired)) -* [#8927](https://github.com/emberjs/data/pull/8927) fix: live-array delete sync should not clear the set on length match ([@runspired](https://github.com/runspired)) -* [#9164](https://github.com/emberjs/data/pull/9164) fix: url configuration should respect / for host and error more meaningfully when invalid ([@runspired](https://github.com/runspired)) -* [#9159](https://github.com/emberjs/data/pull/9159) fix: support full range of json:api for references, update docs ([@runspired](https://github.com/runspired)) -* [#9097](https://github.com/emberjs/data/pull/9097) fix: allow decorator syntax in code comments during yui doc processing ([@jaredgalanis](https://github.com/jaredgalanis)) -* [#9014](https://github.com/emberjs/data/pull/9014) fix: make willCommit slightly safer when race conditions occur ([@runspired](https://github.com/runspired)) -* [#8934](https://github.com/emberjs/data/pull/8934) fix: JSONAPISerializer should not reify empty records ([@runspired](https://github.com/runspired)) -* [#8892](https://github.com/emberjs/data/pull/8892) doc: Fix paths in transform deprecations ([@HeroicEric](https://github.com/HeroicEric)) - -#### :house: Internal - -* [#9125](https://github.com/emberjs/data/pull/9125) Configure ESLint for test packages ([@gitKrystan](https://github.com/gitKrystan)) -* [#8994](https://github.com/emberjs/data/pull/8994) chore: fix recursive pnpm on node 18.18 ([@runspired](https://github.com/runspired)) -* [#9110](https://github.com/emberjs/data/pull/9110) Stricter typescript-eslint config ([@gitKrystan](https://github.com/gitKrystan)) -* [#9101](https://github.com/emberjs/data/pull/9101) chore: Type check test files ([@gitKrystan](https://github.com/gitKrystan)) -* [#9093](https://github.com/emberjs/data/pull/9093) feat(internal): implement legacy mode toggle ([@runspired](https://github.com/runspired)) -* [#9085](https://github.com/emberjs/data/pull/9085) Add type-checking to tests/warp-drive__schema-record ([@gitKrystan](https://github.com/gitKrystan)) -* [#9089](https://github.com/emberjs/data/pull/9089) Add type-checking for packages/unpublished-test-infra ([@gitKrystan](https://github.com/gitKrystan)) -* [#9009](https://github.com/emberjs/data/pull/9009) chore(internal) add @warp-drive/diagnostic/ember ([@runspired](https://github.com/runspired)) -* [#9007](https://github.com/emberjs/data/pull/9007) chore(internal): convert model and adapter tests to use diagnostic ([@runspired](https://github.com/runspired)) -* [#8967](https://github.com/emberjs/data/pull/8967) chore(private): implements a QUnit alternative ([@runspired](https://github.com/runspired)) -* [#9086](https://github.com/emberjs/data/pull/9086) Add ESLint config for tests/warp-drive__schema-record ([@gitKrystan](https://github.com/gitKrystan)) -* [#9078](https://github.com/emberjs/data/pull/9078) docs: add compatibility table to readme ([@runspired](https://github.com/runspired)) -* [#9054](https://github.com/emberjs/data/pull/9054) Initial lint config for tests/blueprints ([@gitKrystan](https://github.com/gitKrystan)) -* [#9061](https://github.com/emberjs/data/pull/9061) Git-ignore .prettier-cache ([@gitKrystan](https://github.com/gitKrystan)) -* [#8993](https://github.com/emberjs/data/pull/8993) chore: fix development test command ([@runspired](https://github.com/runspired)) -* [#8986](https://github.com/emberjs/data/pull/8986) chore: rename schema tests to warp-drive__* variants ([@runspired](https://github.com/runspired)) -* [#8984](https://github.com/emberjs/data/pull/8984) chore: remove unneeded debug-encapsulation tests ([@runspired](https://github.com/runspired)) -* [#8983](https://github.com/emberjs/data/pull/8983) chore: rename request-test-app to ember-data__request ([@runspired](https://github.com/runspired)) -* [#8982](https://github.com/emberjs/data/pull/8982) chore: rename json-api-test-app to ember-data__json-api ([@runspired](https://github.com/runspired)) -* [#8981](https://github.com/emberjs/data/pull/8981) chore: rename adapter-encapsulation-test-app to ember-data__adapter ([@runspired](https://github.com/runspired)) -* [#8980](https://github.com/emberjs/data/pull/8980) chore: rename graph-test-app to ember-data__graph ([@runspired](https://github.com/runspired)) -* [#8979](https://github.com/emberjs/data/pull/8979) chore: rename serializer-encapsulation tests, remove smoke-test ([@runspired](https://github.com/runspired)) -* [#8978](https://github.com/emberjs/data/pull/8978) chore: rename model-encapsulation tests, remove smoke-test ([@runspired](https://github.com/runspired)) -* [#8974](https://github.com/emberjs/data/pull/8974) chore: remove uneeded json-api-encapsulation test app ([@runspired](https://github.com/runspired)) -* [#8960](https://github.com/emberjs/data/pull/8960) internal: fix test settledness ([@runspired](https://github.com/runspired)) -* [#9084](https://github.com/emberjs/data/pull/9084) Add import types ([@gitKrystan](https://github.com/gitKrystan)) -* [#8989](https://github.com/emberjs/data/pull/8989) chore(private): concurrent mode ([@runspired](https://github.com/runspired)) -* [#9082](https://github.com/emberjs/data/pull/9082) Remove remaining @types/ember* packages ([@gitKrystan](https://github.com/gitKrystan)) -* [#8961](https://github.com/emberjs/data/pull/8961) chore: run tests nicely ([@runspired](https://github.com/runspired)) -* [#9062](https://github.com/emberjs/data/pull/9062) Extract qunit ESLint config ([@gitKrystan](https://github.com/gitKrystan)) -* [#9058](https://github.com/emberjs/data/pull/9058) Switch from eslint-plugin-prettier to running prettier directly ([@gitKrystan](https://github.com/gitKrystan)) -* [#9057](https://github.com/emberjs/data/pull/9057) Add eslint-plugin-n to eslint config for node files ([@gitKrystan](https://github.com/gitKrystan)) -* [#9055](https://github.com/emberjs/data/pull/9055) Fix ESLint for VSCode ([@gitKrystan](https://github.com/gitKrystan)) -* [#9051](https://github.com/emberjs/data/pull/9051) chore: use references for tsc, add checks to schema-record, bun to run scripts ([@runspired](https://github.com/runspired)) -* [#9032](https://github.com/emberjs/data/pull/9032) chore(types): split out lint and type commands to be per-package ([@runspired](https://github.com/runspired)) -* [#9050](https://github.com/emberjs/data/pull/9050) chore: use composite mode for tsc ([@runspired](https://github.com/runspired)) -* [#9049](https://github.com/emberjs/data/pull/9049) chore: incremental tsc builds ([@runspired](https://github.com/runspired)) -* [#9046](https://github.com/emberjs/data/pull/9046) chore: reduce number of things turbo builds for build ([@runspired](https://github.com/runspired)) -* [#9027](https://github.com/emberjs/data/pull/9027) chore: improve types for store package ([@runspired](https://github.com/runspired)) -* [#9029](https://github.com/emberjs/data/pull/9029) chore: add @warp-drive/core as home for shared code ([@runspired](https://github.com/runspired)) -* [#9028](https://github.com/emberjs/data/pull/9028) chore: more isolated types ([@runspired](https://github.com/runspired)) -* [#9025](https://github.com/emberjs/data/pull/9025) chore: reconfigure request package type location ([@runspired](https://github.com/runspired)) -* [#9024](https://github.com/emberjs/data/pull/9024) chore: cleanup more types ([@runspired](https://github.com/runspired)) -* [#9021](https://github.com/emberjs/data/pull/9021) chore: cleanup ember-data/-private types ([@runspired](https://github.com/runspired)) -* [#9019](https://github.com/emberjs/data/pull/9019) chore: make model types strict ([@runspired](https://github.com/runspired)) -* [#9017](https://github.com/emberjs/data/pull/9017) chore: make json-api cache strict ([@runspired](https://github.com/runspired)) -* [#9016](https://github.com/emberjs/data/pull/9016) chore: make type-only files strict ([@runspired](https://github.com/runspired)) -* [#9008](https://github.com/emberjs/data/pull/9008) chore: update eslint plugin name ([@runspired](https://github.com/runspired)) -* [#9006](https://github.com/emberjs/data/pull/9006) chore (internal): convert builder and request tests to use diagnostic+runner ([@runspired](https://github.com/runspired)) -* [#9000](https://github.com/emberjs/data/pull/9000) feat(private): native test runner ([@runspired](https://github.com/runspired)) -* [#8995](https://github.com/emberjs/data/pull/8995) chore: add @warp-drive/diagnostic docs ([@runspired](https://github.com/runspired)) -* [#8987](https://github.com/emberjs/data/pull/8987) chore: test-harness improvements ([@runspired](https://github.com/runspired)) -* [#8972](https://github.com/emberjs/data/pull/8972) chore: use new test runner for request tests ([@runspired](https://github.com/runspired)) -* [#8931](https://github.com/emberjs/data/pull/8931) chore: package infra for schema-record ([@runspired](https://github.com/runspired)) -* [#8930](https://github.com/emberjs/data/pull/8930) chore: get last request for any record on instantiation ([@runspired](https://github.com/runspired)) -* [#8923](https://github.com/emberjs/data/pull/8923) chore: prepare files for new eslint plugin ([@runspired](https://github.com/runspired)) -* [#8911](https://github.com/emberjs/data/pull/8911) chore: remove unneeded type cast ([@runspired](https://github.com/runspired)) -* [#8912](https://github.com/emberjs/data/pull/8912) chore: docs for holodeck ([@runspired](https://github.com/runspired)) -* [#8906](https://github.com/emberjs/data/pull/8906) feat: expand mock-server capabilities, add to main tests ([@runspired](https://github.com/runspired)) - -#### Committers: (8) - -Krystan HuffMenne ([@gitKrystan](https://github.com/gitKrystan)) -Kirill Shaplyko ([@Baltazore](https://github.com/Baltazore)) -Chris Thoburn ([@runspired](https://github.com/runspired)) -OMKAR MAKHARE ([@omimakhare](https://github.com/omimakhare)) -Agnik Bakshi ([@Agnik7](https://github.com/Agnik7)) -[@BoussonKarel](https://github.com/BoussonKarel) -Jared Galanis ([@jaredgalanis](https://github.com/jaredgalanis)) -Eric Kelly ([@HeroicEric](https://github.com/HeroicEric)) - -## v5.3.0 (2023-09-18) - -#### :rocket: Enhancement - * [#8849](https://github.com/emberjs/data/pull/8849) feat: docs, tests and fixes for create/update/deleteRecord builders ([@Baltazore](https://github.com/Baltazore)) - * [#8824](https://github.com/emberjs/data/pull/8824) feat: relationshipRollback, serializePatch ([@runspired](https://github.com/runspired)) - * [#8798](https://github.com/emberjs/data/pull/8798) feat: implement a simple LifetimeService utility, improve document reconstruction ([@runspired](https://github.com/runspired)) - * [#8741](https://github.com/emberjs/data/pull/8741) feat: JSON:API serialization utils ([@runspired](https://github.com/runspired)) - * [#8740](https://github.com/emberjs/data/pull/8740) feat: saveRecord builders ([@runspired](https://github.com/runspired)) - * [#8744](https://github.com/emberjs/data/pull/8744) add sortQueryParams, update roadmap with link to checklist ([@runspired](https://github.com/runspired)) - * [#8716](https://github.com/emberjs/data/pull/8716) feat: filterEmpty for query params ([@runspired](https://github.com/runspired)) - * [#8687](https://github.com/emberjs/data/pull/8687) feat: findRecord and query request builders ([@runspired](https://github.com/runspired)) - * [#8673](https://github.com/emberjs/data/pull/8673) DX: Nicer backtracking errors ([@runspired](https://github.com/runspired)) - * [#8736](https://github.com/emberjs/data/pull/8736) chore: refactor IdentityCache to make resource more opaque ([@runspired](https://github.com/runspired)) - -#### :bug: Bug Fix - * [#8876](https://github.com/emberjs/data/pull/8876) fix: Fetch handler should account for empty body ([@runspired](https://github.com/runspired)) - * [#8842](https://github.com/emberjs/data/pull/8842) fix: handle Immutable Response objects ([@runspired](https://github.com/runspired)) - * [#8828](https://github.com/emberjs/data/pull/8828) fix: set headers after setResponse in Fetch handler ([@runspired](https://github.com/runspired)) - * [#8850](https://github.com/emberjs/data/pull/8850) Overwrite addMixin ([@patricklx](https://github.com/patricklx)) - * [#8831](https://github.com/emberjs/data/pull/8831) fix: cleanup build deps and add @ember/string to REST/ActiveRecord builder peer-deps ([@runspired](https://github.com/runspired)) - * [#8826](https://github.com/emberjs/data/pull/8826) fix createRecord error when no adapter is present ([@runspired](https://github.com/runspired)) - * [#8791](https://github.com/emberjs/data/pull/8791) fix: clear relationships properly when unloading new records ([@Windvis](https://github.com/Windvis)) - * [#8794](https://github.com/emberjs/data/pull/8794) Fix check for new records in JSONAPISerializer.serializeHasMany ([@dagroe](https://github.com/dagroe)) - * [#8751](https://github.com/emberjs/data/pull/8751) Forward fixes from 3.12.x into main ([@jrjohnson](https://github.com/jrjohnson)) - * [#8684](https://github.com/emberjs/data/pull/8684) fix: unloadAll(void) should not destroy the notification manager ([@runspired](https://github.com/runspired)) - -#### :evergreen_tree: New Deprecation - * [#8747](https://github.com/emberjs/data/pull/8747) feat: implement legacy imports deprecation ([@runspired](https://github.com/runspired)) - * [#8734](https://github.com/emberjs/data/pull/8734) feat: Implement Strict Types and Id Deprecations ([@runspired](https://github.com/runspired)) - -#### :shower: Deprecation Removal -* `adapter`, `model`, `private-build-infra`, `serializer` - * [#8797](https://github.com/emberjs/data/pull/8797) Drop support for `ember-cli-mocha` and `ember-mocha` when generating test blueprints ([@bertdeblock](https://github.com/bertdeblock)) - -#### :memo: Documentation - * [#8848](https://github.com/emberjs/data/pull/8848) feat: add request options documentation parts to find-record builder ([@Baltazore](https://github.com/Baltazore)) - * [#8825](https://github.com/emberjs/data/pull/8825) feat: more docs for builders ([@runspired](https://github.com/runspired)) - * [#8819](https://github.com/emberjs/data/pull/8819) fix: `JSONAPISerializer.shouldSerializeHasMany` relation param type ([@samridhivig](https://github.com/samridhivig)) - * [#8746](https://github.com/emberjs/data/pull/8746) docs: more documentation for builders ([@runspired](https://github.com/runspired)) - * [#8745](https://github.com/emberjs/data/pull/8745) chore: readme overviews for builders ([@runspired](https://github.com/runspired)) - * [#8724](https://github.com/emberjs/data/pull/8724) chore: rename CacheStoreWrapper => CacheCapabilitiesManager to reflect its role ([@runspired](https://github.com/runspired)) - * [#8671](https://github.com/emberjs/data/pull/8671) Typo correction in ROADMAP.md ([@wagenet](https://github.com/wagenet)) - -#### :goal_net: Test - * [#8878](https://github.com/emberjs/data/pull/8878) test: add basic test for Fetch handler ([@runspired](https://github.com/runspired)) - * [#8849](https://github.com/emberjs/data/pull/8849) feat: docs, tests and fixes for create/update/deleteRecord builders ([@Baltazore](https://github.com/Baltazore)) - * [#8868](https://github.com/emberjs/data/pull/8868) Add tests for filter-empty request util ([@Baltazore](https://github.com/Baltazore)) - * [#8866](https://github.com/emberjs/data/pull/8866) Add tests for parse-cache-control ([@Baltazore](https://github.com/Baltazore)) - * [#8864](https://github.com/emberjs/data/pull/8864) test: confirm records unload properly for #8863 ([@runspired](https://github.com/runspired)) - * [#8780](https://github.com/emberjs/data/pull/8780) chore: add test to demonstrate create props work as expected ([@runspired](https://github.com/runspired)) - -#### :house: Internal - * [#8758](https://github.com/emberjs/data/pull/8758) chore: refactor implicit edge to match resource and collection pattern ([@runspired](https://github.com/runspired)) - * [#8755](https://github.com/emberjs/data/pull/8755) chore: simplify file structure in graph package ([@runspired](https://github.com/runspired)) - * [#8749](https://github.com/emberjs/data/pull/8749) fix: ensure we are not allowing embroider to do anything ([@runspired](https://github.com/runspired)) - * [#8672](https://github.com/emberjs/data/pull/8672) chore: update roadmap for 5.3 ([@runspired](https://github.com/runspired)) - * [#8670](https://github.com/emberjs/data/pull/8670) chore: add ROADMAP and update CONTRIBUTING ([@runspired](https://github.com/runspired)) - * [#8739](https://github.com/emberjs/data/pull/8739) chore: migrate store/graph to strict types config ([@runspired](https://github.com/runspired)) - * [#8733](https://github.com/emberjs/data/pull/8733) chore: improve types and lint ([@runspired](https://github.com/runspired)) - * [#8727](https://github.com/emberjs/data/pull/8727) chore: fix peers and get perf-test-app running again ([@runspired](https://github.com/runspired)) - * [#8717](https://github.com/emberjs/data/pull/8717) Switch from local and @types/ember types to ember-source types ([@BradBarnich](https://github.com/BradBarnich)) - * [#8499](https://github.com/emberjs/data/pull/8499) chore: refactor model hook support to live in the model package ([@runspired](https://github.com/runspired)) - * [#8862](https://github.com/emberjs/data/pull/8862) chore: remove more runloop usage | completely remove rsvp ([@runspired](https://github.com/runspired)) - * [#8861](https://github.com/emberjs/data/pull/8861) chore: remove runloop usage ([@runspired](https://github.com/runspired)) - * [#8859](https://github.com/emberjs/data/pull/8859) chore: update target labels ([@runspired](https://github.com/runspired)) - * [#8858](https://github.com/emberjs/data/pull/8858) chore: update required labels ([@runspired](https://github.com/runspired)) - * [#8830](https://github.com/emberjs/data/pull/8830) chore: cleanup actions/setup usage ([@runspired](https://github.com/runspired)) - * [#8812](https://github.com/emberjs/data/pull/8812) fix typo ([@samridhivig](https://github.com/samridhivig)) - * [#8802](https://github.com/emberjs/data/pull/8802) chore: fix fastboot-testing deprecation ([@runspired](https://github.com/runspired)) - * [#8801](https://github.com/emberjs/data/pull/8801) chore: resolve deprecation in fastboot app ([@runspired](https://github.com/runspired)) - * [#8860](https://github.com/emberjs/data/pull/8860) chore: burn down runloop and RSVP usage ([@runspired](https://github.com/runspired)) - * [#8832](https://github.com/emberjs/data/pull/8832) chore: add recommended JSON:API setup test app ([@runspired](https://github.com/runspired)) - * [#8829](https://github.com/emberjs/data/pull/8829) chore: eliminate dead build code ([@runspired](https://github.com/runspired)) - * [#8823](https://github.com/emberjs/data/pull/8823) fix: graph instantiation should not be required ([@runspired](https://github.com/runspired)) - -#### Committers: 11 -- Bert De Block ([@bertdeblock](https://github.com/bertdeblock)) -- Chris Thoburn ([@runspired](https://github.com/runspired)) -- Daniel Gröger ([@dagroe](https://github.com/dagroe)) -- Kirill Shaplyko ([@Baltazore](https://github.com/Baltazore)) -- Patrick Pircher ([@patricklx](https://github.com/patricklx)) -- Sam Van Campenhout ([@Windvis](https://github.com/Windvis)) -- Samridhi Vig ([@samridhivig](https://github.com/samridhivig)) -- Brad Barnich ([@BradBarnich](https://github.com/BradBarnich)) -- Jon Johnson ([@jrjohnson](https://github.com/jrjohnson)) -- Michal Bryxí ([@MichalBryxi](https://github.com/MichalBryxi)) -- Peter Wagenet ([@wagenet](https://github.com/wagenet)) - -## 5.2.0 (2023-08-17) - -* Re-release of 5.1.2 to keep lockstep pace. This release contains no new work. -## 5.1.2 (2023-08-17) -#### :bug: Bug Fix - -* [#8750](https://github.com/emberjs/data/pull/8750) Backport into release ([@jrjohnson](https://github.com/jrjohnson)) - * fix: @ember-data/debug should declare its peer-dependency on @ember-data/store #8703 - * fix: de-dupe coalescing when includes or adapterOptions is present but still use findRecord #8704 - * fix: make implicit relationship teardown following delete of related record safe #8705 - * fix: catch errors during didCommit in DEBUG #8708 - -## 5.1.1 (2023-07-07) - -#### :bug: Bug Fix - * [#8685](https://github.com/emberjs/data/pull/8685) fix: unloadAll(void) should not destroy the notification manager (backports #8684) ([@runspired](https://github.com/runspired)) - -#### Committers: 1 -- Chris Thoburn ([@runspired](https://github.com/runspired)) - -## 5.1.0 (2023-06-29) - -#### :bug: Bug Fix - * [#8657](https://github.com/emberjs/data/pull/8657) fix: ensure deprecation configs are threaded to each package ([@runspired](https://github.com/runspired)) - * [#8649](https://github.com/emberjs/data/pull/8649) fix: NotificationManager should only invoke resource/document callbacks owned by the originating store ([@runspired](https://github.com/runspired)) - -#### Committers: 1 -- Chris Thoburn ([@runspired](https://github.com/runspired)) - -## 5.0.1 (2023-06-29) - -#### :bug: Bug Fix - * [#8649](https://github.com/emberjs/data/pull/8649) fix: NotificationManager should only invoke resource/document callbacks owned by the originating store ([@runspired](https://github.com/runspired)) - -#### Committers: 1 -- Chris Thoburn ([@runspired](https://github.com/runspired)) - -## 5.0.0 (2023-06-10) - -#### :bug: Bug Fix -* `adapter` - * [#8621](https://github.com/emberjs/data/pull/8621) fix: normalizeErrorResponse should be resilient to non-string details ([@NullVoxPopuli](https://github.com/NullVoxPopuli)) -* Other - * [#8598](https://github.com/emberjs/data/pull/8598) fix: docs generation should maintain a stable relative path ([@runspired](https://github.com/runspired)) -* `json-api`, `legacy-compat`, `store` - * [#8566](https://github.com/emberjs/data/pull/8566) Avoid unnecessary identity notification when record is saved ([@robbytx](https://github.com/robbytx)) -* `model` - * [#8597](https://github.com/emberjs/data/pull/8597) fix: dont share promise cache for all fields ([@runspired](https://github.com/runspired)) -* `store` - * [#8594](https://github.com/emberjs/data/pull/8594) fix: restore Store extends EmberObject :( ([@runspired](https://github.com/runspired)) - * [#8570](https://github.com/emberjs/data/pull/8570) Fix: don't clear RecordArray if remaining record does not match the removed record ([@esbanarango](https://github.com/esbanarango)) -* `graph`, `model`, `private-build-infra` - * [#8555](https://github.com/emberjs/data/pull/8555) fix: fix polymorphic assertions when deprecated code is removed, improve polymorphic dx ([@runspired](https://github.com/runspired)) - -#### :shower: Deprecation Removal -* `-ember-data`, `adapter`, `debug`, `graph`, `json-api`, `legacy-compat`, `model`, `private-build-infra`, `store`, `unpublished-test-infra` - * [#8550](https://github.com/emberjs/data/pull/8550) chore: remove 4.x deprecations ([@runspired](https://github.com/runspired)) - -#### :memo: Documentation -* `store` - * [#8601](https://github.com/emberjs/data/pull/8601) docs: fix forgotten references to FetchManager ([@runspired](https://github.com/runspired)) -* Other - * [#8598](https://github.com/emberjs/data/pull/8598) fix: docs generation should maintain a stable relative path ([@runspired](https://github.com/runspired)) - -#### Committers: 4 -- Chris Thoburn ([@runspired](https://github.com/runspired)) -- Esteban ([@esbanarango](https://github.com/esbanarango)) -- Robby Morgan ([@robbytx](https://github.com/robbytx)) -- [@NullVoxPopuli](https://github.com/NullVoxPopuli) +# Ember Data Changelog ## v4.12.8 (2024-05-08) @@ -510,7 +71,7 @@ Eric Kelly ([@HeroicEric](https://github.com/HeroicEric)) #### Committers: 1 - Chris Thoburn ([@runspired](https://github.com/runspired)) -## LTS 4.12.2 (2023-07-07) +## 4.12.2 (2023-07-07) #### :rocket: Enhancement * [#8660](https://github.com/emberjs/data/pull/8660) DX: Nicer backtracking errors ([@runspired](https://github.com/runspired)) @@ -521,7 +82,7 @@ Eric Kelly ([@HeroicEric](https://github.com/HeroicEric)) #### Committers: 1 - Chris Thoburn ([@runspired](https://github.com/runspired)) -## LTS 4.12.1 (2023-06-29) +## 4.12.1 (2023-06-29) #### :bug: Bug Fix * [#8656](https://github.com/emberjs/data/pull/8656) fix: NotificationManager should only invoke resource/document callbacks owned by the originating store (#8649) ([@runspired](https://github.com/runspired)) @@ -533,6 +94,7 @@ Eric Kelly ([@HeroicEric](https://github.com/HeroicEric)) - Chris Thoburn ([@runspired](https://github.com/runspired)) - Esteban ([@esbanarango](https://github.com/esbanarango)) + ## 4.12.0 (2023-04-06) #### :rocket: Enhancement @@ -884,46 +446,6 @@ This is a re-release of 4.10.0 - [@law-rence](https://github.com/law-rence) - Eugen Ciur ([@ciur](https://github.com/ciur)) -## v4.6.5 (2024-05-08) - -#### :bug: Bug Fix -* [#9316](https://github.com/emberjs/data/pull/9316) Notify on length when notifying that many-array has changed - -#### Committers: 1 -- -Ross Grayton ([@grayt0r](https://github.com/grayt0r)) - - -## v4.6.4 (2022-10-02) - -#### :bug: Bug Fix -* `private-build-infra` - * [#8199](https://github.com/emberjs/data/pull/8199) [backport release-prev] fix: thread polyfillUUID config through nested deps ([@runspired](https://github.com/runspired)) - -#### Committers: 1 -- Chris Thoburn ([@runspired](https://github.com/runspired)) - -## v4.6.3 (2022-09-15) - -#### :bug: Bug Fix -* `store` - * fix: allow ManyArray being passed to createRecord - -## v4.6.2 (2022-09-15) - -#### :bug: Bug Fix -* `store` - * [#8169](https://github.com/emberjs/data/pull/8169) fix: uuid polyfill logic ([@jrjohnson](https://github.com/jrjohnson)) -* `-ember-data`, `model` - * [#8148](https://github.com/emberjs/data/pull/8148) Clear subscriptions once unsubscribed, don't unnecessarily churn on subscriptions ([@jrjohnson](https://github.com/jrjohnson)) -* `private-build-infra` - * [#8145](https://github.com/emberjs/data/pull/8145) fix earlier versions of node-14 (#8108) ([@jrjohnson](https://github.com/jrjohnson)) -* `private-build-infra`, `store` - * [#8144](https://github.com/emberjs/data/pull/8144) Backport add optional polyfill (#8109) ([@jrjohnson](https://github.com/jrjohnson)) - -#### Committers: 1 -- Jon Johnson ([@jrjohnson](https://github.com/jrjohnson)) - ## v4.6.1 (2022-07-28) #### :bug: Bug Fix @@ -1034,15 +556,6 @@ Ross Grayton ([@grayt0r](https://github.com/grayt0r)) - Cameron Dubas ([@camerondubas](https://github.com/camerondubas)) - Jen Weber ([@jenweber](https://github.com/jenweber)) -## v4.4.2 (2023-08-01) - -#### :bug: Bug Fix -* `model` - * [#8713](https://github.com/emberjs/data/pull/8713) Notify on length when notifying that many-array has changed ([@richgt](https://github.com/richgt)) - -#### Committers: 1 -- Rich Glazerman ([@richgt](https://github.com/richgt)) - ## v4.1.0 (2021-12-30) #### :house: Internal @@ -3418,7 +2931,7 @@ The full API reference of `DS.Snapshot` can be found [here](https://api.emberjs. - fetch() -> fetchById() in docs - Run findHasMany inside an ED runloop - Cleanup debug adapter test: Watching Records -- Fixed didDelete event/callback not fired in uncommitted state +- Fixed didDelete event/callback not fired in uncommited state - Add main entry point for package.json. - register the store as a service - Warn when expected coalesced records are not found in the response diff --git a/config/package.json b/config/package.json index df035eda2a8..a9888f9e860 100644 --- a/config/package.json +++ b/config/package.json @@ -1,7 +1,7 @@ { "name": "@warp-drive/internal-config", "private": true, - "version": "5.4.0-alpha.136", + "version": "4.13.0-alpha.3", "type": "module", "dependencies": { "@babel/cli": "^7.24.5", @@ -11,10 +11,9 @@ "@typescript-eslint/eslint-plugin": "^8.10.0", "@typescript-eslint/parser": "^8.10.0", "typescript-eslint": "^8.10.0", - "@embroider/addon-dev": "^7.1.1", + "@embroider/addon-dev": "^4.3.1", "@eslint/js": "^9.13.0", "globals": "^15.11.0", - "glob": "^11.0.1", "ember-eslint-parser": "^0.5.2", "eslint": "^9.12.0", "eslint-config-prettier": "^9.1.0", diff --git a/config/rollup/external.js b/config/rollup/external.js index f8ff9f47922..f65507dd201 100644 --- a/config/rollup/external.js +++ b/config/rollup/external.js @@ -1,6 +1,6 @@ import path from 'path'; import fs from 'fs'; -import { globSync } from '../utils/glob.js'; +import { globSync } from 'fs'; function loadConfig() { const configPath = path.join(process.cwd(), './package.json'); diff --git a/config/vite/fix-module-output-plugin.js b/config/vite/fix-module-output-plugin.js index faea8857a19..9077cd47287 100644 --- a/config/vite/fix-module-output-plugin.js +++ b/config/vite/fix-module-output-plugin.js @@ -1,6 +1,5 @@ import child_process from 'child_process'; -import { readFileSync, writeFileSync } from 'fs'; -import { globSync } from '../utils/glob.js'; +import { globSync, readFileSync, writeFileSync } from 'fs'; import path from 'path'; const DEBUG = process.env.DEBUG === '*'; diff --git a/config/vite/keep-assets.js b/config/vite/keep-assets.js index c35b196a571..c002c6c169c 100644 --- a/config/vite/keep-assets.js +++ b/config/vite/keep-assets.js @@ -1,6 +1,5 @@ import { join } from 'path'; -import { copyFileSync, mkdirSync } from 'fs'; -import { globSync } from '../utils/glob.js'; +import { copyFileSync, globSync, mkdirSync } from 'fs'; export function keepAssets({ from, include, dist }) { return { diff --git a/guides/community-resources.md b/guides/community-resources.md index 656f39f74fe..42708761c8a 100644 --- a/guides/community-resources.md +++ b/guides/community-resources.md @@ -10,4 +10,3 @@ - [Not Your Parent's EmberData](https://runspired.com/2024/01/31/modern-ember-data.html) | *2024-01-31* by [Chris Thoburn](https://github.com/runspired) - [Adventures in WarpDrive | Cascade On Delete](https://runspired.com/2024/11/29/cascade-on-delete.html) | *2024-11-29* by [Chris Thoburn](https://github.com/runspired) -- [Exploring Transformed and Derived Values in @warp-drive/schema-record](https://runspired.com/2025/02/06/exploring-transformed-and-derivied-values-in-schema-record.html) | *2025-02-06* by [Chris Thoburn](https://github.com/runspired) diff --git a/guides/index.md b/guides/index.md index a7683f0f262..1a1a1b67863 100644 --- a/guides/index.md +++ b/guides/index.md @@ -10,7 +10,6 @@ Read [The Manual](./manual/0-index.md) - [Typescript](./typescript/index.md) - [Terminology](./terminology.md) - [Cookbook](./cookbook/index.md) -- [The Two Store Migration Approach](./migrating/two-store-migration.md) ## Community Resources diff --git a/guides/requests/examples/0-basic-usage.md b/guides/requests/examples/0-basic-usage.md index d79631a0b25..412d9a15833 100644 --- a/guides/requests/examples/0-basic-usage.md +++ b/guides/requests/examples/0-basic-usage.md @@ -68,7 +68,8 @@ Lets see how we'd approach this request. import RequestManager from '@ember-data/request'; import Fetch from '@ember-data/request/fetch'; -const fetch = new RequestManager().use([Fetch]); +const fetch = new RequestManager(); +manager.use([Fetch]); export default fetch; ``` diff --git a/guides/requests/index.md b/guides/requests/index.md index 16bfeef7687..4d74ac42a04 100644 --- a/guides/requests/index.md +++ b/guides/requests/index.md @@ -323,9 +323,13 @@ import RequestManager from '@ember-data/request'; import Fetch from '@ember-data/request/fetch'; class extends Store { - requestManager = new RequestManager() - .use([Fetch]) - .useCache(CacheHandler); + requestManager = new RequestManager(); + + constructor(args) { + super(args); + this.requestManager.use([Fetch]); + this.requestManager.useCache(CacheHandler); + } } ``` @@ -341,16 +345,19 @@ Additional handlers or a service injection like the above would need to be done consuming application in order to make broader use of `RequestManager`. ```ts -import Store from 'ember-data/store'; -import { CacheHandler } from '@ember-data/store'; +import Store, { CacheHandler } from 'ember-data/store'; import RequestManager from '@ember-data/request'; import Fetch from '@ember-data/request/fetch'; import { LegacyNetworkHandler } from '@ember-data/legacy-compat'; export default class extends Store { - requestManager = new RequestManager() - .use([LegacyNetworkHandler, Fetch]) - .useCache(CacheHandler); + requestManager = new RequestManager(); + + constructor(args) { + super(args); + this.requestManager.use([LegacyNetworkHandler, Fetch]); + this.requestManager.useCache(CacheHandler); + } } ``` diff --git a/guides/typescript/0-installation.md b/guides/typescript/0-installation.md index 725695231d4..b8e64d8b240 100644 --- a/guides/typescript/0-installation.md +++ b/guides/typescript/0-installation.md @@ -19,9 +19,7 @@ simply running the command again. For additional documentation or to manuall install and configure, continue reading the below guide. - ---- - +========================================================= > [!CAUTION] > EmberData does not maintain the DefinitelyTyped types for @@ -31,50 +29,43 @@ below guide. > [!IMPORTANT] > EmberData's Native Types require the use of Ember's > Native Types. -> + +> [!IMPORTANT] > Type definitions need to be installed top-level, this means > you have to install every EmberData package `ember-data` > depends on. > [!TIP] -> When installing packages, use an NPM dist tag to get the latest -> version for a given channel. E.g. `pnpm install ember-data@latest` -> valid channels with types are `latest`, `canary`, `v4-latest` and `v4-canary` +> When installing packages, use the `@canary` dist tag to get the latest +> version. E.g. `pnpm install ember-data@canary` There are currently two ways to gain access to EmberData's native types. -1) [Use A Version That Has Types](#using-native-types) +1) [Use Canary](#using-canary) 2) [Use Official Types Packages](#using-types-packages) with releases `>= 4.12.*` --- -### Using Versions That Supply Types - -The following versions supply their own type definitions. These type definitions will still need to be configured for use in tsconfig. - -- Versions of 4.x >= 4.13.0-alpha.0 -- Versions of 5.x >= 5.3.8 - -In order to use the types for these versions, the dependencies of `ember-data` (and their peer-dependencies) must also be added to `package.json`. - -Generally that means the following packages are needed, though you may need fewer (or more!) depending on if you have migrated away from Adapter/Serializer and replaced Model with SchemaRecord: - -| Name | Latest | Canary | V4 | -| ---- | -------| ------ | -- | -| [ember-data](https://github.com/emberjs/data/blob/main/packages/-ember-data/README.md) | ![NPM Stable Version](https://img.shields.io/npm/v/ember-data/latest?label=&color=90EE90) | ![NPM Canary Version](https://img.shields.io/npm/v/ember-data/canary?label=&color=90EE90) | ![NPM V4 Version](https://img.shields.io/npm/v/ember-data/v4-canary?label=&color=90EE90) | -| [@ember-data/adapter](https://github.com/emberjs/data/blob/main/packages/adapter/README.md) | ![NPM Stable Version](https://img.shields.io/npm/v/%40ember-data/adapter/latest?label=&color=90EE90) | ![NPM Canary Version](https://img.shields.io/npm/v/%40ember-data/adapter/canary?label=&color=90EE90) | ![NPM V4 Version](https://img.shields.io/npm/v/%40ember-data/adapter/v4-canary?label=&color=90EE90) | -| [@ember-data/graph](https://github.com/emberjs/data/blob/main/packages/graph/README.md) | ![NPM Stable Version](https://img.shields.io/npm/v/%40ember-data/graph/latest?label=&color=90EE90) | ![NPM Canary Version](https://img.shields.io/npm/v/%40ember-data/graph/canary?label=&color=90EE90) | ![NPM V4 Version](https://img.shields.io/npm/v/%40ember-data/graph/v4-canary?label=&color=90EE90) | -| [@ember-data/json-api](https://github.com/emberjs/data/blob/main/packages/json-api/README.md) | ![NPM Stable Version](https://img.shields.io/npm/v/%40ember-data/json-api/latest?label=&color=90EE90) | ![NPM Canary Version](https://img.shields.io/npm/v/%40ember-data/json-api/canary?label=&color=90EE90) | ![NPM V4 Version](https://img.shields.io/npm/v/%40ember-data/json-api/v4-canary?label=&color=90EE90) | -| [@ember-data/legacy-compat](https://github.com/emberjs/data/blob/main/packages/legacy-compat/README.md) | ![NPM Stable Version](https://img.shields.io/npm/v/%40ember-data/legacy-compat/latest?label=&color=90EE90) | ![NPM Canary Version](https://img.shields.io/npm/v/%40ember-data/legacy-compat/canary?label=&color=90EE90) | ![NPM V4 Version](https://img.shields.io/npm/v/%40ember-data/legacy-compat/v4-canary?label=&color=90EE90) | -| [@ember-data/model](https://github.com/emberjs/data/blob/main/packages/model/README.md) | ![NPM Stable Version](https://img.shields.io/npm/v/%40ember-data/model/latest?label=&color=90EE90) | ![NPM Canary Version](https://img.shields.io/npm/v/%40ember-data/model/canary?label=&color=90EE90) | ![NPM V4 Version](https://img.shields.io/npm/v/%40ember-data/model/v4-canary?label=&color=90EE90) | -| [@ember-data/request](https://github.com/emberjs/data/blob/main/packages/request/README.md) | ![NPM Stable Version](https://img.shields.io/npm/v/%40ember-data/request/latest?label=&color=90EE90) | ![NPM Canary Version](https://img.shields.io/npm/v/%40ember-data/request/canary?label=&color=90EE90) | ![NPM V4 Version](https://img.shields.io/npm/v/%40ember-data/request/v4-canary?label=&color=90EE90) | -| [@ember-data/request-utils](https://github.com/emberjs/data/blob/main/packages/request-utils/README.md) | ![NPM Stable Version](https://img.shields.io/npm/v/%40ember-data/request-utils/latest?label=&color=90EE90) | ![NPM Canary Version](https://img.shields.io/npm/v/%40ember-data/request-utils/canary?label=&color=90EE90) | ![NPM V4 Version](https://img.shields.io/npm/v/%40ember-data/request-utils/v4-canary?label=&color=90EE90) | -| [@ember-data/serializer](https://github.com/emberjs/data/blob/main/packages/serializer/README.md) | ![NPM Stable Version](https://img.shields.io/npm/v/%40ember-data/serializer/latest?label=&color=90EE90) | ![NPM Canary Version](https://img.shields.io/npm/v/%40ember-data/serializer/canary?label=&color=90EE90) | ![NPM V4 Version](https://img.shields.io/npm/v/%40ember-data/serializer/v4-canary?label=&color=90EE90) | -| [@ember-data/store](https://github.com/emberjs/data/blob/main/packages/store/README.md) | ![NPM Stable Version](https://img.shields.io/npm/v/%40ember-data/store/latest?label=&color=90EE90) | ![NPM Canary Version](https://img.shields.io/npm/v/%40ember-data/store/canary?label=&color=90EE90) | ![NPM V4 Version](https://img.shields.io/npm/v/%40ember-data/store/v4-canary?label=&color=90EE90) | -| [@ember-data/tracking](https://github.com/emberjs/data/blob/main/packages/tracking/README.md) | ![NPM Stable Version](https://img.shields.io/npm/v/%40ember-data/tracking/latest?label=&color=90EE90) | ![NPM Canary Version](https://img.shields.io/npm/v/%40ember-data/tracking/canary?label=&color=90EE90) | ![NPM V4 Version](https://img.shields.io/npm/v/%40ember-data/tracking/v4-canary?label=&color=90EE90) | -| [@warp-drive/core-types](https://github.com/emberjs/data/blob/main/packages/core-types/README.md) | ![NPM Latest Version](https://img.shields.io/npm/v/%40warp-drive/core-types/latest?label=&color=90EE90) | ![NPM Canary Version](https://img.shields.io/npm/v/%40warp-drive/core-types/canary?label=&color=90EE90) | ![NPM V4 Version](https://img.shields.io/npm/v/%40warp-drive/core-types/v4-canary?label=&color=90EE90) | +### Using Canary + +Required Packages for Canary Types + +| Name | Version | +| ---- | ------- | +| [ember-data](https://github.com/emberjs/data/blob/main/packages/-ember-data/README.md) | ![NPM Canary Version](https://img.shields.io/npm/v/ember-data/canary?label=&color=90EE90) | +| [@ember-data/adapter](https://github.com/emberjs/data/blob/main/packages/adapter/README.md) | ![NPM Canary Version](https://img.shields.io/npm/v/%40ember-data/adapter/canary?label=&color=90EE90) | +| [@ember-data/graph](https://github.com/emberjs/data/blob/main/packages/graph/README.md) | ![NPM Canary Version](https://img.shields.io/npm/v/%40ember-data/graph/canary?label=&color=90EE90) | +| [@ember-data/json-api](https://github.com/emberjs/data/blob/main/packages/json-api/README.md) | ![NPM Canary Version](https://img.shields.io/npm/v/%40ember-data/json-api/canary?label=&color=90EE90) | +| [@ember-data/legacy-compat](https://github.com/emberjs/data/blob/main/packages/legacy-compat/README.md) | ![NPM Canary Version](https://img.shields.io/npm/v/%40ember-data/legacy-compat/canary?label=&color=90EE90) | +| [@ember-data/model](https://github.com/emberjs/data/blob/main/packages/model/README.md) | ![NPM Canary Version](https://img.shields.io/npm/v/%40ember-data/model/canary?label=&color=90EE90) | +| [@ember-data/request](https://github.com/emberjs/data/blob/main/packages/request/README.md) | ![NPM Canary Version](https://img.shields.io/npm/v/%40ember-data/request/canary?label=&color=90EE90) | +| [@ember-data/request-utils](https://github.com/emberjs/data/blob/main/packages/request-utils/README.md) | ![NPM Canary Version](https://img.shields.io/npm/v/%40ember-data/request-utils/canary?label=&color=90EE90) | +| [@ember-data/serializer](https://github.com/emberjs/data/blob/main/packages/serializer/README.md) | ![NPM Canary Version](https://img.shields.io/npm/v/%40ember-data/serializer/canary?label=&color=90EE90) | +| [@ember-data/store](https://github.com/emberjs/data/blob/main/packages/store/README.md) | ![NPM Canary Version](https://img.shields.io/npm/v/%40ember-data/store/canary?label=&color=90EE90) | +| [@ember-data/tracking](https://github.com/emberjs/data/blob/main/packages/tracking/README.md) | ![NPM Canary Version](https://img.shields.io/npm/v/%40ember-data/tracking/canary?label=&color=90EE90) | +| [@warp-drive/core-types](https://github.com/emberjs/data/blob/main/packages/core-types/README.md) | ![NPM Canary Version](https://img.shields.io/npm/v/%40warp-drive/core-types/canary?label=&color=90EE90) | Here's a single install command for pnpm. Swap pnpm for yarn or npm as needed. @@ -83,7 +74,7 @@ PACKAGES=("@types/ember" "@types/ember-data" "@types/ember-data__adapter" "@type for pkg in "${PACKAGES[@]}"; do pnpm remove "$pkg"; done -pnpm install ember-data@latest @ember-data/adapter@latest @ember-data/graph@latest @ember-data/json-api@latest @ember-data/legacy-compat@latest @ember-data/model@latest @ember-data/request@latest @ember-data/request-utils@latest @ember-data/serializer@latest @ember-data/store@latest @ember-data/tracking@latest @warp-drive/core-types@latest +pnpm install ember-data@canary @ember-data/adapter@canary @ember-data/graph@canary @ember-data/json-api@canary @ember-data/legacy-compat@canary @ember-data/model@canary @ember-data/request@canary @ember-data/request-utils@canary @ember-data/serializer@canary @ember-data/store@canary @ember-data/tracking@canary @warp-drive/core-types@canary ``` Here's an example change to package.json which drops all use of types from `@types/` for both Ember and EmberData and adds the appropriate canary packages. diff --git a/guides/typescript/1-configuration.md b/guides/typescript/1-configuration.md index 1e98871a73b..15fe1586ad0 100644 --- a/guides/typescript/1-configuration.md +++ b/guides/typescript/1-configuration.md @@ -4,7 +4,7 @@ There are currently two ways to gain access to EmberData's native types. Follow the configuration guide below for the [installation](./0-installation.md) option you chose. -1) [Use A Version That Has Types](#using-native-types) +1) [Use Canary](#using-canary) 2) [Use Official Types Packages](#using-types-packages) with releases `>= 4.12.*` @@ -14,7 +14,7 @@ with releases `>= 4.12.*` > Native Types, the configuration below will also setup > Your application to consume Ember's Native Types. -### Using Native Types +### Using Canary To consume `alpha` stage types, you must import the types in your project's `tsconfig.json`. @@ -26,18 +26,18 @@ potential volatility. "compilerOptions": { + "types": [ + "ember-source/types", -+ "ember-data/unstable-preview-types", -+ "@ember-data/store/unstable-preview-types", -+ "@ember-data/adapter/unstable-preview-types", -+ "@ember-data/graph/unstable-preview-types", -+ "@ember-data/json-api/unstable-preview-types", -+ "@ember-data/legacy-compat/unstable-preview-types", -+ "@ember-data/request/unstable-preview-types", -+ "@ember-data/request-utils/unstable-preview-types", -+ "@ember-data/model/unstable-preview-types", -+ "@ember-data/serializer/unstable-preview-types", -+ "@ember-data/tracking/unstable-preview-types", -+ "@warp-drive/core-types/unstable-preview-types" ++ "./node_modules/ember-data/unstable-preview-types", ++ "./node_modules/@ember-data/store/unstable-preview-types", ++ "./node_modules/@ember-data/adapter/unstable-preview-types", ++ "./node_modules/@ember-data/graph/unstable-preview-types", ++ "./node_modules/@ember-data/json-api/unstable-preview-types", ++ "./node_modules/@ember-data/legacy-compat/unstable-preview-types", ++ "./node_modules/@ember-data/request/unstable-preview-types", ++ "./node_modules/@ember-data/request-utils/unstable-preview-types", ++ "./node_modules/@ember-data/model/unstable-preview-types", ++ "./node_modules/@ember-data/serializer/unstable-preview-types", ++ "./node_modules/@ember-data/tracking/unstable-preview-types", ++ "./node_modules/@warp-drive/core-types/unstable-preview-types" + ] } } diff --git a/guides/typescript/index.md b/guides/typescript/index.md index f38d656d78f..f9787c47ede 100644 --- a/guides/typescript/index.md +++ b/guides/typescript/index.md @@ -10,10 +10,10 @@ the following two sections --- - Installation - - [Using Versions That Supply Types](./0-installation.md#using-versions-that-supply-types) + - [Using Canary](./0-installation.md#using-canary) - [Using Types Packages](./0-installation.md#using-types-packages) - Configuration - - [Using Native Types](./1-configuration.md#using-native-types) + - [Using Canary](./1-configuration.md#using-canary) - [Using Types Packages](./1-configuration.md#using-types-packages) - Usage - [Why Brands](./2-why-brands.md) @@ -43,7 +43,7 @@ Each package in the project can choose its own stage for types. ## Contributing Type Fixes -Even though EmberData/WarpDrive is typed, what makes for good types for a project doesn't necessarily make for good types for that project's consumers (your application). +Even though EmberData is typed, what makes for good types for a project doesn't necessarily make for good types for that project's consumers (your application). Currently, TypeScript support is `alpha` largely because we expect to need to improve **a lot** of type signatures to make them more useful and correct for your app. diff --git a/package.json b/package.json index ea7696d47ba..d407a4d57c9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "root", - "version": "5.4.0-alpha.136", + "version": "4.13.0-alpha.3", "private": true, "license": "MIT", "repository": { @@ -35,6 +35,7 @@ }, "devDependencies": { "@babel/core": "^7.24.5", + "@ember/test-helpers": "3.3.0", "@glimmer/component": "^1.1.2", "@glint/core": "1.5.0", "@glint/environment-ember-loose": "1.5.0", @@ -42,7 +43,7 @@ "@glint/template": "1.5.0", "@types/semver": "^7.5.8", "badge-maker": "4.1.0", - "bun-types": "^1.2.2", + "bun-types": "^1.1.30", "chalk": "^4.1.2", "co": "^4.6.0", "command-line-args": "^5.2.1", @@ -56,7 +57,7 @@ "lerna-changelog": "^2.2.0", "prettier": "^3.3.2", "prettier-plugin-ember-template-tag": "^2.0.2", - "rimraf": "^5.0.10", + "rimraf": "^5.0.6", "semver": "^7.6.3", "silent-error": "^1.1.1", "typescript": "^5.7.2", @@ -168,13 +169,11 @@ "typescript" ] }, - "allowNonAppliedPatches": true, "patchedDependencies": { "qunit@2.19.4": "patches/qunit@2.19.4.patch", "testem@3.11.0": "patches/testem@3.11.0.patch", "@ember/test-helpers@3.3.0": "patches/@ember__test-helpers@3.3.0.patch", - "@ember/test-helpers@4.0.4": "patches/@ember__test-helpers@4.0.4.patch", - "@ember/test-helpers@5.1.0": "patches/@ember__test-helpers@5.1.0.patch" + "@ember/test-helpers@4.0.4": "patches/@ember__test-helpers@4.0.4.patch" } } } diff --git a/packages/-ember-data/app/adapters/-json-api.js b/packages/-ember-data/app/adapters/-json-api.js new file mode 100644 index 00000000000..2cbb7cd7057 --- /dev/null +++ b/packages/-ember-data/app/adapters/-json-api.js @@ -0,0 +1 @@ +export { default } from '@ember-data/adapter/json-api'; diff --git a/packages/-ember-data/app/initializers/ember-data.js b/packages/-ember-data/app/initializers/ember-data.js index 4c8e1d14e7b..a3dd9a3a6f5 100644 --- a/packages/-ember-data/app/initializers/ember-data.js +++ b/packages/-ember-data/app/initializers/ember-data.js @@ -1,12 +1,11 @@ -import '@ember-data/request-utils/deprecation-support'; +import 'ember-data'; + +import setupContainer from 'ember-data/setup-container'; /* This code initializes EmberData in an Ember application. */ export default { name: 'ember-data', - initialize(application) { - application.registerOptionsForType('serializer', { singleton: false }); - application.registerOptionsForType('adapter', { singleton: false }); - }, + initialize: setupContainer, }; diff --git a/packages/-ember-data/app/instance-initializers/ember-data.js b/packages/-ember-data/app/instance-initializers/ember-data.js new file mode 100644 index 00000000000..b48556a316c --- /dev/null +++ b/packages/-ember-data/app/instance-initializers/ember-data.js @@ -0,0 +1,5 @@ +/* exists only for things that historically used "after" or "before" */ +export default { + name: 'ember-data', + initialize() {}, +}; diff --git a/packages/-ember-data/app/serializers/-default.js b/packages/-ember-data/app/serializers/-default.js new file mode 100644 index 00000000000..d617bfb1824 --- /dev/null +++ b/packages/-ember-data/app/serializers/-default.js @@ -0,0 +1 @@ +export { default } from '@ember-data/serializer/json'; diff --git a/packages/-ember-data/app/serializers/-json-api.js b/packages/-ember-data/app/serializers/-json-api.js new file mode 100644 index 00000000000..59723c5ab2a --- /dev/null +++ b/packages/-ember-data/app/serializers/-json-api.js @@ -0,0 +1 @@ +export { default } from '@ember-data/serializer/json-api'; diff --git a/packages/-ember-data/app/serializers/-rest.js b/packages/-ember-data/app/serializers/-rest.js new file mode 100644 index 00000000000..d6878ba3c3e --- /dev/null +++ b/packages/-ember-data/app/serializers/-rest.js @@ -0,0 +1 @@ +export { default } from '@ember-data/serializer/rest'; diff --git a/packages/-ember-data/app/services/store.js b/packages/-ember-data/app/services/store.js index 94f7019f584..043aebdc25a 100644 --- a/packages/-ember-data/app/services/store.js +++ b/packages/-ember-data/app/services/store.js @@ -1,10 +1,11 @@ import { deprecate } from '@ember/debug'; export { default } from 'ember-data/store'; +import { DISABLE_6X_DEPRECATIONS } from '@warp-drive/build-config/deprecations'; deprecate( "You are relying on ember-data auto-magically installing the store service. Use `export { default } from 'ember-data/store';` in app/services/store.js instead", - false, + /* inline-macro-config */ DISABLE_6X_DEPRECATIONS, { id: 'ember-data:deprecate-legacy-imports', for: 'ember-data', diff --git a/packages/-ember-data/app/transforms/boolean.js b/packages/-ember-data/app/transforms/boolean.js index be707e054f2..764286175bc 100644 --- a/packages/-ember-data/app/transforms/boolean.js +++ b/packages/-ember-data/app/transforms/boolean.js @@ -1,10 +1,12 @@ import { deprecate } from '@ember/debug'; +import { DISABLE_6X_DEPRECATIONS } from '@warp-drive/build-config/deprecations'; + export { BooleanTransform as default } from '@ember-data/serializer/transform'; deprecate( "You are relying on ember-data auto-magically installing the BooleanTransform. Use `export { BooleanTransform as default } from '@ember-data/serializer/transform';` in app/transforms/boolean.js instead", - false, + /* inline-macro-config */ DISABLE_6X_DEPRECATIONS, { id: 'ember-data:deprecate-legacy-imports', for: 'ember-data', diff --git a/packages/-ember-data/app/transforms/date.js b/packages/-ember-data/app/transforms/date.js index 1ba53723825..187c01d2e40 100644 --- a/packages/-ember-data/app/transforms/date.js +++ b/packages/-ember-data/app/transforms/date.js @@ -1,10 +1,12 @@ import { deprecate } from '@ember/debug'; +import { DISABLE_6X_DEPRECATIONS } from '@warp-drive/build-config/deprecations'; + export { DateTransform as default } from '@ember-data/serializer/transform'; deprecate( "You are relying on ember-data auto-magically installing the DateTransform. Use `export { DateTransform as default } from '@ember-data/serializer/transform';` in app/transforms/date.js instead", - false, + /* inline-macro-config */ DISABLE_6X_DEPRECATIONS, { id: 'ember-data:deprecate-legacy-imports', for: 'ember-data', diff --git a/packages/-ember-data/app/transforms/number.js b/packages/-ember-data/app/transforms/number.js index e33eac7b1a4..c79ef0d21dd 100644 --- a/packages/-ember-data/app/transforms/number.js +++ b/packages/-ember-data/app/transforms/number.js @@ -1,10 +1,12 @@ import { deprecate } from '@ember/debug'; +import { DISABLE_6X_DEPRECATIONS } from '@warp-drive/build-config/deprecations'; + export { NumberTransform as default } from '@ember-data/serializer/transform'; deprecate( "You are relying on ember-data auto-magically installing the NumberTransform. Use `export { NumberTransform as default } from '@ember-data/serializer/transform';` in app/transforms/number.js instead", - false, + /* inline-macro-config */ DISABLE_6X_DEPRECATIONS, { id: 'ember-data:deprecate-legacy-imports', for: 'ember-data', diff --git a/packages/-ember-data/app/transforms/string.js b/packages/-ember-data/app/transforms/string.js index 95d95453178..929095af070 100644 --- a/packages/-ember-data/app/transforms/string.js +++ b/packages/-ember-data/app/transforms/string.js @@ -1,10 +1,12 @@ import { deprecate } from '@ember/debug'; +import { DISABLE_6X_DEPRECATIONS } from '@warp-drive/build-config/deprecations'; + export { StringTransform as default } from '@ember-data/serializer/transform'; deprecate( "You are relying on ember-data auto-magically installing the StringTransform. Use `export { StringTransform as default } from '@ember-data/serializer/transform';` in app/transforms/string.js instead", - false, + /* inline-macro-config */ DISABLE_6X_DEPRECATIONS, { id: 'ember-data:deprecate-legacy-imports', for: 'ember-data', diff --git a/packages/-ember-data/package.json b/packages/-ember-data/package.json index 5952688a1be..39848b6e609 100644 --- a/packages/-ember-data/package.json +++ b/packages/-ember-data/package.json @@ -1,6 +1,6 @@ { "name": "ember-data", - "version": "5.4.0-alpha.136", + "version": "4.13.0-alpha.3", "description": "The lightweight reactive data library for JavaScript applications", "keywords": [ "ember-addon" @@ -120,13 +120,13 @@ "@ember-data/store": "workspace:*", "@ember-data/tracking": "workspace:*", "@ember/edition-utils": "^1.2.0", - "@embroider/macros": "^1.16.10", + "@embroider/macros": "^1.16.6", "@warp-drive/core-types": "workspace:*", "@warp-drive/build-config": "workspace:*" }, "peerDependencies": { "ember-source": "3.28.12 || ^4.0.4 || ^5.0.0 || ^6.0.0", - "@ember/test-helpers": "^3.3.0 || ^4.0.4 || ^5.1.0", + "@ember/test-helpers": "^3.3.0 || ^4.0.4", "@ember/test-waiters": "^3.1.0 || ^4.0.0", "qunit": "^2.18.0" }, @@ -150,7 +150,7 @@ "@glimmer/component": "^1.1.2", "@glimmer/tracking": "^1.1.2", "@types/qunit": "2.19.10", - "@ember/test-helpers": "5.1.0", + "@ember/test-helpers": "4.0.4", "@warp-drive/internal-config": "workspace:*", "ember-source": "~5.12.0", "eslint": "^9.12.0", diff --git a/packages/-ember-data/src/-private/index.ts b/packages/-ember-data/src/-private/index.ts index d677821855a..ef3ed492d25 100644 --- a/packages/-ember-data/src/-private/index.ts +++ b/packages/-ember-data/src/-private/index.ts @@ -4,15 +4,21 @@ import { deprecate } from '@ember/debug'; import PromiseProxyMixin from '@ember/object/promise-proxy-mixin'; import ObjectProxy from '@ember/object/proxy'; -deprecate('Importing from `ember-data/-private` is deprecated without replacement.', false, { - id: 'ember-data:deprecate-legacy-imports', - for: 'ember-data', - until: '6.0', - since: { - enabled: '5.2', - available: '4.13', - }, -}); +import { DISABLE_6X_DEPRECATIONS } from '@warp-drive/build-config/deprecations'; + +deprecate( + 'Importing from `ember-data/-private` is deprecated without replacement.', + /* inline-macro-config */ DISABLE_6X_DEPRECATIONS, + { + id: 'ember-data:deprecate-legacy-imports', + for: 'ember-data', + until: '6.0', + since: { + enabled: '5.2', + available: '4.13', + }, + } +); export { default as Store } from '../store'; diff --git a/packages/-ember-data/src/adapter.ts b/packages/-ember-data/src/adapter.ts index 86e9e39f16c..7f870c613c2 100644 --- a/packages/-ember-data/src/adapter.ts +++ b/packages/-ember-data/src/adapter.ts @@ -1,10 +1,12 @@ import { deprecate } from '@ember/debug'; +import { DISABLE_6X_DEPRECATIONS } from '@warp-drive/build-config/deprecations'; + export { default } from '@ember-data/adapter'; deprecate( 'Importing from `ember-data/adapter` is deprecated. Please import from `@ember-data/adapter` instead.', - false, + /* inline-macro-config */ DISABLE_6X_DEPRECATIONS, { id: 'ember-data:deprecate-legacy-imports', for: 'ember-data', diff --git a/packages/-ember-data/src/adapters/errors.ts b/packages/-ember-data/src/adapters/errors.ts index ef0abcbe475..ffa8d144a6a 100644 --- a/packages/-ember-data/src/adapters/errors.ts +++ b/packages/-ember-data/src/adapters/errors.ts @@ -1,5 +1,7 @@ import { deprecate } from '@ember/debug'; +import { DISABLE_6X_DEPRECATIONS } from '@warp-drive/build-config/deprecations'; + export { AbortError, default as AdapterError, @@ -10,11 +12,13 @@ export { ServerError, TimeoutError, UnauthorizedError, + errorsArrayToHash, + errorsHashToArray, } from '@ember-data/adapter/error'; deprecate( 'Importing from `ember-data/adapters/errors` is deprecated. Please import from `@ember-data/adapter` instead.', - false, + /* inline-macro-config */ DISABLE_6X_DEPRECATIONS, { id: 'ember-data:deprecate-legacy-imports', for: 'ember-data', diff --git a/packages/-ember-data/src/adapters/json-api.ts b/packages/-ember-data/src/adapters/json-api.ts index 5b02150cc2b..d056eea91f9 100644 --- a/packages/-ember-data/src/adapters/json-api.ts +++ b/packages/-ember-data/src/adapters/json-api.ts @@ -1,10 +1,12 @@ import { deprecate } from '@ember/debug'; +import { DISABLE_6X_DEPRECATIONS } from '@warp-drive/build-config/deprecations'; + export { default } from '@ember-data/adapter/json-api'; deprecate( 'Importing from `ember-data/adapters/json-api` is deprecated. Please import from `@ember-data/adapter/json-api` instead.', - false, + /* inline-macro-config */ DISABLE_6X_DEPRECATIONS, { id: 'ember-data:deprecate-legacy-imports', for: 'ember-data', diff --git a/packages/-ember-data/src/adapters/rest.ts b/packages/-ember-data/src/adapters/rest.ts index 3e8d5887c90..7bea4ec2261 100644 --- a/packages/-ember-data/src/adapters/rest.ts +++ b/packages/-ember-data/src/adapters/rest.ts @@ -1,10 +1,12 @@ import { deprecate } from '@ember/debug'; +import { DISABLE_6X_DEPRECATIONS } from '@warp-drive/build-config/deprecations'; + export { default } from '@ember-data/adapter/rest'; deprecate( 'Importing from `ember-data/adapters/rest` is deprecated. Please import from `@ember-data/adapter/rest` instead.', - false, + /* inline-macro-config */ DISABLE_6X_DEPRECATIONS, { id: 'ember-data:deprecate-legacy-imports', for: 'ember-data', diff --git a/packages/-ember-data/src/attr.ts b/packages/-ember-data/src/attr.ts index d4d29c0318a..93b5cd32c1e 100644 --- a/packages/-ember-data/src/attr.ts +++ b/packages/-ember-data/src/attr.ts @@ -1,13 +1,19 @@ import { deprecate } from '@ember/debug'; +import { DISABLE_6X_DEPRECATIONS } from '@warp-drive/build-config/deprecations'; + export { attr as default } from '@ember-data/model'; -deprecate('Importing from `ember-data/attr` is deprecated. Please import from `@ember-data/model` instead.', false, { - id: 'ember-data:deprecate-legacy-imports', - for: 'ember-data', - until: '6.0', - since: { - enabled: '5.2', - available: '4.13', - }, -}); +deprecate( + 'Importing from `ember-data/attr` is deprecated. Please import from `@ember-data/model` instead.', + /* inline-macro-config */ DISABLE_6X_DEPRECATIONS, + { + id: 'ember-data:deprecate-legacy-imports', + for: 'ember-data', + until: '6.0', + since: { + enabled: '5.2', + available: '4.13', + }, + } +); diff --git a/packages/-ember-data/src/index.ts b/packages/-ember-data/src/index.ts index 6db2ee494c9..87d64f7b851 100644 --- a/packages/-ember-data/src/index.ts +++ b/packages/-ember-data/src/index.ts @@ -185,6 +185,7 @@ import Transform, { NumberTransform, StringTransform, } from '@ember-data/serializer/transform'; +import { DISABLE_6X_DEPRECATIONS } from '@warp-drive/build-config/deprecations'; import { DS, @@ -201,7 +202,7 @@ import setupContainer from './setup-container'; deprecate( 'Importing from `ember-data` is deprecated. Please import from the appropriate `@ember-data/*` instead.', - false, + /* inline-macro-config */ DISABLE_6X_DEPRECATIONS, { id: 'ember-data:deprecate-legacy-imports', for: 'ember-data', diff --git a/packages/-ember-data/src/model.ts b/packages/-ember-data/src/model.ts index f27c9832bca..4164b3a5dd1 100644 --- a/packages/-ember-data/src/model.ts +++ b/packages/-ember-data/src/model.ts @@ -1,13 +1,19 @@ import { deprecate } from '@ember/debug'; +import { DISABLE_6X_DEPRECATIONS } from '@warp-drive/build-config/deprecations'; + export { default } from '@ember-data/model'; -deprecate('Importing from `ember-data/model` is deprecated. Please import from `@ember-data/model` instead.', false, { - id: 'ember-data:deprecate-legacy-imports', - for: 'ember-data', - until: '6.0', - since: { - enabled: '5.2', - available: '4.13', - }, -}); +deprecate( + 'Importing from `ember-data/model` is deprecated. Please import from `@ember-data/model` instead.', + /* inline-macro-config */ DISABLE_6X_DEPRECATIONS, + { + id: 'ember-data:deprecate-legacy-imports', + for: 'ember-data', + until: '6.0', + since: { + enabled: '5.2', + available: '4.13', + }, + } +); diff --git a/packages/-ember-data/src/relationships.ts b/packages/-ember-data/src/relationships.ts index 24578d4944c..5c5cabfbf14 100644 --- a/packages/-ember-data/src/relationships.ts +++ b/packages/-ember-data/src/relationships.ts @@ -1,10 +1,12 @@ import { deprecate } from '@ember/debug'; +import { DISABLE_6X_DEPRECATIONS } from '@warp-drive/build-config/deprecations'; + export { belongsTo, hasMany } from '@ember-data/model'; deprecate( 'Importing from `ember-data/relationships` is deprecated. Please import from `@ember-data/model` instead.', - false, + /* inline-macro-config */ DISABLE_6X_DEPRECATIONS, { id: 'ember-data:deprecate-legacy-imports', for: 'ember-data', diff --git a/packages/-ember-data/src/serializer.ts b/packages/-ember-data/src/serializer.ts index 33c1b590691..6c878eaeccb 100644 --- a/packages/-ember-data/src/serializer.ts +++ b/packages/-ember-data/src/serializer.ts @@ -1,10 +1,12 @@ import { deprecate } from '@ember/debug'; +import { DISABLE_6X_DEPRECATIONS } from '@warp-drive/build-config/deprecations'; + export { default } from '@ember-data/serializer'; deprecate( 'Importing from `ember-data/serializer` is deprecated. Please import from `@ember-data/serializer` instead.', - false, + /* inline-macro-config */ DISABLE_6X_DEPRECATIONS, { id: 'ember-data:deprecate-legacy-imports', for: 'ember-data', diff --git a/packages/-ember-data/src/serializers/embedded-records-mixin.ts b/packages/-ember-data/src/serializers/embedded-records-mixin.ts index 8cc22d64b6f..6e5ad9a040f 100644 --- a/packages/-ember-data/src/serializers/embedded-records-mixin.ts +++ b/packages/-ember-data/src/serializers/embedded-records-mixin.ts @@ -1,10 +1,12 @@ import { deprecate } from '@ember/debug'; +import { DISABLE_6X_DEPRECATIONS } from '@warp-drive/build-config/deprecations'; + export { EmbeddedRecordsMixin as default } from '@ember-data/serializer/rest'; deprecate( 'Importing from `ember-data/serializers/embedded-records-mixin` is deprecated. Please import from `@ember-data/serializer/rest` instead.', - false, + /* inline-macro-config */ DISABLE_6X_DEPRECATIONS, { id: 'ember-data:deprecate-legacy-imports', for: 'ember-data', diff --git a/packages/-ember-data/src/serializers/json-api.ts b/packages/-ember-data/src/serializers/json-api.ts index 272e7f8deb5..cd8bb715d8f 100644 --- a/packages/-ember-data/src/serializers/json-api.ts +++ b/packages/-ember-data/src/serializers/json-api.ts @@ -1,10 +1,12 @@ import { deprecate } from '@ember/debug'; +import { DISABLE_6X_DEPRECATIONS } from '@warp-drive/build-config/deprecations'; + export { default } from '@ember-data/serializer/json-api'; deprecate( 'Importing from `ember-data/serializers/json-api` is deprecated. Please import from `@ember-data/serializer/json-api` instead.', - false, + /* inline-macro-config */ DISABLE_6X_DEPRECATIONS, { id: 'ember-data:deprecate-legacy-imports', for: 'ember-data', diff --git a/packages/-ember-data/src/serializers/json.ts b/packages/-ember-data/src/serializers/json.ts index 3148ab0ff37..46fccf1e318 100644 --- a/packages/-ember-data/src/serializers/json.ts +++ b/packages/-ember-data/src/serializers/json.ts @@ -1,10 +1,12 @@ import { deprecate } from '@ember/debug'; +import { DISABLE_6X_DEPRECATIONS } from '@warp-drive/build-config/deprecations'; + export { default } from '@ember-data/serializer/json'; deprecate( 'Importing from `ember-data/serializers/json` is deprecated. Please import from `@ember-data/serializer/json` instead.', - false, + /* inline-macro-config */ DISABLE_6X_DEPRECATIONS, { id: 'ember-data:deprecate-legacy-imports', for: 'ember-data', diff --git a/packages/-ember-data/src/serializers/rest.ts b/packages/-ember-data/src/serializers/rest.ts index 41743413946..4c65661a043 100644 --- a/packages/-ember-data/src/serializers/rest.ts +++ b/packages/-ember-data/src/serializers/rest.ts @@ -1,10 +1,12 @@ import { deprecate } from '@ember/debug'; +import { DISABLE_6X_DEPRECATIONS } from '@warp-drive/build-config/deprecations'; + export { default } from '@ember-data/serializer/rest'; deprecate( 'Importing from `ember-data/serializers/rest` is deprecated. Please import from `@ember-data/serializer/rest` instead.', - false, + /* inline-macro-config */ DISABLE_6X_DEPRECATIONS, { id: 'ember-data:deprecate-legacy-imports', for: 'ember-data', diff --git a/packages/-ember-data/src/setup-container.ts b/packages/-ember-data/src/setup-container.ts index f6c010d3548..00a519ce955 100644 --- a/packages/-ember-data/src/setup-container.ts +++ b/packages/-ember-data/src/setup-container.ts @@ -1,6 +1,8 @@ import type Application from '@ember/application'; import { deprecate } from '@ember/debug'; +import { DISABLE_6X_DEPRECATIONS } from '@warp-drive/build-config/deprecations'; + function initializeStore(application: Application) { application.registerOptionsForType('serializer', { singleton: false }); application.registerOptionsForType('adapter', { singleton: false }); @@ -10,12 +12,16 @@ export default function setupContainer(application: Application) { initializeStore(application); } -deprecate('Importing from `ember-data/setup-container` is deprecated without replacement', false, { - id: 'ember-data:deprecate-legacy-imports', - for: 'ember-data', - until: '6.0', - since: { - enabled: '5.2', - available: '4.13', - }, -}); +deprecate( + 'Importing from `ember-data/setup-container` is deprecated without replacement', + /* inline-macro-config */ DISABLE_6X_DEPRECATIONS, + { + id: 'ember-data:deprecate-legacy-imports', + for: 'ember-data', + until: '6.0', + since: { + enabled: '5.2', + available: '4.13', + }, + } +); diff --git a/packages/-ember-data/src/store.ts b/packages/-ember-data/src/store.ts index 7482209de23..d750281c67e 100644 --- a/packages/-ember-data/src/store.ts +++ b/packages/-ember-data/src/store.ts @@ -24,6 +24,12 @@ function hasRequestManager(store: BaseStore): boolean { return 'requestManager' in store; } +// FIXME @ember-data/store +// may also need to do all of this configuration +// because in 4.12 we had not yet caused it to be +// required to use `ember-data/store` to get the configured +// store except in the case of RequestManager. +// so for instance in tests new Store would mostly just work (tm) export default class Store extends BaseStore { declare _fetchManager: FetchManager; diff --git a/packages/-ember-data/src/transform.ts b/packages/-ember-data/src/transform.ts index ca0fb52b174..c9124e01d93 100644 --- a/packages/-ember-data/src/transform.ts +++ b/packages/-ember-data/src/transform.ts @@ -1,10 +1,12 @@ import { deprecate } from '@ember/debug'; +import { DISABLE_6X_DEPRECATIONS } from '@warp-drive/build-config/deprecations'; + export { default } from '@ember-data/serializer/transform'; deprecate( 'Importing from `ember-data/transform` is deprecated. Please import from `@ember-data/serializer/transform` instead.', - false, + /* inline-macro-config */ DISABLE_6X_DEPRECATIONS, { id: 'ember-data:deprecate-legacy-imports', for: 'ember-data', diff --git a/packages/active-record/package.json b/packages/active-record/package.json index d46b1922f6c..998f940fd86 100644 --- a/packages/active-record/package.json +++ b/packages/active-record/package.json @@ -1,7 +1,7 @@ { "name": "@ember-data/active-record", "description": "ActiveRecord Format Support for EmberData", - "version": "5.4.0-alpha.136", + "version": "4.13.0-alpha.3", "private": false, "license": "MIT", "author": "Chris Thoburn ", @@ -45,7 +45,7 @@ "version": 2 }, "dependencies": { - "@embroider/macros": "^1.16.10", + "@embroider/macros": "^1.16.6", "@warp-drive/build-config": "workspace:*" }, "peerDependencies": { diff --git a/packages/adapter/README.md b/packages/adapter/README.md index d153408105b..90d98d724f4 100644 --- a/packages/adapter/README.md +++ b/packages/adapter/README.md @@ -60,9 +60,13 @@ import RequestManager from '@ember-data/request'; import { LegacyNetworkHandler } from '@ember-data/legacy-compat'; export default class extends Store { - requestManager = new RequestManager() - .use([LegacyNetworkHandler]) - .useCache(CacheHandler); + requestManager = new RequestManager(); + + constructor(args) { + super(args); + this.requestManager.use([LegacyNetworkHandler]); + this.requestManager.useCache(CacheHandler); + } } ``` diff --git a/packages/adapter/package.json b/packages/adapter/package.json index 3c95fd24b91..20c57b23e6e 100644 --- a/packages/adapter/package.json +++ b/packages/adapter/package.json @@ -1,6 +1,6 @@ { "name": "@ember-data/adapter", - "version": "5.4.0-alpha.136", + "version": "4.13.0-alpha.3", "description": "Provides Legacy JSON:API and REST Implementations of the Adapter Interface for use with @ember-data/store", "keywords": [ "ember-addon" @@ -84,7 +84,7 @@ } }, "dependencies": { - "@embroider/macros": "^1.16.10", + "@embroider/macros": "^1.16.6", "ember-cli-test-info": "^1.0.0", "ember-cli-string-utils": "^1.1.0", "ember-cli-path-utils": "^1.0.0", @@ -104,7 +104,7 @@ "@ember-data/tracking": "workspace:*", "@ember/test-waiters": "^3.1.0", "@glimmer/component": "^1.1.2", - "decorator-transforms": "^2.3.0", + "decorator-transforms": "^2.2.2", "@types/jquery": "^3.5.30", "@warp-drive/core-types": "workspace:*", "@warp-drive/internal-config": "workspace:*", diff --git a/packages/adapter/src/error.js b/packages/adapter/src/error.js index f19e7149b9d..d21456c1334 100644 --- a/packages/adapter/src/error.js +++ b/packages/adapter/src/error.js @@ -1,6 +1,9 @@ /** @module @ember-data/adapter/error */ +import { deprecate } from '@ember/debug'; + +import { DEPRECATE_HELPERS } from '@warp-drive/build-config/deprecations'; import { assert } from '@warp-drive/build-config/macros'; import { getOrSetGlobal } from '@warp-drive/core-types/-private'; @@ -353,3 +356,161 @@ export const ServerError = getOrSetGlobal( extend(AdapterError, 'The adapter operation failed due to a server error') ); ServerError.prototype.code = 'ServerError'; + +function makeArray(value) { + return Array.isArray(value) ? value : [value]; +} + +const SOURCE_POINTER_REGEXP = /^\/?data\/(attributes|relationships)\/(.*)/; +const SOURCE_POINTER_PRIMARY_REGEXP = /^\/?data/; +const PRIMARY_ATTRIBUTE_KEY = 'base'; +/** + Convert an hash of errors into an array with errors in JSON-API format. + ```javascript + import { errorsHashToArray } from '@ember-data/adapter/error'; + + let errors = { + base: 'Invalid attributes on saving this record', + name: 'Must be present', + age: ['Must be present', 'Must be a number'] + }; + let errorsArray = errorsHashToArray(errors); + // [ + // { + // title: "Invalid Document", + // detail: "Invalid attributes on saving this record", + // source: { pointer: "/data" } + // }, + // { + // title: "Invalid Attribute", + // detail: "Must be present", + // source: { pointer: "/data/attributes/name" } + // }, + // { + // title: "Invalid Attribute", + // detail: "Must be present", + // source: { pointer: "/data/attributes/age" } + // }, + // { + // title: "Invalid Attribute", + // detail: "Must be a number", + // source: { pointer: "/data/attributes/age" } + // } + // ] + ``` + @method errorsHashToArray + @for @ember-data/adapter/error + @static + @deprecated + @public + @param {Object} errors hash with errors as properties + @return {Array} array of errors in JSON-API format +*/ +export function errorsHashToArray(errors) { + if (DEPRECATE_HELPERS) { + deprecate(`errorsHashToArray helper has been deprecated.`, false, { + id: 'ember-data:deprecate-errors-hash-to-array-helper', + for: 'ember-data', + until: '5.0', + since: { available: '4.7', enabled: '4.7' }, + }); + const out = []; + + if (errors) { + Object.keys(errors).forEach((key) => { + const messages = makeArray(errors[key]); + for (let i = 0; i < messages.length; i++) { + let title = 'Invalid Attribute'; + let pointer = `/data/attributes/${key}`; + if (key === PRIMARY_ATTRIBUTE_KEY) { + title = 'Invalid Document'; + pointer = `/data`; + } + out.push({ + title: title, + detail: messages[i], + source: { + pointer: pointer, + }, + }); + } + }); + } + + return out; + } + assert(`errorsHashToArray helper has been removed`); +} + +/** + Convert an array of errors in JSON-API format into an object. + + ```javascript + import { errorsArrayToHash } from '@ember-data/adapter/error'; + + let errorsArray = [ + { + title: 'Invalid Attribute', + detail: 'Must be present', + source: { pointer: '/data/attributes/name' } + }, + { + title: 'Invalid Attribute', + detail: 'Must be present', + source: { pointer: '/data/attributes/age' } + }, + { + title: 'Invalid Attribute', + detail: 'Must be a number', + source: { pointer: '/data/attributes/age' } + } + ]; + + let errors = errorsArrayToHash(errorsArray); + // { + // "name": ["Must be present"], + // "age": ["Must be present", "must be a number"] + // } + ``` + + @method errorsArrayToHash + @static + @for @ember-data/adapter/error + @deprecated + @public + @param {Array} errors array of errors in JSON-API format + @return {Object} +*/ +export function errorsArrayToHash(errors) { + if (DEPRECATE_HELPERS) { + deprecate(`errorsArrayToHash helper has been deprecated.`, false, { + id: 'ember-data:deprecate-errors-array-to-hash-helper', + for: 'ember-data', + until: '5.0', + since: { available: '4.7', enabled: '4.7' }, + }); + const out = {}; + + if (errors) { + errors.forEach((error) => { + if (error.source && error.source.pointer) { + let key = error.source.pointer.match(SOURCE_POINTER_REGEXP); + + if (key) { + key = key[2]; + } else if (error.source.pointer.search(SOURCE_POINTER_PRIMARY_REGEXP) !== -1) { + key = PRIMARY_ATTRIBUTE_KEY; + } + + if (key) { + out[key] = out[key] || []; + out[key].push(error.detail || error.title); + } + } + }); + } + + return out; + } + assert(`errorsArrayToHash helper has been removed`); +} diff --git a/packages/build-config/package.json b/packages/build-config/package.json index ddc6bafad23..4b51db38a69 100644 --- a/packages/build-config/package.json +++ b/packages/build-config/package.json @@ -1,6 +1,6 @@ { "name": "@warp-drive/build-config", - "version": "5.4.0-alpha.136", + "version": "4.13.0-alpha.3", "description": "Provides Build Configuration for projects using WarpDrive or EmberData", "keywords": [ "ember-data", @@ -41,8 +41,8 @@ } }, "dependencies": { - "@embroider/macros": "^1.16.10", - "@embroider/addon-shim": "^1.9.0", + "@embroider/macros": "^1.16.6", + "@embroider/addon-shim": "^1.8.9", "babel-import-util": "^2.1.1", "broccoli-funnel": "^3.0.8", "semver": "^7.6.3" @@ -56,7 +56,7 @@ "@babel/core": "^7.24.5", "pnpm-sync-dependencies-meta-injected": "0.0.14", "typescript": "^5.7.2", - "bun-types": "^1.2.2", + "bun-types": "^1.1.30", "vite": "^5.2.11" }, "engines": { diff --git a/packages/build-config/src/-private/utils/deprecations.ts b/packages/build-config/src/-private/utils/deprecations.ts index 1a8862e389b..621b68e0c83 100644 --- a/packages/build-config/src/-private/utils/deprecations.ts +++ b/packages/build-config/src/-private/utils/deprecations.ts @@ -8,7 +8,7 @@ function deprecationIsResolved(deprecatedSince: MajorMinor, compatVersion: Major return semver.lte(semver.minVersion(deprecatedSince)!, semver.minVersion(compatVersion)!); } -const NextMajorVersion = '6.'; +const NextMajorVersion = '5.'; function deprecationIsNextMajorCycle(deprecatedSince: MajorMinor) { return deprecatedSince.startsWith(NextMajorVersion); @@ -20,17 +20,17 @@ export function getDeprecations( ): { [key in DeprecationFlag]: boolean } { const flags = {} as Record; const keys = Object.keys(CURRENT_DEPRECATIONS) as DeprecationFlag[]; - const DISABLE_7X_DEPRECATIONS = deprecations?.DISABLE_7X_DEPRECATIONS ?? true; + const DISABLE_6X_DEPRECATIONS = deprecations?.DISABLE_6X_DEPRECATIONS ?? true; keys.forEach((flag) => { const deprecatedSince = CURRENT_DEPRECATIONS[flag]; - const isDeactivatedDeprecationNotice = DISABLE_7X_DEPRECATIONS && deprecationIsNextMajorCycle(deprecatedSince); + const isDeactivatedDeprecationNotice = DISABLE_6X_DEPRECATIONS && deprecationIsNextMajorCycle(deprecatedSince); let flagState = true; // default to no code-stripping if (!isDeactivatedDeprecationNotice) { // if we have a specific flag setting, use it if (typeof deprecations?.[flag] === 'boolean') { - flagState = deprecations?.[flag]!; + flagState = deprecations?.[flag]; } else if (compatVersion) { // if we are told we are compatible with a version // we check if we can strip this flag diff --git a/packages/build-config/src/debugging.ts b/packages/build-config/src/debugging.ts index ce19966afe2..60d6a2880c5 100644 --- a/packages/build-config/src/debugging.ts +++ b/packages/build-config/src/debugging.ts @@ -103,11 +103,3 @@ export const LOG_GRAPH: boolean = false; * @public */ export const LOG_INSTANCE_CACHE: boolean = false; -/** - * Log key count metrics, useful for performance - * debugging. - * - * @property {boolean} LOG_METRIC_COUNTS - * @public - */ -export const LOG_METRIC_COUNTS: boolean = false; diff --git a/packages/build-config/src/deprecation-versions.ts b/packages/build-config/src/deprecation-versions.ts index 4f9389e1b09..80d5148b4d3 100644 --- a/packages/build-config/src/deprecation-versions.ts +++ b/packages/build-config/src/deprecation-versions.ts @@ -88,6 +88,621 @@ * @public */ export const DEPRECATE_CATCH_ALL = '99.0'; + +/** + * **id: ember-data:rsvp-unresolved-async** + * + * Deprecates when a request promise did not resolve prior to the store tearing down. + * + * Note: in most cases even with the promise guard that is now being deprecated + * a test crash would still be encountered. + * + * To resolve: Tests or Fastboot instances which crash need to find triggers requests + * and properly await them before tearing down. + * + * @property DEPRECATE_RSVP_PROMISE + * @since 4.4 + * @until 5.0 + * @public + */ +export const DEPRECATE_RSVP_PROMISE = '4.4'; + +/** + * **id: ember-data:model-save-promise** + * + * Affects + * - model.save / store.saveRecord + * - model.reload + * + * Deprecates the promise-proxy returned by these methods in favor of + * a Promise return value. + * + * To resolve this deprecation, `await` or `.then` the return value + * before doing work with the result instead of accessing values via + * the proxy. + * + * To continue utilizing flags such as `isPending` in your templates + * consider using [ember-promise-helpers](https://github.com/fivetanley/ember-promise-helpers) + * + * @property DEPRECATE_SAVE_PROMISE_ACCESS + * @since 4.4 + * @until 5.0 + * @public + */ +export const DEPRECATE_SAVE_PROMISE_ACCESS = '4.4'; + +/** + * **id: ember-data:deprecate-snapshot-model-class-access** + * + * Deprecates accessing the factory class for a given resource type + * via properties on various classes. + * + * Guards + * + * - SnapshotRecordArray.type + * - Snapshot.type + * - RecordArray.type + * + * Use `store.modelFor()` instead. + * + * @property DEPRECATE_SNAPSHOT_MODEL_CLASS_ACCESS + * @since 4.5 + * @until 5.0 + * @public + */ +export const DEPRECATE_SNAPSHOT_MODEL_CLASS_ACCESS = '4.5'; + +/** + * **id: ember-data:deprecate-store-find** + * + * Deprecates using `store.find` instead of `store.findRecord`. Typically + * `store.find` is a mistaken call that occurs when using implicit route behaviors + * in Ember which attempt to derive how to load data via parsing the route params + * for a route which does not implement a `model` hook. + * + * To resolve, use `store.findRecord`. This may require implementing an associated + * route's `model() {}` hook. + * + * @property DEPRECATE_STORE_FIND + * @since 4.5 + * @until 5.0 + * @public + */ +export const DEPRECATE_STORE_FIND = '4.5'; + +/** + * **id: ember-data:deprecate-has-record-for-id** + * + * Deprecates `store.hasRecordForId(type, id)` in favor of `store.peekRecord({ type, id }) !== null`. + * + * Broadly speaking, while the ability to query for presence is important, a key distinction exists + * between these methods that make relying on `hasRecordForId` unsafe, as it may report `true` for a + * record which is not-yet loaded and un-peekable. `peekRecord` offers a safe mechanism by which to check + * for whether a record is present in a usable manner. + * + * @property DEPRECATE_HAS_RECORD + * @since 4.5 + * @until 5.0 + * @public + */ +export const DEPRECATE_HAS_RECORD = '4.5'; + +/** + * **id: ember-data:deprecate-string-arg-schemas** + * + * Deprecates `schema.attributesDefinitionFor(type)` and + * `schema.relationshipsDefinitionFor(type)` in favor of + * a consistent object signature (`identifier | { type }`). + * + * To resolve change + * + * ```diff + * - store.getSchemaDefinitionService().attributesDefinitionFor('user') + * + store.getSchemaDefinitionService().attributesDefinitionFor({ type: 'user' }) + * ``` + * + * @property DEPRECATE_STRING_ARG_SCHEMAS + * @since 4.5 + * @until 5.0 + * @public + */ +export const DEPRECATE_STRING_ARG_SCHEMAS = '4.5'; + +/** + * **id: ember-data:deprecate-secret-adapter-fallback** + * + * Deprecates the secret `-json-api` fallback adapter in favor + * or an explicit "catch all" application adapter. In addition + * to this deprecation ensuring the user has explicitly chosen an + * adapter, this ensures that the user may choose to use no adapter + * at all. + * + * Simplest fix: + * + * */app/adapters/application.js* + * ```js + * export { default } from '@ember-data/adapter/json-api'; + * ``` + * + * @property DEPRECATE_JSON_API_FALLBACK + * @since 4.5 + * @until 5.0 + * @public + */ +export const DEPRECATE_JSON_API_FALLBACK = '4.5'; + +/** + * **id: ember-data:deprecate-model-reopen** + * + * ---- + * + * For properties known ahead of time, instead of + * + * ```ts + * class User extends Model { @attr firstName; } + * + * User.reopen({ lastName: attr() }); + * ``` + * + * Extend `User` again or include it in the initial definition. + * + * ```ts + * class User extends Model { @attr firstName; @attr lastName } + * ``` + * + * For properties generated dynamically, consider registering + * a `SchemaDefinitionService` with the store , as such services + * are capable of dynamically adjusting their schemas, and utilize + * the `instantiateRecord` hook to create a Proxy based class that + * can react to the changes in the schema. + * + * + * Use Foo extends Model to extend your class instead + * + * + * + * + * **id: ember-data:deprecate-model-reopenclass** + * + * ---- + * + * Instead of reopenClass, define `static` properties with native class syntax + * or add them to the final object. + * + * ```ts + * // instead of + * User.reopenClass({ aStaticMethod() {} }); + * + * // do this + * class User { + * static aStaticMethod() {} + * } + * + * // or do this + * User.aStaticMethod = function() {} + * ``` + * + * + * @property DEPRECATE_MODEL_REOPEN + * @since 4.7 + * @until 5.0 + * @public + */ +export const DEPRECATE_MODEL_REOPEN = '4.7'; + +/** + * **id: ember-data:deprecate-early-static** + * + * This deprecation triggers if static computed properties + * or methods are triggered without looking up the record + * via the store service's `modelFor` hook. Accessing this + * static information without looking up the model via the + * store most commonly occurs when + * + * - using ember-cli-mirage (to fix, refactor to not use its auto-discovery of ember-data models) + * - importing a model class and accessing its static information via the import + * + * Instead of + * + * ```js + * import User from 'my-app/models/user'; + * + * const relationships = User.relationshipsByName; + * ``` + * + * Do *at least* this + * + * ```js + * const relationships = store.modelFor('user').relationshipsByName; + * ``` + * + * However, the much more future proof refactor is to not use `modelFor` at all but instead + * to utilize the schema service for this static information. + * + * ```js + * const relationships = store.getSchemaDefinitionService().relationshipsDefinitionFor({ type: 'user' }); + * ``` + * + * + * @property DEPRECATE_EARLY_STATIC + * @since 4.7 + * @until 5.0 + * @public + */ +export const DEPRECATE_EARLY_STATIC = '4.7'; + +/** + * **id: ember-data:deprecate-errors-hash-to-array-helper** + * **id: ember-data:deprecate-errors-array-to-hash-helper** + * **id: ember-data:deprecate-normalize-modelname-helper** + * + * Deprecates `errorsHashToArray` `errorsArrayToHash` and `normalizeModelName` + * + * Users making use of these (already private) utilities can trivially copy them + * into their own codebase to continue using them, though we recommend refactoring + * to a more direct conversion into the expected errors format for the errors helpers. + * + * For refactoring normalizeModelName we also recommend following the guidance in + * [RFC#740 Deprecate Non-Strict Types](https://github.com/emberjs/rfcs/pull/740). + * + * + * @property DEPRECATE_HELPERS + * @since 4.7 + * @until 5.0 + * @public + */ +export const DEPRECATE_HELPERS = '4.7'; + +/** + * **id: ember-data:deprecate-promise-many-array-behavior** + * + * [RFC Documentation](https://rfcs.emberjs.com/id/0745-ember-data-deprecate-methods-on-promise-many-array) + * + * This deprecation deprecates accessing values on the asynchronous proxy + * in favor of first "resolving" or "awaiting" the promise to retrieve a + * synchronous value. + * + * Template iteration of the asynchronous value will still work and not trigger + * the deprecation, but all JS access should be avoided and HBS access for anything + * but `{{#each}}` should also be refactored. + * + * Recommended approaches include using the addon `ember-promise-helpers`, using + * Ember's `resource` pattern (including potentially the addon `ember-data-resources`), + * resolving the value in routes/provider components, or using the references API. + * + * An example of using the [hasMany](https://api.emberjs.com/ember-data/4.11/classes/Model/methods/hasMany?anchor=hasMany) [reference API](https://api.emberjs.com/ember-data/release/classes/HasManyReference): + * + * ```ts + * // get the synchronous "ManyArray" value for the asynchronous "friends" relationship. + * // note, this will return `null` if the relationship has not been loaded yet + * const value = person.hasMany('friends').value(); + * + * // to get just the list of related IDs + * const ids = person.hasMany('friends').ids(); + * ``` + * + * References participate in autotracking and getters/cached getters etc. which consume them + * will recompute if the value changes. + * + * @property DEPRECATE_PROMISE_MANY_ARRAY_BEHAVIORS + * @since 4.7 + * @until 5.0 + * @public + */ +export const DEPRECATE_PROMISE_MANY_ARRAY_BEHAVIORS = '4.7'; + +/** + * **id: ember-data:deprecate-non-strict-relationships** + * + * Deprecates when belongsTo and hasMany relationships are defined + * without specifying the inverse record's type. + * + * Instead of + * + * ```ts + * class Company extends Model { + * @hasMany() employees; + * } + * class Employee extends Model { + * @belongsTo() company; + * } + * ``` + * + * Use + * + * ```ts + * class Company extends Model { + * @hasMany('employee', { async: true, inverse: 'company' }) employees; + * } + * + * class Employee extends Model { + * @belongsTo('company', { async: true, inverse: 'employees' }) company; + * } + * ``` + * + * @property DEPRECATE_RELATIONSHIPS_WITHOUT_TYPE + * @since 4.7 + * @until 5.0 + * @public + */ +export const DEPRECATE_RELATIONSHIPS_WITHOUT_TYPE = '4.7'; + +/** + * **id: ember-data:deprecate-non-strict-relationships** + * + * Deprecates when belongsTo and hasMany relationships are defined + * without specifying whether the relationship is asynchronous. + * + * The current behavior is that relationships which do not define + * this setting are aschronous (`{ async: true }`). + * + * Instead of + * + * ```ts + * class Company extends Model { + * @hasMany('employee') employees; + * } + * class Employee extends Model { + * @belongsTo('company') company; + * } + * ``` + * + * Use + * + * ```ts + * class Company extends Model { + * @hasMany('employee', { async: true, inverse: 'company' }) employees; + * } + * + * class Employee extends Model { + * @belongsTo('company', { async: true, inverse: 'employees' }) company; + * } + * ``` + * + * @property DEPRECATE_RELATIONSHIPS_WITHOUT_ASYNC + * @since 4.7 + * @until 5.0 + * @public + */ +export const DEPRECATE_RELATIONSHIPS_WITHOUT_ASYNC = '4.7'; + +/** + * **id: ember-data:deprecate-non-strict-relationships** + * + * Deprecates when belongsTo and hasMany relationships are defined + * without specifying the inverse field on the related type. + * + * The current behavior is that relationships which do not define + * this setting have their inverse determined at runtime, which is + * potentially non-deterministic when mixins and polymorphism are involved. + * + * If an inverse relationship exists and you wish changes on one side to + * reflect onto the other side, use the inverse key. If you wish to not have + * changes reflected or no inverse relationship exists, specify `inverse: null`. + * + * Instead of + * + * ```ts + * class Company extends Model { + * @hasMany('employee') employees; + * } + * class Employee extends Model { + * @belongsTo('company') company; + * } + * ``` + * + * Use + * + * ```ts + * class Company extends Model { + * @hasMany('employee', { async: true, inverse: 'company' }) employees; + * } + * + * class Employee extends Model { + * @belongsTo('company', { async: true, inverse: 'employees' }) company; + * } + * ``` + * + * Instead of + * + * ```ts + * class Company extends Model { + * @hasMany('employee') employees; + * } + * class Employee extends Model { + * @attr name; + * } + * ``` + * + * Use + * + * ```ts + * class Company extends Model { + * @hasMany('employee', { async: true, inverse: null }) employees; + * } + * + * class Employee extends Model { + * @attr name; + * } + * ``` + * + * @property DEPRECATE_RELATIONSHIPS_WITHOUT_INVERSE + * @since 4.7 + * @until 5.0 + * @public + */ +export const DEPRECATE_RELATIONSHIPS_WITHOUT_INVERSE = '4.7'; + +/** + * **id: ember-data:no-a-with-array-like** + * + * Deprecates when calling `A()` on an EmberData ArrayLike class + * is detected. This deprecation may not always trigger due to complexities + * in ember-source versions and the use (or disabling) of prototype extensions. + * + * To fix, just use the native array methods instead of the EmberArray methods + * and refrain from wrapping the array in `A()`. + * + * Note that some computed property macros may themselves utilize `A()`, in which + * scenario the computed properties need to be upgraded to octane syntax. + * + * For instance, instead of: + * + * ```ts + * class extends Component { + * @filterBy('items', 'isComplete') completedItems; + * } + * ``` + * + * Use the following: + * + * ```ts + * class extends Component { + * get completedItems() { + * return this.items.filter(item => item.isComplete); + * } + * } + * ``` + * + * @property DEPRECATE_A_USAGE + * @since 4.7 + * @until 5.0 + * @public + */ +export const DEPRECATE_A_USAGE = '4.7'; + +/** + * **id: ember-data:deprecate-promise-proxies** + * + * Additional Reading: [RFC#846 Deprecate Proxies](https://rfcs.emberjs.com/id/0846-ember-data-deprecate-proxies) + * + * Deprecates using the proxy object/proxy array capabilities of values returned from + * + * - `store.findRecord` + * - `store.findAll` + * - `store.query` + * - `store.queryRecord` + * - `record.save` + * - `recordArray.save` + * - `recordArray.update` + * + * These methods will now return a native Promise that resolves with the value. + * + * Note that this does not deprecate the proxy behaviors of `PromiseBelongsTo`. See RFC for reasoning. + * The opportunity should still be taken if available to stop using these proxy behaviors; however, this class + * will remain until `import Model from '@ember-data/model';` is deprecated more broadly. + * + * @property DEPRECATE_PROMISE_PROXIES + * @since 4.7 + * @until 5.0 + * @public + */ +export const DEPRECATE_PROMISE_PROXIES = '4.7'; + +/** + * **id: ember-data:deprecate-array-like** + * + * Deprecates Ember "Array-like" methods on RecordArray and ManyArray. + * + * These are the arrays returned respectively by `store.peekAll()`, `store.findAll()`and + * hasMany relationships on instance of Model or `record.hasMany('relationshipName').value()`. + * + * The appropriate refactor is to treat these arrays as native arrays and to use native array methods. + * + * For instance, instead of: + * + * ```ts + * users.firstObject; + * ``` + * + * Use: + * + * ```ts + * users[0]; + * // or + * users.at(0); + * ``` + * + * @property DEPRECATE_ARRAY_LIKE + * @since 4.7 + * @until 5.0 + * @public + */ +export const DEPRECATE_ARRAY_LIKE = '4.7'; + +/** + * **id: ** + * + * This is a planned deprecation which will trigger when observer or computed + * chains are used to watch for changes on any EmberData RecordArray, ManyArray + * or PromiseManyArray. + * + * Support for these chains is currently guarded by the inactive deprecation flag + * listed here. + * + * @property DEPRECATE_COMPUTED_CHAINS + * @since 5.0 + * @until 6.0 + * @public + */ +export const DEPRECATE_COMPUTED_CHAINS = '5.0'; + +/** + * **id: ember-data:non-explicit-relationships** + * + * Deprecates when polymorphic relationships are detected via inheritance or mixins + * and no polymorphic relationship configuration has been setup. + * + * For further reading please review [RFC#793](https://rfcs.emberjs.com/id/0793-polymporphic-relations-without-inheritance) + * which introduced support for explicit relationship polymorphism without + * mixins or inheritance. + * + * You may still use mixins and inheritance to setup your polymorphism; however, the class + * structure is no longer what drives the design. Instead polymorphism is "traits" based or "structural": + * so long as each model which can satisfy the polymorphic relationship defines the inverse in the same + * way they work. + * + * Notably: `inverse: null` relationships can receive any type as a record with no additional configuration + * at all. + * + * Example Polymorphic Relationship Configuration + * + * ```ts + * // polymorphic relationship + * class Tag extends Model { + * @hasMany("taggable", { async: false, polymorphic: true, inverse: "tags" }) tagged; + * } + * + * // an inverse concrete relationship (e.g. satisfies "taggable") + * class Post extends Model { + * @hasMany("tag", { async: false, inverse: "tagged", as: "taggable" }) tags; + * } + * ``` + * + * @property DEPRECATE_NON_EXPLICIT_POLYMORPHISM + * @since 4.7 + * @until 5.0 + * @public + */ +export const DEPRECATE_NON_EXPLICIT_POLYMORPHISM = '4.7'; + +/** + * **id: ember-data:deprecate-many-array-duplicates** + * + * When the flag is `true` (default), adding duplicate records to a `ManyArray` + * is deprecated in non-production environments. In production environments, + * duplicate records added to a `ManyArray` will be deduped and no error will + * be thrown. + * + * When the flag is `false`, an error will be thrown when duplicates are added. + * + * @property DEPRECATE_MANY_ARRAY_DUPLICATES + * @since 5.3 + * @until 6.0 + * @public + */ +export const DEPRECATE_MANY_ARRAY_DUPLICATES = '4.12'; // '5.3'; + /** * **id: ember-data:deprecate-non-strict-types** * @@ -146,42 +761,6 @@ export const DEPRECATE_NON_STRICT_TYPES = '5.3'; */ export const DEPRECATE_NON_STRICT_ID = '5.3'; -/** - * **id: ** - * - * This is a planned deprecation which will trigger when observer or computed - * chains are used to watch for changes on any EmberData LiveArray, CollectionRecordArray, - * ManyArray or PromiseManyArray. - * - * Support for these chains is currently guarded by the deprecation flag - * listed here, enabling removal of the behavior if desired. - * - * @property DEPRECATE_COMPUTED_CHAINS - * @since 5.0 - * @until 6.0 - * @public - */ -export const DEPRECATE_COMPUTED_CHAINS = '5.0'; - -/** - * **id: ember-data:deprecate-legacy-imports** - * - * Deprecates when importing from `ember-data/*` instead of `@ember-data/*` - * in order to prepare for the eventual removal of the legacy `ember-data/*` - * - * All imports from `ember-data/*` should be updated to `@ember-data/*` - * except for `ember-data/store`. When you are using `ember-data` (as opposed to - * installing the indivudal packages) you should import from `ember-data/store` - * instead of `@ember-data/store` in order to receive the appropriate configuration - * of defaults. - * - * @property DEPRECATE_LEGACY_IMPORTS - * @since 5.3 - * @until 6.0 - * @public - */ -export const DEPRECATE_LEGACY_IMPORTS = '5.3'; - /** * **id: ember-data:deprecate-non-unique-collection-payloads** * @@ -372,23 +951,6 @@ export const DEPRECATE_NON_UNIQUE_PAYLOADS = '5.3'; */ export const DEPRECATE_RELATIONSHIP_REMOTE_UPDATE_CLEARING_LOCAL_STATE = '5.3'; -/** - * **id: ember-data:deprecate-many-array-duplicates** - * - * When the flag is `true` (default), adding duplicate records to a `ManyArray` - * is deprecated in non-production environments. In production environments, - * duplicate records added to a `ManyArray` will be deduped and no error will - * be thrown. - * - * When the flag is `false`, an error will be thrown when duplicates are added. - * - * @property DEPRECATE_MANY_ARRAY_DUPLICATES - * @since 5.3 - * @until 6.0 - * @public - */ -export const DEPRECATE_MANY_ARRAY_DUPLICATES = '5.3'; - /** * **id: ember-data:deprecate-store-extends-ember-object** * @@ -461,18 +1023,18 @@ export const ENABLE_LEGACY_SCHEMA_SERVICE = '5.4'; export const DEPRECATE_EMBER_INFLECTOR = '5.3'; /** - * This is a special flag that can be used to opt-in early to receiving deprecations introduced in 6.x - * which have had their infra backported to 5.x versions of EmberData. + * This is a special flag that can be used to opt-in early to receiving deprecations introduced in 5.x + * which have had their infra backported to 4.x versions of EmberData. * - * When this flag is not present or set to `true`, the deprecations from the 6.x branch + * When this flag is not present or set to `true`, the deprecations from the 5.x branch * will not print their messages and the deprecation cannot be resolved. * - * When this flag is present and set to `false`, the deprecations from the 6.x branch will + * When this flag is present and set to `false`, the deprecations from the 5.x branch will * print and can be resolved. * - * @property DISABLE_7X_DEPRECATIONS - * @since 5.3 - * @until 7.0 + * @property DISABLE_6X_DEPRECATIONS + * @since 4.13 + * @until 5.0 * @public */ -export const DISABLE_7X_DEPRECATIONS = '7.0'; +export const DISABLE_6X_DEPRECATIONS = '6.0'; diff --git a/packages/build-config/src/deprecations.ts b/packages/build-config/src/deprecations.ts index 03413ccf20d..344315da32f 100644 --- a/packages/build-config/src/deprecations.ts +++ b/packages/build-config/src/deprecations.ts @@ -1,13 +1,31 @@ // deprecations export const DEPRECATE_CATCH_ALL: boolean = true; +export const DEPRECATE_SAVE_PROMISE_ACCESS: boolean = true; +export const DEPRECATE_RSVP_PROMISE: boolean = true; +export const DEPRECATE_SNAPSHOT_MODEL_CLASS_ACCESS: boolean = true; +export const DEPRECATE_STORE_FIND: boolean = true; +export const DEPRECATE_HAS_RECORD: boolean = true; +// FIXME we can potentially drop this since the cache implementations we care about turn out to all be stuck 4.6 or earlier +export const DEPRECATE_STRING_ARG_SCHEMAS: boolean = true; +export const DEPRECATE_JSON_API_FALLBACK: boolean = true; +export const DEPRECATE_MODEL_REOPEN: boolean = true; +export const DEPRECATE_EARLY_STATIC: boolean = true; +export const DEPRECATE_HELPERS: boolean = true; +export const DEPRECATE_PROMISE_MANY_ARRAY_BEHAVIORS: boolean = true; +export const DEPRECATE_RELATIONSHIPS_WITHOUT_TYPE: boolean = true; +export const DEPRECATE_RELATIONSHIPS_WITHOUT_ASYNC: boolean = true; +export const DEPRECATE_RELATIONSHIPS_WITHOUT_INVERSE: boolean = true; +export const DEPRECATE_A_USAGE: boolean = true; +export const DEPRECATE_PROMISE_PROXIES: boolean = true; +export const DEPRECATE_ARRAY_LIKE: boolean = true; export const DEPRECATE_COMPUTED_CHAINS: boolean = true; +export const DEPRECATE_NON_EXPLICIT_POLYMORPHISM: boolean = true; +export const DEPRECATE_MANY_ARRAY_DUPLICATES: boolean = true; export const DEPRECATE_NON_STRICT_TYPES: boolean = true; export const DEPRECATE_NON_STRICT_ID: boolean = true; -export const DEPRECATE_LEGACY_IMPORTS: boolean = true; export const DEPRECATE_NON_UNIQUE_PAYLOADS: boolean = true; export const DEPRECATE_RELATIONSHIP_REMOTE_UPDATE_CLEARING_LOCAL_STATE: boolean = true; -export const DEPRECATE_MANY_ARRAY_DUPLICATES: boolean = true; export const DEPRECATE_STORE_EXTENDS_EMBER_OBJECT: boolean = true; export const ENABLE_LEGACY_SCHEMA_SERVICE: boolean = true; export const DEPRECATE_EMBER_INFLECTOR: boolean = true; -export const DISABLE_7X_DEPRECATIONS: boolean = true; +export const DISABLE_6X_DEPRECATIONS: boolean = true; diff --git a/packages/core-types/package.json b/packages/core-types/package.json index 62d3b2775d8..f2301ab135a 100644 --- a/packages/core-types/package.json +++ b/packages/core-types/package.json @@ -1,6 +1,6 @@ { "name": "@warp-drive/core-types", - "version": "5.4.0-alpha.136", + "version": "4.13.0-alpha.3", "description": "Provides core logic, utils and types for WarpDrive and EmberData", "keywords": [ "ember-addon" @@ -38,7 +38,7 @@ } }, "dependencies": { - "@embroider/macros": "^1.16.10", + "@embroider/macros": "^1.16.6", "@warp-drive/build-config": "workspace:*" }, "devDependencies": { diff --git a/packages/core-types/src/identifier.ts b/packages/core-types/src/identifier.ts index d740cb5dda2..605c5ef1d07 100644 --- a/packages/core-types/src/identifier.ts +++ b/packages/core-types/src/identifier.ts @@ -2,19 +2,13 @@ @module @ember-data/store */ -import { DEBUG } from '@warp-drive/build-config/env'; - // provided for additional debuggability export const DEBUG_CLIENT_ORIGINATED: unique symbol = Symbol('record-originated-on-client'); export const DEBUG_IDENTIFIER_BUCKET: unique symbol = Symbol('identifier-bucket'); export const DEBUG_STALE_CACHE_OWNER: unique symbol = Symbol('warpDriveStaleCache'); -function ProdSymbol(str: T): T { - return DEBUG ? (Symbol(str) as unknown as T) : str; -} - // also present in production -export const CACHE_OWNER = ProdSymbol('__$co'); +export const CACHE_OWNER: unique symbol = Symbol('warpDriveCache'); export type IdentifierBucket = 'record' | 'document'; diff --git a/packages/core-types/src/request.ts b/packages/core-types/src/request.ts index 115fa4fed29..559e4e5ffa1 100644 --- a/packages/core-types/src/request.ts +++ b/packages/core-types/src/request.ts @@ -302,8 +302,6 @@ export type RequestInfo = Request & { options?: Record; [RequestSignature]?: RT; - - [EnableHydration]?: boolean; }; /** diff --git a/packages/core-types/src/schema/fields.ts b/packages/core-types/src/schema/fields.ts index 34056b03715..22b485841f2 100644 --- a/packages/core-types/src/schema/fields.ts +++ b/packages/core-types/src/schema/fields.ts @@ -729,40 +729,6 @@ export type LegacyBelongsToField = { */ polymorphic?: boolean; - /** - * Whether this field should ever make use of the legacy support infra - * from @ember-data/model and the LegacyNetworkMiddleware for adapters and serializers. - * - * When true, none of the legacy support will be utilized. Sync relationships - * will be expected to already have all their data. When reloading a sync relationship - * you would be expected to have a `related link` available from a prior relationship - * payload e.g. - * - * ```ts - * { - * data: { - * type: 'user', - * id: '2', - * attributes: { name: 'Chris' }, - * relationships: { - * bestFriend: { - * links: { related: "/users/1/bestFriend" }, - * data: { type: 'user', id: '1' }, - * } - * } - * }, - * included: [ - * { type: 'user', id: '1', attributes: { name: 'Krystan' } } - * ] - * } - * ``` - * - * Async relationships will be loaded via their link if needed. - * - * @typedoc - */ - linksMode?: true; - /** * When omitted, the cache data for this field will * clear local state of all changes except for the @@ -853,40 +819,6 @@ export type LegacyHasManyField = { */ polymorphic?: boolean; - /** - * Whether this field should ever make use of the legacy support infra - * from @ember-data/model and the LegacyNetworkMiddleware for adapters and serializers. - * - * When true, none of the legacy support will be utilized. Sync relationships - * will be expected to already have all their data. When reloading a sync relationship - * you would be expected to have a `related link` available from a prior relationship - * payload e.g. - * - * ```ts - * { - * data: { - * type: 'user', - * id: '2', - * attributes: { name: 'Chris' }, - * relationships: { - * bestFriends: { - * links: { related: "/users/1/bestFriends" }, - * data: [ { type: 'user', id: '1' } ], - * } - * } - * }, - * included: [ - * { type: 'user', id: '1', attributes: { name: 'Krystan' } } - * ] - * } - * ``` - * - * Async relationships will be loaded via their link if needed. - * - * @typedoc - */ - linksMode?: true; - /** * When omitted, the cache data for this field will * clear local state of all changes except for the diff --git a/packages/core-types/src/spec/document.ts b/packages/core-types/src/spec/document.ts index 93ab42c4e67..0ee5fa01399 100644 --- a/packages/core-types/src/spec/document.ts +++ b/packages/core-types/src/spec/document.ts @@ -9,13 +9,13 @@ export interface ResourceMetaDocument { links?: Links | PaginationLinks; } -export interface SingleResourceDataDocument { +export interface SingleResourceDataDocument { // the url or cache-key associated with the structured document lid?: string; links?: Links | PaginationLinks; meta?: Meta; data: T | null; - included?: R[]; + included?: T[]; } export interface CollectionResourceDataDocument { diff --git a/packages/debug/package.json b/packages/debug/package.json index 9703351ec4e..c46ab76f1d5 100644 --- a/packages/debug/package.json +++ b/packages/debug/package.json @@ -1,6 +1,6 @@ { "name": "@ember-data/debug", - "version": "5.4.0-alpha.136", + "version": "4.13.0-alpha.3", "description": "Provides support for the ember-inspector for apps built with Ember and EmberData", "keywords": [ "ember-addon" @@ -74,7 +74,7 @@ }, "dependencies": { "@ember/edition-utils": "^1.2.0", - "@embroider/macros": "^1.16.10", + "@embroider/macros": "^1.16.6", "@warp-drive/build-config": "workspace:*" }, "devDependencies": { @@ -93,7 +93,7 @@ "@warp-drive/core-types": "workspace:*", "@warp-drive/internal-config": "workspace:*", "ember-source": "~5.12.0", - "decorator-transforms": "^2.3.0", + "decorator-transforms": "^2.2.2", "pnpm-sync-dependencies-meta-injected": "0.0.14", "typescript": "^5.7.2", "vite": "^5.2.11" diff --git a/packages/diagnostic/README.md b/packages/diagnostic/README.md index b79dd33e53b..ae7773a9fc0 100644 --- a/packages/diagnostic/README.md +++ b/packages/diagnostic/README.md @@ -452,7 +452,7 @@ module('My Module', function(hooks) { 1. Add the following peer-deps to your app: ```diff -+ "@ember/test-helpers": "^3.3.0 || ^4.0.4 || ^5.1.0", ++ "@ember/test-helpers": "^3.3.0 || ^4.0.4", + "ember-cli-test-loader": ">= 3.1.0", + "@embroider/addon-shim": ">= 1.8.6" ``` diff --git a/packages/diagnostic/package.json b/packages/diagnostic/package.json index d93db097dfe..e320f1c0b5c 100644 --- a/packages/diagnostic/package.json +++ b/packages/diagnostic/package.json @@ -1,6 +1,7 @@ { "name": "@warp-drive/diagnostic", - "version": "0.0.0-alpha.122", + "version": "4.12.9-alpha.3", + "private": true, "description": "⚡️ A Lightweight Modern Test Runner", "keywords": [ "test", @@ -74,7 +75,7 @@ }, "peerDependencies": { "ember-source": "3.28.12 || ^4.0.4 || ^5.0.0 || ^6.0.0", - "@ember/test-helpers": "5.1.0", + "@ember/test-helpers": "4.0.4", "ember-cli-test-loader": ">= 3.1.0" }, "peerDependenciesMeta": { @@ -102,8 +103,8 @@ "@babel/preset-typescript": "^7.24.1", "@babel/runtime": "^7.24.5", "@warp-drive/internal-config": "workspace:*", - "bun-types": "^1.2.2", - "@ember/test-helpers": "5.1.0", + "bun-types": "^1.1.30", + "@ember/test-helpers": "4.0.4", "ember-source": "~5.12.0", "@glimmer/component": "^1.1.2", "ember-cli-test-loader": "^3.1.0", diff --git a/packages/graph/eslint.config.mjs b/packages/graph/eslint.config.mjs index 63296bac802..c42683457ca 100644 --- a/packages/graph/eslint.config.mjs +++ b/packages/graph/eslint.config.mjs @@ -2,6 +2,7 @@ import { globalIgnores } from '@warp-drive/internal-config/eslint/ignore.js'; import * as node from '@warp-drive/internal-config/eslint/node.js'; import * as typescript from '@warp-drive/internal-config/eslint/typescript.js'; +import { externals } from './vite.config.mjs'; /** @type {import('eslint').Linter.FlatConfig[]} */ export default [ @@ -11,7 +12,7 @@ export default [ // browser (js/ts) ================ typescript.browser({ srcDirs: ['src'], - allowedImports: ['@ember/debug'], + allowedImports: externals, }), // node (module) ================ diff --git a/packages/graph/package.json b/packages/graph/package.json index 72f2e66b3cc..d67a302f0ee 100644 --- a/packages/graph/package.json +++ b/packages/graph/package.json @@ -1,6 +1,6 @@ { "name": "@ember-data/graph", - "version": "5.4.0-alpha.136", + "version": "4.13.0-alpha.3", "description": "Provides a normalized graph for managing relationships between resources", "keywords": [ "ember-addon" @@ -63,7 +63,7 @@ "@warp-drive/core-types": "workspace:*" }, "dependencies": { - "@embroider/macros": "^1.16.10", + "@embroider/macros": "^1.16.6", "@warp-drive/build-config": "workspace:*" }, "devDependencies": { diff --git a/packages/graph/src/-private/-diff.ts b/packages/graph/src/-private/-diff.ts index 12cc805eb5d..0041ab66a66 100644 --- a/packages/graph/src/-private/-diff.ts +++ b/packages/graph/src/-private/-diff.ts @@ -1,6 +1,6 @@ import { deprecate } from '@ember/debug'; -import { DEPRECATE_NON_UNIQUE_PAYLOADS } from '@warp-drive/build-config/deprecations'; +import { DEPRECATE_NON_UNIQUE_PAYLOADS, DISABLE_6X_DEPRECATIONS } from '@warp-drive/build-config/deprecations'; import { DEBUG } from '@warp-drive/build-config/env'; import { assert } from '@warp-drive/build-config/macros'; import type { StableRecordIdentifier } from '@warp-drive/core-types'; @@ -14,7 +14,6 @@ import replaceRelatedRecord from './operations/replace-related-record'; import replaceRelatedRecords from './operations/replace-related-records'; function _deprecatedCompare( - priorLocalState: T[] | null, newState: T[], newMembers: Set, prevState: T[], @@ -31,7 +30,6 @@ function _deprecatedCompare( const duplicates = new Map(); const finalSet = new Set(); const finalState: T[] = []; - const priorLocalLength = priorLocalState?.length ?? 0; for (let i = 0, j = 0; i < iterationLength; i++) { let adv = false; @@ -71,11 +69,6 @@ function _deprecatedCompare( // j is always less than i and so if i < prevLength, j < prevLength if (member !== prevState[j]) { changed = true; - } else if (!changed && j < priorLocalLength) { - const priorLocalMember = priorLocalState![j]; - if (priorLocalMember !== member) { - changed = true; - } } if (!newMembers.has(prevMember)) { @@ -107,7 +100,6 @@ function _deprecatedCompare( } function _compare( - priorLocalState: T[] | null, finalState: T[], finalSet: Set, prevState: T[], @@ -122,7 +114,6 @@ function _compare( let changed: boolean = finalSet.size !== prevSet.size; const added = new Set(); const removed = new Set(); - const priorLocalLength = priorLocalState?.length ?? 0; for (let i = 0; i < iterationLength; i++) { let member: T | undefined; @@ -144,11 +135,6 @@ function _compare( // detect reordering if (equalLength && member !== prevMember) { changed = true; - } else if (equalLength && !changed && i < priorLocalLength) { - const priorLocalMember = priorLocalState![i]; - if (priorLocalMember !== prevMember) { - changed = true; - } } if (!finalSet.has(prevMember)) { @@ -183,24 +169,16 @@ export function diffCollection( onDel: (v: StableRecordIdentifier) => void ): Diff { const finalSet = new Set(finalState); - const { localState: priorLocalState, remoteState, remoteMembers } = relationship; + const { remoteState, remoteMembers } = relationship; if (DEPRECATE_NON_UNIQUE_PAYLOADS) { if (finalState.length !== finalSet.size) { - const { diff, duplicates } = _deprecatedCompare( - priorLocalState, - finalState, - finalSet, - remoteState, - remoteMembers, - onAdd, - onDel - ); + const { diff, duplicates } = _deprecatedCompare(finalState, finalSet, remoteState, remoteMembers, onAdd, onDel); if (DEBUG) { deprecate( `Expected all entries in the relationship ${relationship.definition.type}:${relationship.definition.key} to be unique, see log for a list of duplicate entry indeces`, - false, + /* inline-macro-config */ DISABLE_6X_DEPRECATIONS, { id: 'ember-data:deprecate-non-unique-relationship-entries', for: 'ember-data', @@ -221,7 +199,7 @@ export function diffCollection( ); } - return _compare(priorLocalState, finalState, finalSet, remoteState, remoteMembers, onAdd, onDel); + return _compare(finalState, finalSet, remoteState, remoteMembers, onAdd, onDel); } export function computeLocalState(storage: CollectionEdge): StableRecordIdentifier[] { @@ -298,6 +276,10 @@ export function _addLocal( relationship.localState.push(value); } } + assert( + `Expected relationship to be dirty when adding a local mutation`, + relationship.localState || relationship.isDirty + ); return true; } diff --git a/packages/graph/src/-private/-edge-definition.ts b/packages/graph/src/-private/-edge-definition.ts index 9c157e4d437..a6868f487db 100644 --- a/packages/graph/src/-private/-edge-definition.ts +++ b/packages/graph/src/-private/-edge-definition.ts @@ -1,4 +1,5 @@ import type Store from '@ember-data/store'; +import { DEPRECATE_RELATIONSHIPS_WITHOUT_INVERSE } from '@warp-drive/build-config/deprecations'; import { DEBUG } from '@warp-drive/build-config/env'; import { assert } from '@warp-drive/build-config/macros'; import type { StableRecordIdentifier } from '@warp-drive/core-types'; @@ -124,7 +125,6 @@ export interface UpgradedMeta { isCollection: boolean; isPolymorphic: boolean; resetOnRemoteUpdate: boolean; - isLinksMode: boolean; inverseKind: 'implicit' | RelationshipFieldKind; /** @@ -141,7 +141,6 @@ export interface UpgradedMeta { inverseIsImplicit: boolean; inverseIsCollection: boolean; inverseIsPolymorphic: boolean; - inverseIsLinksMode: boolean; } export interface EdgeDefinition { @@ -197,7 +196,6 @@ function syncMeta(definition: UpgradedMeta, inverseDefinition: UpgradedMeta) { definition.inverseIsCollection = inverseDefinition.isCollection; definition.inverseIsPolymorphic = inverseDefinition.isPolymorphic; definition.inverseIsImplicit = inverseDefinition.isImplicit; - definition.inverseIsLinksMode = inverseDefinition.isLinksMode; const resetOnRemoteUpdate = definition.resetOnRemoteUpdate === false || inverseDefinition.resetOnRemoteUpdate === false ? false : true; definition.resetOnRemoteUpdate = resetOnRemoteUpdate; @@ -218,20 +216,18 @@ function upgradeMeta(meta: RelationshipField): UpgradedMeta { niceMeta.isImplicit = false; niceMeta.isCollection = meta.kind === 'hasMany'; niceMeta.isPolymorphic = options && !!options.polymorphic; - niceMeta.isLinksMode = options.linksMode ?? false; niceMeta.inverseKey = (options && options.inverse) || STR_LATER; niceMeta.inverseType = STR_LATER; niceMeta.inverseIsAsync = BOOL_LATER; niceMeta.inverseIsImplicit = (options && options.inverse === null) || BOOL_LATER; niceMeta.inverseIsCollection = BOOL_LATER; - niceMeta.inverseIsLinksMode = BOOL_LATER; - // prettier-ignore - niceMeta.resetOnRemoteUpdate = !isLegacyField(meta) ? false - : meta.options?.linksMode ? false - : meta.options?.resetOnRemoteUpdate === false ? false - : true; + niceMeta.resetOnRemoteUpdate = isLegacyField(meta) + ? meta.options?.resetOnRemoteUpdate === false + ? false + : true + : false; return niceMeta; } @@ -586,12 +582,27 @@ export function upgradeDefinition( return info; } +type RelationshipDefinition = RelationshipField & { + _inverseKey: (store: Store, modelClass: unknown) => string | null; +}; + +function metaIsRelationshipDefinition(meta: FieldSchema): meta is RelationshipDefinition { + return typeof (meta as RelationshipDefinition)._inverseKey === 'function'; +} + function inverseForRelationship(store: Store, identifier: StableRecordIdentifier | { type: string }, key: string) { const definition = store.schema.fields(identifier).get(key); if (!definition) { return null; } + if (DEPRECATE_RELATIONSHIPS_WITHOUT_INVERSE) { + if (metaIsRelationshipDefinition(definition)) { + const modelClass = store.modelFor(identifier.type); + return definition._inverseKey(store, modelClass); + } + } + assert(`Expected ${key} to be a relationship`, isRelationshipField(definition)); assert( `Expected the relationship defintion to specify the inverse type or null.`, diff --git a/packages/graph/src/-private/-utils.ts b/packages/graph/src/-private/-utils.ts index f0082161238..ccdf4aa740d 100644 --- a/packages/graph/src/-private/-utils.ts +++ b/packages/graph/src/-private/-utils.ts @@ -143,7 +143,7 @@ export function removeIdentifierCompletelyFromRelationship( // we shouldn't be notifying here though, figure out where // a notification was missed elsewhere. if (!silenceNotifications) { - notifyChange(graph, relationship); + notifyChange(graph, relationship.identifier, relationship.definition.key); } } } else if (isHasMany(relationship)) { @@ -164,7 +164,7 @@ export function removeIdentifierCompletelyFromRelationship( // we shouldn't be notifying here though, figure out where // a notification was missed elsewhere. if (!silenceNotifications) { - notifyChange(graph, relationship); + notifyChange(graph, relationship.identifier, relationship.definition.key); } } } @@ -174,14 +174,8 @@ export function removeIdentifierCompletelyFromRelationship( } } -export function notifyChange(graph: Graph, relationship: CollectionEdge | ResourceEdge): void { - if (!relationship.accessed) { - return; - } - - const identifier = relationship.identifier; - const key = relationship.definition.key; - +// TODO add silencing at the graph level +export function notifyChange(graph: Graph, identifier: StableRecordIdentifier, key: string) { if (identifier === graph._removing) { if (LOG_GRAPH) { // eslint-disable-next-line no-console diff --git a/packages/graph/src/-private/coerce-id.ts b/packages/graph/src/-private/coerce-id.ts index 7ad7db9c748..4c44a8b22e0 100644 --- a/packages/graph/src/-private/coerce-id.ts +++ b/packages/graph/src/-private/coerce-id.ts @@ -1,6 +1,6 @@ import { deprecate } from '@ember/debug'; -import { DEPRECATE_NON_STRICT_ID } from '@warp-drive/build-config/deprecations'; +import { DEPRECATE_NON_STRICT_ID, DISABLE_6X_DEPRECATIONS } from '@warp-drive/build-config/deprecations'; import { assert } from '@warp-drive/build-config/macros'; // Used by the store to normalize IDs entering the store. Despite the fact @@ -24,7 +24,7 @@ export function coerceId(id: Coercable): string | null { `The resource id '<${typeof id}> ${String( id )} ' is not normalized. Update your application code to use '${JSON.stringify(normalized)}' instead.`, - normalized === id, + /* inline-macro-config */ DISABLE_6X_DEPRECATIONS ? true : normalized === id, { id: 'ember-data:deprecate-non-strict-id', until: '6.0', diff --git a/packages/graph/src/-private/debug/assert-polymorphic-type.ts b/packages/graph/src/-private/debug/assert-polymorphic-type.ts index 597af2e72eb..ee164ad6bb8 100644 --- a/packages/graph/src/-private/debug/assert-polymorphic-type.ts +++ b/packages/graph/src/-private/debug/assert-polymorphic-type.ts @@ -1,11 +1,34 @@ /* eslint-disable @typescript-eslint/no-shadow */ -import type { CacheCapabilitiesManager } from '@ember-data/store/types'; +import type Mixin from '@ember/object/mixin'; + +import type Store from '@ember-data/store'; +import type { CacheCapabilitiesManager, ModelSchema } from '@ember-data/store/types'; +import { DEPRECATE_NON_EXPLICIT_POLYMORPHISM } from '@warp-drive/build-config/deprecations'; import { DEBUG } from '@warp-drive/build-config/env'; import { assert } from '@warp-drive/build-config/macros'; import type { StableRecordIdentifier } from '@warp-drive/core-types'; import { isLegacyField, isRelationshipField, temporaryConvertToLegacy, type UpgradedMeta } from '../-edge-definition'; +type Model = ModelSchema; + +// A pile of soft-lies to deal with mixin APIs +type ModelWithMixinApis = Model & { + isModel?: boolean; + __isMixin?: boolean; + __mixin: Mixin; + PrototypeMixin: Mixin; + detect: (mixin: Model | Mixin | ModelWithMixinApis) => boolean; + prototype: Model; + [Symbol.hasInstance](model: Model): true; +}; + +function assertModelSchemaIsModel( + schema: ModelSchema | Model | ModelWithMixinApis +): asserts schema is ModelWithMixinApis { + assert(`Expected Schema to be an instance of Model`, 'isModel' in schema && schema.isModel === true); +} + /* Assert that `addedRecord` has a valid type so it can be added to the relationship of the `record`. @@ -27,6 +50,21 @@ let assertPolymorphicType: ( let assertInheritedSchema: (definition: UpgradedMeta, type: string) => void; if (DEBUG) { + const checkPolymorphic = function checkPolymorphic(modelClass: ModelSchema, addedModelClass: ModelSchema) { + assertModelSchemaIsModel(modelClass); + assertModelSchemaIsModel(addedModelClass); + + if (modelClass.__isMixin) { + return ( + modelClass.__mixin.detect(addedModelClass.PrototypeMixin) || + // handle native class extension e.g. `class Post extends Model.extend(Commentable) {}` + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + modelClass.__mixin.detect(Object.getPrototypeOf(addedModelClass).PrototypeMixin) + ); + } + return addedModelClass.prototype instanceof modelClass || modelClass.detect(addedModelClass); + }; + function validateSchema(definition: UpgradedMeta, meta: PrintConfig) { const errors = new Map(); @@ -61,9 +99,9 @@ if (DEBUG) { kind: string; options: { as?: string; - async: boolean; + async?: boolean; polymorphic?: boolean; - inverse: string | null; + inverse?: string | null; }; }; type RelationshipSchemaError = 'name' | 'type' | 'kind' | 'as' | 'async' | 'polymorphic' | 'inverse'; @@ -136,7 +174,6 @@ if (DEBUG) { kind: definition.inverseKind, isAsync: definition.inverseIsAsync, isPolymorphic: true, - isLinksMode: definition.isLinksMode, isCollection: definition.inverseIsCollection, isImplicit: definition.inverseIsImplicit, inverseKey: definition.key, @@ -144,7 +181,6 @@ if (DEBUG) { inverseKind: definition.kind, inverseIsAsync: definition.isAsync, inverseIsPolymorphic: definition.isPolymorphic, - inverseIsLinksMode: definition.inverseIsLinksMode, inverseIsImplicit: definition.isImplicit, inverseIsCollection: definition.isCollection, resetOnRemoteUpdate: definition.resetOnRemoteUpdate, @@ -213,69 +249,143 @@ if (DEBUG) { if (parentDefinition.inverseIsImplicit) { return; } + let asserted = false; + if (parentDefinition.isPolymorphic) { - let meta = store.schema.fields(addedIdentifier).get(parentDefinition.inverseKey); - assert( - `No '${parentDefinition.inverseKey}' field exists on '${ - addedIdentifier.type - }'. To use this type in the polymorphic relationship '${parentDefinition.inverseType}.${ - parentDefinition.key - }' the relationships schema definition for ${addedIdentifier.type} should include:${expectedSchema( - parentDefinition - )}`, - meta - ); - assert( - `Expected the field ${parentDefinition.inverseKey} to be a relationship`, - meta && isRelationshipField(meta) - ); - meta = isLegacyField(meta) ? meta : temporaryConvertToLegacy(meta); + const rawMeta = store.schema.fields(addedIdentifier).get(parentDefinition.inverseKey); assert( - `You should not specify both options.as and options.inverse as null on ${addedIdentifier.type}.${parentDefinition.inverseKey}, as if there is no inverse field there is no abstract type to conform to. You may have intended for this relationship to be polymorphic, or you may have mistakenly set inverse to null.`, - !(meta.options.inverse === null && meta?.options.as?.length) - ); - const errors = validateSchema(parentDefinition, meta); - assert( - `The schema for the relationship '${parentDefinition.inverseKey}' on '${ - addedIdentifier.type - }' type does not correctly implement '${parentDefinition.type}' and thus cannot be assigned to the '${ - parentDefinition.key - }' relationship in '${ - parentIdentifier.type - }'. If using this record in this polymorphic relationship is desired, correct the errors in the schema shown below:${printSchema( - meta, - errors - )}`, - errors.size === 0 + `Expected to find a relationship field schema for ${parentDefinition.inverseKey} on ${addedIdentifier.type} but none was found`, + !rawMeta || isRelationshipField(rawMeta) ); + const meta = rawMeta && (isLegacyField(rawMeta) ? rawMeta : temporaryConvertToLegacy(rawMeta)); + + if (DEPRECATE_NON_EXPLICIT_POLYMORPHISM) { + if (meta?.options?.as) { + asserted = true; + assert( + `No '${parentDefinition.inverseKey}' field exists on '${addedIdentifier.type}'. To use this type in the polymorphic relationship '${parentDefinition.inverseType}.${parentDefinition.key}' the relationships schema definition for ${addedIdentifier.type} should include:${expectedSchema(parentDefinition)}`, + meta + ); + assert( + `You should not specify both options.as and options.inverse as null on ${addedIdentifier.type}.${parentDefinition.inverseKey}, as if there is no inverse field there is no abstract type to conform to. You may have intended for this relationship to be polymorphic, or you may have mistakenly set inverse to null.`, + !(meta.options.inverse === null && meta?.options.as?.length > 0) + ); + const errors = validateSchema(parentDefinition, meta); + assert( + `The schema for the relationship '${parentDefinition.inverseKey}' on '${addedIdentifier.type}' type does not correctly implement '${parentDefinition.type}' and thus cannot be assigned to the '${parentDefinition.key}' relationship in '${parentIdentifier.type}'. If using this record in this polymorphic relationship is desired, correct the errors in the schema shown below:${printSchema(meta, errors)}`, + errors.size === 0 + ); + } + } else { + assert( + `No '${parentDefinition.inverseKey}' field exists on '${ + addedIdentifier.type + }'. To use this type in the polymorphic relationship '${parentDefinition.inverseType}.${ + parentDefinition.key + }' the relationships schema definition for ${addedIdentifier.type} should include:${expectedSchema( + parentDefinition + )}`, + meta + ); + assert( + `Expected the field ${parentDefinition.inverseKey} to be a relationship`, + meta && isRelationshipField(meta) + ); + assert( + `You should not specify both options.as and options.inverse as null on ${addedIdentifier.type}.${parentDefinition.inverseKey}, as if there is no inverse field there is no abstract type to conform to. You may have intended for this relationship to be polymorphic, or you may have mistakenly set inverse to null.`, + !(meta.options.inverse === null && meta?.options.as?.length) + ); + const errors = validateSchema(parentDefinition, meta); + assert( + `The schema for the relationship '${parentDefinition.inverseKey}' on '${ + addedIdentifier.type + }' type does not correctly implement '${parentDefinition.type}' and thus cannot be assigned to the '${ + parentDefinition.key + }' relationship in '${ + parentIdentifier.type + }'. If using this record in this polymorphic relationship is desired, correct the errors in the schema shown below:${printSchema( + meta, + errors + )}`, + errors.size === 0 + ); + } } else if (addedIdentifier.type !== parentDefinition.type) { // if we are not polymorphic // then the addedIdentifier.type must be the same as the parentDefinition.type - let meta = store.schema.fields(addedIdentifier).get(parentDefinition.inverseKey); + const rawMeta = store.schema.fields(addedIdentifier).get(parentDefinition.inverseKey); assert( - `Expected the field ${parentDefinition.inverseKey} to be a relationship`, - !meta || isRelationshipField(meta) + `Expected to find a relationship field schema for ${parentDefinition.inverseKey} on ${addedIdentifier.type} but none was found`, + !rawMeta || isRelationshipField(rawMeta) ); - meta = meta && (isLegacyField(meta) ? meta : temporaryConvertToLegacy(meta)); - if (meta?.options.as === parentDefinition.type) { - // inverse is likely polymorphic but missing the polymorphic flag - let meta = store.schema.fields({ type: parentDefinition.inverseType }).get(parentDefinition.key); - assert(`Expected the field ${parentDefinition.key} to be a relationship`, meta && isRelationshipField(meta)); - meta = isLegacyField(meta) ? meta : temporaryConvertToLegacy(meta); - const errors = validateSchema(definitionWithPolymorphic(inverseDefinition(parentDefinition)), meta); - assert( - `The '<${addedIdentifier.type}>.${ - parentDefinition.inverseKey - }' relationship cannot be used polymorphically because '<${parentDefinition.inverseType}>.${ - parentDefinition.key - } is not a polymorphic relationship. To use this relationship in a polymorphic manner, fix the following schema issues on the relationships schema for '${ - parentDefinition.inverseType - }':${printSchema(meta, errors)}` - ); - } else { + const meta = rawMeta && (isLegacyField(rawMeta) ? rawMeta : temporaryConvertToLegacy(rawMeta)); + + if (!DEPRECATE_NON_EXPLICIT_POLYMORPHISM) { + if (meta?.options.as === parentDefinition.type) { + // inverse is likely polymorphic but missing the polymorphic flag + const inverseMeta = store.schema.fields({ type: parentDefinition.inverseType }).get(parentDefinition.key); + assert( + `Expected to find a relationship field schema for ${parentDefinition.inverseKey} on ${addedIdentifier.type} but none was found`, + inverseMeta && isRelationshipField(inverseMeta) + ); + const legacyInverseMeta = + inverseMeta && (isLegacyField(inverseMeta) ? inverseMeta : temporaryConvertToLegacy(inverseMeta)); + const errors = validateSchema( + definitionWithPolymorphic(inverseDefinition(parentDefinition)), + legacyInverseMeta + ); + assert( + `The '<${addedIdentifier.type}>.${parentDefinition.inverseKey}' relationship cannot be used polymorphically because '<${parentDefinition.inverseType}>.${parentDefinition.key} is not a polymorphic relationship. To use this relationship in a polymorphic manner, fix the following schema issues on the relationships schema for '${parentDefinition.inverseType}':${printSchema(legacyInverseMeta, errors)}` + ); + } else { + assert( + `The '${addedIdentifier.type}' type does not implement '${parentDefinition.type}' and thus cannot be assigned to the '${parentDefinition.key}' relationship in '${parentIdentifier.type}'. If this relationship should be polymorphic, mark ${parentDefinition.inverseType}.${parentDefinition.key} as \`polymorphic: true\` and ${addedIdentifier.type}.${parentDefinition.inverseKey} as implementing it via \`as: '${parentDefinition.type}'\`.` + ); + } + } else if ((meta?.options?.as?.length ?? 0) > 0) { + asserted = true; assert( - `The '${addedIdentifier.type}' type does not implement '${parentDefinition.type}' and thus cannot be assigned to the '${parentDefinition.key}' relationship in '${parentIdentifier.type}'. If this relationship should be polymorphic, mark ${parentDefinition.inverseType}.${parentDefinition.key} as \`polymorphic: true\` and ${addedIdentifier.type}.${parentDefinition.inverseKey} as implementing it via \`as: '${parentDefinition.type}'\`.` + `Expected the field ${parentDefinition.inverseKey} to be a relationship`, + !meta || isRelationshipField(meta) ); + const legacyMeta = meta && (isLegacyField(meta) ? meta : temporaryConvertToLegacy(meta)); + if (legacyMeta?.options.as === parentDefinition.type) { + // inverse is likely polymorphic but missing the polymorphic flag + let meta = store.schema.fields({ type: parentDefinition.inverseType }).get(parentDefinition.key); + assert(`Expected the field ${parentDefinition.key} to be a relationship`, meta && isRelationshipField(meta)); + meta = isLegacyField(meta) ? meta : temporaryConvertToLegacy(meta); + const errors = validateSchema(definitionWithPolymorphic(inverseDefinition(parentDefinition)), meta); + assert( + `The '<${addedIdentifier.type}>.${ + parentDefinition.inverseKey + }' relationship cannot be used polymorphically because '<${parentDefinition.inverseType}>.${ + parentDefinition.key + } is not a polymorphic relationship. To use this relationship in a polymorphic manner, fix the following schema issues on the relationships schema for '${ + parentDefinition.inverseType + }':${printSchema(meta, errors)}` + ); + } else { + assert( + `The '${addedIdentifier.type}' type does not implement '${parentDefinition.type}' and thus cannot be assigned to the '${parentDefinition.key}' relationship in '${parentIdentifier.type}'. If this relationship should be polymorphic, mark ${parentDefinition.inverseType}.${parentDefinition.key} as \`polymorphic: true\` and ${addedIdentifier.type}.${parentDefinition.inverseKey} as implementing it via \`as: '${parentDefinition.type}'\`.` + ); + } + } + } + + if (DEPRECATE_NON_EXPLICIT_POLYMORPHISM) { + if (!asserted) { + const storeService = (store as unknown as { _store: Store })._store; + const addedModelName = addedIdentifier.type; + const parentModelName = parentIdentifier.type; + const key = parentDefinition.key; + const relationshipModelName = parentDefinition.type; + const relationshipClass = storeService.modelFor(relationshipModelName); + const addedClass = storeService.modelFor(addedModelName); + + const assertionMessage = `The '${addedModelName}' type does not implement '${relationshipModelName}' and thus cannot be assigned to the '${key}' relationship in '${parentModelName}'. Make it a descendant of '${relationshipModelName}' or use a mixin of the same name.`; + const isPolymorphic = checkPolymorphic(relationshipClass, addedClass); + + assert(assertionMessage, isPolymorphic); } } }; diff --git a/packages/graph/src/-private/edges/collection.ts b/packages/graph/src/-private/edges/collection.ts index a61e678aa04..ed61a0c6a7b 100644 --- a/packages/graph/src/-private/edges/collection.ts +++ b/packages/graph/src/-private/edges/collection.ts @@ -22,24 +22,8 @@ export interface CollectionEdge { links: Links | PaginationLinks | null; localState: StableRecordIdentifier[] | null; - /** - * Whether the localState for this edge is out-of-sync - * with the remoteState. - * - * if state.hasReceivedData=false we are also - * not dirty since there is nothing to sync with. - * - * @typedoc - */ isDirty: boolean; transactionRef: number; - /** - * Whether data for this edge has been accessed at least once - * via `graph.getData` - * - * @typedoc - */ - accessed: boolean; _diff?: { add: Set; @@ -61,15 +45,13 @@ export function createCollectionEdge(definition: UpgradedMeta, identifier: Stabl links: null, localState: null, - isDirty: false, + isDirty: true, transactionRef: 0, - accessed: false, _diff: undefined, }; } export function legacyGetCollectionRelationshipData(source: CollectionEdge): CollectionRelationship { - source.accessed = true; const payload: CollectionRelationship = {}; if (source.state.hasReceivedData) { diff --git a/packages/graph/src/-private/edges/resource.ts b/packages/graph/src/-private/edges/resource.ts index 5776734009c..3d5c615335a 100644 --- a/packages/graph/src/-private/edges/resource.ts +++ b/packages/graph/src/-private/edges/resource.ts @@ -23,7 +23,6 @@ export interface ResourceEdge { meta: Meta | null; links: Links | PaginationLinks | null; transactionRef: number; - accessed: boolean; } export function createResourceEdge(definition: UpgradedMeta, identifier: StableRecordIdentifier): ResourceEdge { @@ -36,12 +35,10 @@ export function createResourceEdge(definition: UpgradedMeta, identifier: StableR remoteState: null, meta: null, links: null, - accessed: false, }; } export function legacyGetResourceRelationshipData(source: ResourceEdge): ResourceRelationship { - source.accessed = true; let data: StableRecordIdentifier | null | undefined; const payload: ResourceRelationship = {}; if (source.localState) { diff --git a/packages/graph/src/-private/graph.ts b/packages/graph/src/-private/graph.ts index 18211af7fa2..b2a5b5b87f2 100644 --- a/packages/graph/src/-private/graph.ts +++ b/packages/graph/src/-private/graph.ts @@ -559,7 +559,7 @@ export class Graph { this._willSyncLocal = false; const updated = this._updatedRelationships; this._updatedRelationships = new Set(); - updated.forEach((rel) => notifyChange(this, rel)); + updated.forEach((rel) => notifyChange(this, rel.identifier, rel.definition.key)); } destroy() { @@ -641,7 +641,7 @@ function destroyRelationship(graph: Graph, rel: GraphEdge, silenceNotifications? // leave the ui relationship populated since the record is destroyed and // internally we've fully cleaned up. if (!rel.definition.isAsync && !silenceNotifications) { - /*#__NOINLINE__*/ notifyChange(graph, rel); + /*#__NOINLINE__*/ notifyChange(graph, rel.identifier, rel.definition.key); } } } @@ -713,7 +713,7 @@ function removeDematerializedInverse( } if (!silenceNotifications) { - notifyChange(graph, relationship); + notifyChange(graph, relationship.identifier, relationship.definition.key); } } else { if (!relationship.definition.isAsync || (inverseIdentifier && isNew(inverseIdentifier))) { @@ -728,7 +728,7 @@ function removeDematerializedInverse( } if (!silenceNotifications) { - notifyChange(graph, relationship); + notifyChange(graph, relationship.identifier, relationship.definition.key); } } } @@ -753,7 +753,7 @@ function removeCompletelyFromInverse(graph: Graph, relationship: GraphEdge) { if (!relationship.definition.isAsync) { clearRelationship(relationship); - notifyChange(graph, relationship); + notifyChange(graph, relationship.identifier, relationship.definition.key); } } else { relationship.remoteMembers.clear(); diff --git a/packages/graph/src/-private/operations/add-to-related-records.ts b/packages/graph/src/-private/operations/add-to-related-records.ts index 9757e040964..2c08cb4981b 100644 --- a/packages/graph/src/-private/operations/add-to-related-records.ts +++ b/packages/graph/src/-private/operations/add-to-related-records.ts @@ -19,13 +19,6 @@ export default function addToRelatedRecords(graph: Graph, op: AddToRelatedRecord `You can only '${op.op}' on a hasMany relationship. ${record.type}.${op.field} is a ${relationship.definition.kind}`, isHasMany(relationship) ); - - // if we are not dirty but have a null localState then we - // are mutating a relationship that has never been fetched - // so we initialize localState to an empty array - if (!relationship.isDirty && !relationship.localState) { - relationship.localState = []; - } if (Array.isArray(value)) { for (let i = 0; i < value.length; i++) { addRelatedRecord(graph, relationship, record, value[i], index !== undefined ? index + i : index, isRemote); @@ -34,7 +27,7 @@ export default function addToRelatedRecords(graph: Graph, op: AddToRelatedRecord addRelatedRecord(graph, relationship, record, value, index, isRemote); } - notifyChange(graph, relationship); + notifyChange(graph, relationship.identifier, relationship.definition.key); } function addRelatedRecord( diff --git a/packages/graph/src/-private/operations/merge-identifier.ts b/packages/graph/src/-private/operations/merge-identifier.ts index 91459c1f8e3..8c95f40179b 100644 --- a/packages/graph/src/-private/operations/merge-identifier.ts +++ b/packages/graph/src/-private/operations/merge-identifier.ts @@ -40,7 +40,7 @@ function mergeBelongsTo(graph: Graph, rel: ResourceEdge, op: MergeOperation): vo } if (rel.localState === op.record) { rel.localState = op.value; - notifyChange(graph, rel); + notifyChange(graph, rel.identifier, rel.definition.key); } } @@ -63,7 +63,7 @@ function mergeHasMany(graph: Graph, rel: CollectionEdge, op: MergeOperation): vo rel.isDirty = true; } if (rel.isDirty) { - notifyChange(graph, rel); + notifyChange(graph, rel.identifier, rel.definition.key); } } diff --git a/packages/graph/src/-private/operations/remove-from-related-records.ts b/packages/graph/src/-private/operations/remove-from-related-records.ts index 1d0f02543da..63f198d136f 100644 --- a/packages/graph/src/-private/operations/remove-from-related-records.ts +++ b/packages/graph/src/-private/operations/remove-from-related-records.ts @@ -30,7 +30,7 @@ export default function removeFromRelatedRecords(graph: Graph, op: RemoveFromRel } else { removeRelatedRecord(graph, relationship, record, value, isRemote); } - notifyChange(graph, relationship); + notifyChange(graph, relationship.identifier, relationship.definition.key); } function removeRelatedRecord( diff --git a/packages/graph/src/-private/operations/replace-related-record.ts b/packages/graph/src/-private/operations/replace-related-record.ts index 6acd0977d00..ee8833f2cbb 100644 --- a/packages/graph/src/-private/operations/replace-related-record.ts +++ b/packages/graph/src/-private/operations/replace-related-record.ts @@ -1,6 +1,9 @@ import { deprecate } from '@ember/debug'; -import { DEPRECATE_RELATIONSHIP_REMOTE_UPDATE_CLEARING_LOCAL_STATE } from '@warp-drive/build-config/deprecations'; +import { + DEPRECATE_RELATIONSHIP_REMOTE_UPDATE_CLEARING_LOCAL_STATE, + DISABLE_6X_DEPRECATIONS, +} from '@warp-drive/build-config/deprecations'; import { DEBUG } from '@warp-drive/build-config/env'; import { assert } from '@warp-drive/build-config/macros'; import type { StableRecordIdentifier } from '@warp-drive/core-types'; @@ -97,7 +100,7 @@ export default function replaceRelatedRecord(graph: Graph, op: ReplaceRelatedRec } belongsTo relationship but will not be once this deprecation is resolved:\n\n\t${ localState ? 'Added: ' + localState.lid + '\n\t' : '' }${existingState ? 'Removed: ' + existingState.lid : ''}`, - false, + /* inline-macro-config */ DISABLE_6X_DEPRECATIONS, { id: 'ember-data:deprecate-relationship-remote-update-clearing-local-state', for: 'ember-data', @@ -107,7 +110,7 @@ export default function replaceRelatedRecord(graph: Graph, op: ReplaceRelatedRec } ); - notifyChange(graph, relationship); + notifyChange(graph, relationship.identifier, relationship.definition.key); } } } @@ -156,7 +159,7 @@ export default function replaceRelatedRecord(graph: Graph, op: ReplaceRelatedRec // and we can safely sync the new remoteState to local if (localState !== remoteState && localState === existingState) { relationship.localState = remoteState; - notifyChange(graph, relationship); + notifyChange(graph, relationship.identifier, relationship.definition.key); // But when localState does not match the new remoteState and // and localState !== existingState then we know we have a local mutation // that has not been persisted yet. @@ -176,7 +179,7 @@ export default function replaceRelatedRecord(graph: Graph, op: ReplaceRelatedRec } belongsTo relationship but will not be once this deprecation is resolved:\n\n\t${ localState ? 'Added: ' + localState.lid + '\n\t' : '' }${existingState ? 'Removed: ' + existingState.lid : ''}`, - false, + /* inline-macro-config */ DISABLE_6X_DEPRECATIONS, { id: 'ember-data:deprecate-relationship-remote-update-clearing-local-state', for: 'ember-data', @@ -186,10 +189,10 @@ export default function replaceRelatedRecord(graph: Graph, op: ReplaceRelatedRec } ); - notifyChange(graph, relationship); + notifyChange(graph, relationship.identifier, relationship.definition.key); } } } else { - notifyChange(graph, relationship); + notifyChange(graph, relationship.identifier, relationship.definition.key); } } diff --git a/packages/graph/src/-private/operations/replace-related-records.ts b/packages/graph/src/-private/operations/replace-related-records.ts index 5f89d9015e2..6cc3c58c6a7 100644 --- a/packages/graph/src/-private/operations/replace-related-records.ts +++ b/packages/graph/src/-private/operations/replace-related-records.ts @@ -1,6 +1,9 @@ import { deprecate } from '@ember/debug'; -import { DEPRECATE_RELATIONSHIP_REMOTE_UPDATE_CLEARING_LOCAL_STATE } from '@warp-drive/build-config/deprecations'; +import { + DEPRECATE_RELATIONSHIP_REMOTE_UPDATE_CLEARING_LOCAL_STATE, + DISABLE_6X_DEPRECATIONS, +} from '@warp-drive/build-config/deprecations'; import { DEBUG } from '@warp-drive/build-config/env'; import { assert } from '@warp-drive/build-config/macros'; import type { StableRecordIdentifier } from '@warp-drive/core-types'; @@ -80,12 +83,18 @@ function replaceRelatedRecordsLocal(graph: Graph, op: ReplaceRelatedRecordsOpera const relationship = graph.get(op.record, op.field); assert(`expected hasMany relationship`, isHasMany(relationship)); + // relationships for newly created records begin in the dirty state, so if updated + // before flushed we would fail to notify. This check helps us avoid that. + const isMaybeFirstUpdate = + relationship.remoteState.length === 0 && + relationship.localState === null && + relationship.state.hasReceivedData === false; relationship.state.hasReceivedData = true; const { additions, removals } = relationship; const { inverseKey, type } = relationship.definition; const { record } = op; const wasDirty = relationship.isDirty; - let localBecameDirty = false; + relationship.isDirty = false; const onAdd = (identifier: StableRecordIdentifier) => { // Since we are diffing against the remote state, we check @@ -99,8 +108,7 @@ function replaceRelatedRecordsLocal(graph: Graph, op: ReplaceRelatedRecordsOpera graph.registerPolymorphicType(type, identifier.type); } - // we've added a record locally that wasn't in the local state before - localBecameDirty = true; + relationship.isDirty = true; addToInverse(graph, identifier, inverseKey, op.record, isRemote); if (removalsHas) { @@ -114,8 +122,7 @@ function replaceRelatedRecordsLocal(graph: Graph, op: ReplaceRelatedRecordsOpera // if our previous local state had contained this identifier const additionsHas = additions?.has(identifier); if (additionsHas || !removals?.has(identifier)) { - // we've removed a record locally that was in the local state before - localBecameDirty = true; + relationship.isDirty = true; removeFromInverse(graph, identifier, inverseKey, record, isRemote); if (additionsHas) { @@ -125,38 +132,41 @@ function replaceRelatedRecordsLocal(graph: Graph, op: ReplaceRelatedRecordsOpera }; const diff = diffCollection(identifiers, relationship, onAdd, onRemove); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + let becameDirty = relationship.isDirty || diff.changed; // any additions no longer in the local state - // also need to be removed from the inverse + // need to be removed from the inverse if (additions && additions.size > 0) { additions.forEach((identifier) => { if (!diff.add.has(identifier)) { - localBecameDirty = true; + becameDirty = true; onRemove(identifier); } }); } // any removals no longer in the local state - // also need to be added back to the inverse + // need to be added back to the inverse if (removals && removals.size > 0) { removals.forEach((identifier) => { if (!diff.del.has(identifier)) { - localBecameDirty = true; + becameDirty = true; onAdd(identifier); } }); } - const becameDirty = diff.changed || localBecameDirty; relationship.additions = diff.add; relationship.removals = diff.del; relationship.localState = diff.finalState; + relationship.isDirty = wasDirty; - // we only notify if the localState changed and were not already dirty before - // because if we were already dirty then we have already notified - if (becameDirty && !wasDirty) { - notifyChange(graph, relationship); + if ( + isMaybeFirstUpdate || + !wasDirty /*&& becameDirty // TODO to guard like this we need to detect reorder when diffing local */ + ) { + notifyChange(graph, op.record, op.field); } } @@ -171,15 +181,6 @@ function replaceRelatedRecordsRemote(graph: Graph, op: ReplaceRelatedRecordsOper if (isRemote) { graph._addToTransaction(relationship); } - - const wasDirty = relationship.isDirty; - // if this is our first time receiving data - // we need to mark the relationship as dirty - // so that non-materializing APIs like `hasManyReference.value()` - // will get notified and updated. - if (!relationship.state.hasReceivedData) { - relationship.isDirty = true; - } relationship.state.hasReceivedData = true; // cache existing state @@ -237,13 +238,7 @@ function replaceRelatedRecordsRemote(graph: Graph, op: ReplaceRelatedRecordsOper if (DEPRECATE_RELATIONSHIP_REMOTE_UPDATE_CLEARING_LOCAL_STATE) { // only do this for legacy hasMany, not collection // and provide a way to incrementally migrate - if ( - // we do not guard by diff.changed here - // because we want to clear local changes even if - // no change has occurred to preserve the legacy behavior - relationship.definition.kind === 'hasMany' && - relationship.definition.resetOnRemoteUpdate !== false - ) { + if (relationship.definition.kind === 'hasMany' && relationship.definition.resetOnRemoteUpdate !== false) { const deprecationInfo: { removals: StableRecordIdentifier[]; additions: StableRecordIdentifier[]; @@ -262,7 +257,7 @@ function replaceRelatedRecordsRemote(graph: Graph, op: ReplaceRelatedRecordsOper // if we are still in removals at this point then // we were not "committed" which means we are present // in the remoteMembers. So we "add back" on the inverse. - addToInverse(graph, identifier, definition.inverseKey, op.record, false); + addToInverse(graph, identifier, definition.inverseKey, op.record, isRemote); }); relationship.removals = null; } @@ -278,7 +273,7 @@ function replaceRelatedRecordsRemote(graph: Graph, op: ReplaceRelatedRecordsOper deprecationInfo.additions.push(identifier); relationship.isDirty = true; relationship.additions!.delete(identifier); - removeFromInverse(graph, identifier, definition.inverseKey, op.record, false); + removeFromInverse(graph, identifier, definition.inverseKey, op.record, isRemote); } }); if (relationship.additions.size === 0) { @@ -295,7 +290,7 @@ function replaceRelatedRecordsRemote(graph: Graph, op: ReplaceRelatedRecordsOper } hasMany relationship but will not be once this deprecation is resolved by opting into the new behavior:\n\n\tAdded: [${deprecationInfo.additions .map((i) => i.lid) .join(', ')}]\n\tRemoved: [${deprecationInfo.removals.map((i) => i.lid).join(', ')}]`, - false, + /* inline-macro-config */ DISABLE_6X_DEPRECATIONS, { id: 'ember-data:deprecate-relationship-remote-update-clearing-local-state', for: 'ember-data', @@ -308,7 +303,7 @@ function replaceRelatedRecordsRemote(graph: Graph, op: ReplaceRelatedRecordsOper } } - if (relationship.isDirty && !wasDirty) { + if (relationship.isDirty) { flushCanonical(graph, relationship); } } @@ -347,8 +342,7 @@ export function addToInverse( removeFromInverse(graph, relationship.localState, relationship.definition.inverseKey, identifier, isRemote); } relationship.localState = value; - - notifyChange(graph, relationship); + notifyChange(graph, identifier, key); } } else if (isHasMany(relationship)) { if (isRemote) { @@ -370,15 +364,8 @@ export function addToInverse( } } } else { - // if we are not dirty but have a null localState then we - // are mutating a relationship that has never been fetched - // so we initialize localState to an empty array - if (!relationship.isDirty && !relationship.localState) { - relationship.localState = []; - } - if (_addLocal(graph, identifier, relationship, value, null)) { - notifyChange(graph, relationship); + notifyChange(graph, identifier, key); } } } else { @@ -404,7 +391,7 @@ export function notifyInverseOfPotentialMaterialization( ) { const relationship = graph.get(identifier, key); if (isHasMany(relationship) && isRemote && relationship.remoteMembers.has(value)) { - notifyChange(graph, relationship); + notifyChange(graph, identifier, key); } } @@ -426,17 +413,17 @@ export function removeFromInverse( if (relationship.localState === value) { relationship.localState = null; - notifyChange(graph, relationship); + notifyChange(graph, identifier, key); } } else if (isHasMany(relationship)) { if (isRemote) { graph._addToTransaction(relationship); if (_removeRemote(relationship, value)) { - notifyChange(graph, relationship); + notifyChange(graph, identifier, key); } } else { if (_removeLocal(relationship, value)) { - notifyChange(graph, relationship); + notifyChange(graph, identifier, key); } } } else { @@ -452,7 +439,5 @@ export function removeFromInverse( } function flushCanonical(graph: Graph, rel: CollectionEdge) { - if (rel.accessed) { - graph._scheduleLocalSync(rel); - } + graph._scheduleLocalSync(rel); } diff --git a/packages/graph/src/-private/operations/update-relationship.ts b/packages/graph/src/-private/operations/update-relationship.ts index 6f8c7bdbc16..96fd82f7887 100644 --- a/packages/graph/src/-private/operations/update-relationship.ts +++ b/packages/graph/src/-private/operations/update-relationship.ts @@ -157,7 +157,7 @@ export default function updateRelationshipOperation(graph: Graph, op: UpdateRela ) { relationship.state.isStale = true; - notifyChange(graph, relationship); + notifyChange(graph, relationship.identifier, relationship.definition.key); } else { relationship.state.isStale = false; } diff --git a/packages/graph/vite.config.mjs b/packages/graph/vite.config.mjs index 7936563a474..a548ae349de 100644 --- a/packages/graph/vite.config.mjs +++ b/packages/graph/vite.config.mjs @@ -1,6 +1,7 @@ import { createConfig } from '@warp-drive/internal-config/vite/config.js'; export const externals = [ + '@ember/object/mixin', // type only '@ember/debug', // assert, deprecate ]; diff --git a/packages/holodeck/README.md b/packages/holodeck/README.md index 0f736954864..4a8263a486a 100644 --- a/packages/holodeck/README.md +++ b/packages/holodeck/README.md @@ -107,8 +107,8 @@ import RequestManager from '@ember-data/request'; import Fetch from '@ember-data/request/fetch'; import { MockServerHandler } from '@warp-drive/holodeck'; -const manager = new RequestManager() - .use([new MockServerHandler(testContext), Fetch]); +const manager = new RequestManager(); +manager.use([new MockServerHandler(testContext), Fetch]); ``` From within a test this might look like: @@ -121,8 +121,8 @@ import { module, test } from 'qunit'; module('my module', function() { test('my test', async function() { - const manager = new RequestManager() - .use([new MockServerHandler(this), Fetch]); + const manager = new RequestManager(); + manager.use([new MockServerHandler(this), Fetch]); }); }); ``` diff --git a/packages/holodeck/package.json b/packages/holodeck/package.json index 2d97785a827..f05a0039a90 100644 --- a/packages/holodeck/package.json +++ b/packages/holodeck/package.json @@ -1,7 +1,8 @@ { "name": "@warp-drive/holodeck", "description": "⚡️ Simple, Fast HTTP Mocking for Tests", - "version": "0.0.0-alpha.122", + "version": "4.12.9-alpha.3", + "private": true, "license": "MIT", "author": "Chris Thoburn ", "repository": { @@ -23,7 +24,7 @@ "dependencies": { "@hono/node-server": "^1.11.1", "chalk": "^5.3.0", - "hono": "^4.7.0" + "hono": "^4.6.5" }, "type": "module", "files": [ diff --git a/packages/json-api/package.json b/packages/json-api/package.json index 6a093e3682c..e536ae89016 100644 --- a/packages/json-api/package.json +++ b/packages/json-api/package.json @@ -1,6 +1,6 @@ { "name": "@ember-data/json-api", - "version": "5.4.0-alpha.136", + "version": "4.13.0-alpha.3", "description": "Provides a JSON:API document and resource cache implementation for EmberData", "keywords": [ "ember-addon" @@ -72,7 +72,7 @@ "@warp-drive/core-types": "workspace:*" }, "dependencies": { - "@embroider/macros": "^1.16.10", + "@embroider/macros": "^1.16.6", "@warp-drive/build-config": "workspace:*" }, "devDependencies": { diff --git a/packages/json-api/src/-private/cache.ts b/packages/json-api/src/-private/cache.ts index ccefec1b8e2..4f6cbeb4fea 100644 --- a/packages/json-api/src/-private/cache.ts +++ b/packages/json-api/src/-private/cache.ts @@ -4,7 +4,6 @@ import type { CollectionEdge, Graph, GraphEdge, ImplicitEdge, ResourceEdge } from '@ember-data/graph/-private'; import { graphFor, isBelongsTo, peekGraph } from '@ember-data/graph/-private'; import type Store from '@ember-data/store'; -import { logGroup } from '@ember-data/store/-private'; import type { CacheCapabilitiesManager } from '@ember-data/store/types'; import { LOG_MUTATIONS, LOG_OPERATIONS, LOG_REQUESTS } from '@warp-drive/build-config/debugging'; import { DEPRECATE_RELATIONSHIP_REMOTE_UPDATE_CLEARING_LOCAL_STATE } from '@warp-drive/build-config/deprecations'; @@ -50,8 +49,6 @@ import type { SingleResourceRelationship, } from '@warp-drive/core-types/spec/json-api-raw'; -import { validateDocumentFields } from './validate-document-fields'; - type IdentifierCache = Store['identifierCache']; type InternalCapabilitiesManager = CacheCapabilitiesManager & { _store: Store }; @@ -216,10 +213,6 @@ export default class JSONAPICache implements Cache { let i: number, length: number; const { identifierCache } = this._capabilities; - if (DEBUG) { - validateDocumentFields(this._capabilities.schema, jsonApiDoc); - } - if (LOG_REQUESTS) { const Counts = new Map(); if (included) { @@ -277,6 +270,11 @@ export default class JSONAPICache implements Cache { ); } + assert( + `Expected a resource object in the 'data' property in the document provided to the cache, but was ${typeof jsonApiDoc.data}`, + typeof jsonApiDoc.data === 'object' + ); + const identifier = putOne(this, identifierCache, jsonApiDoc.data); return this._putDocument( doc as StructuredDataDocument, @@ -335,7 +333,7 @@ export default class JSONAPICache implements Cache { const hasExisting = this.__documents.has(identifier.lid); this.__documents.set(identifier.lid, doc as StructuredDocument); - this._capabilities.notifyChange(identifier, hasExisting ? 'updated' : 'added', null); + this._capabilities.notifyChange(identifier, hasExisting ? 'updated' : 'added'); } return resourceDocument; @@ -384,22 +382,16 @@ export default class JSONAPICache implements Cache { */ mutate(mutation: LocalRelationshipOperation): void { if (LOG_MUTATIONS) { - logGroup('cache', 'mutate', mutation.record.type, mutation.record.lid, mutation.field, mutation.op); try { const _data = JSON.parse(JSON.stringify(mutation)) as object; // eslint-disable-next-line no-console - console.log(_data); + console.log(`EmberData | Mutation - update ${mutation.op}`, _data); } catch { // eslint-disable-next-line no-console - console.log(mutation); + console.log(`EmberData | Mutation - update ${mutation.op}`, mutation); } } this.__graph.update(mutation, false); - - if (LOG_MUTATIONS) { - // eslint-disable-next-line no-console - console.groupEnd(); - } } /** @@ -523,7 +515,7 @@ export default class JSONAPICache implements Cache { data: ExistingResourceObject, calculateChanges?: boolean ): void | string[] { - let changedKeys: Set | undefined; + let changedKeys: string[] | undefined; const peeked = this.__safePeek(identifier, false); const existed = !!peeked; const cached = peeked || this._createCache(identifier); @@ -532,50 +524,38 @@ export default class JSONAPICache implements Cache { const isUpdate = /*#__NOINLINE__*/ !_isEmpty(peeked) && !isLoading; if (LOG_OPERATIONS) { - logGroup( - 'cache', - 'upsert', - identifier.type, - identifier.lid, - existed ? 'merged' : 'inserted', - calculateChanges ? 'has-subscription' : '' - ); try { const _data = JSON.parse(JSON.stringify(data)) as object; - // eslint-disable-next-line no-console - console.log(_data); + console.log(`EmberData | Operation - upsert (${existed ? 'merge' : 'insert'})`, _data); } catch { // eslint-disable-next-line no-console - console.log(data); + console.log(`EmberData | Operation - upsert (${existed ? 'merge' : 'insert'})`, data); } } if (cached.isNew) { cached.isNew = false; - this._capabilities.notifyChange(identifier, 'identity', null); - this._capabilities.notifyChange(identifier, 'state', null); + this._capabilities.notifyChange(identifier, 'identity'); + this._capabilities.notifyChange(identifier, 'state'); } - // if no cache entry existed, no record exists / property has been accessed - // and thus we do not need to notify changes to any properties. - if (calculateChanges && existed && data.attributes) { - changedKeys = calculateChangedKeys(cached, data.attributes); + if (calculateChanges) { + changedKeys = existed ? calculateChangedKeys(cached, data.attributes) : Object.keys(data.attributes || {}); } cached.remoteAttrs = Object.assign( cached.remoteAttrs || (Object.create(null) as Record), data.attributes ); - if (cached.localAttrs) { - if (patchLocalAttributes(cached, changedKeys)) { - this._capabilities.notifyChange(identifier, 'state', null); + if (patchLocalAttributes(cached)) { + this._capabilities.notifyChange(identifier, 'state'); } } if (!isUpdate) { - this._capabilities.notifyChange(identifier, 'added', null); + this._capabilities.notifyChange(identifier, 'added'); } if (data.id) { @@ -586,16 +566,11 @@ export default class JSONAPICache implements Cache { setupRelationships(this.__graph, this._capabilities, identifier, data); } - if (changedKeys?.size) { + if (changedKeys && changedKeys.length) { notifyAttributes(this._capabilities, identifier, changedKeys); } - if (LOG_OPERATIONS) { - // eslint-disable-next-line no-console - console.groupEnd(); - } - - return changedKeys?.size ? Array.from(changedKeys) : undefined; + return changedKeys; } // Cache Forking Support @@ -787,7 +762,7 @@ export default class JSONAPICache implements Cache { } } - this._capabilities.notifyChange(identifier, 'added', null); + this._capabilities.notifyChange(identifier, 'added'); return createOptions; } @@ -893,7 +868,7 @@ export default class JSONAPICache implements Cache { isNew: false, }); cached.isDeletionCommitted = true; - this._capabilities.notifyChange(identifier, 'removed', null); + this._capabilities.notifyChange(identifier, 'removed'); // TODO @runspired should we early exit here? } @@ -915,7 +890,7 @@ export default class JSONAPICache implements Cache { cached.id = data.id; } if (identifier === committedIdentifier && identifier.id !== existingId) { - this._capabilities.notifyChange(identifier, 'identity', null); + this._capabilities.notifyChange(identifier, 'identity'); } assert( @@ -958,7 +933,7 @@ export default class JSONAPICache implements Cache { } newCanonicalAttributes = data.attributes; } - const changedKeys = newCanonicalAttributes && calculateChangedKeys(cached, newCanonicalAttributes); + const changedKeys = calculateChangedKeys(cached, newCanonicalAttributes); cached.remoteAttrs = Object.assign( cached.remoteAttrs || (Object.create(null) as Record), @@ -966,15 +941,15 @@ export default class JSONAPICache implements Cache { newCanonicalAttributes ); cached.inflightAttrs = null; - patchLocalAttributes(cached, changedKeys); + patchLocalAttributes(cached); if (cached.errors) { cached.errors = null; - this._capabilities.notifyChange(identifier, 'errors', null); + this._capabilities.notifyChange(identifier, 'errors'); } - if (changedKeys?.size) notifyAttributes(this._capabilities, identifier, changedKeys); - this._capabilities.notifyChange(identifier, 'state', null); + notifyAttributes(this._capabilities, identifier, changedKeys); + this._capabilities.notifyChange(identifier, 'state'); const included = payload && payload.included; if (included) { @@ -1015,7 +990,7 @@ export default class JSONAPICache implements Cache { if (errors) { cached.errors = errors; } - this._capabilities.notifyChange(identifier, 'errors', null); + this._capabilities.notifyChange(identifier, 'errors'); } /** @@ -1065,7 +1040,7 @@ export default class JSONAPICache implements Cache { if (areAllModelsUnloaded(storeWrapper, relatedIdentifiers)) { for (let i = 0; i < relatedIdentifiers.length; ++i) { const relatedIdentifier = relatedIdentifiers[i]; - storeWrapper.notifyChange(relatedIdentifier, 'removed', null); + storeWrapper.notifyChange(relatedIdentifier, 'removed'); removed = true; storeWrapper.disconnectRecord(relatedIdentifier); } @@ -1095,7 +1070,7 @@ export default class JSONAPICache implements Cache { } if (!removed && removeFromRecordArray) { - storeWrapper.notifyChange(identifier, 'removed', null); + storeWrapper.notifyChange(identifier, 'removed'); } } @@ -1382,13 +1357,13 @@ export default class JSONAPICache implements Cache { if (cached.errors) { cached.errors = null; - this._capabilities.notifyChange(identifier, 'errors', null); + this._capabilities.notifyChange(identifier, 'errors'); } - this._capabilities.notifyChange(identifier, 'state', null); + this._capabilities.notifyChange(identifier, 'state'); if (dirtyKeys && dirtyKeys.length) { - notifyAttributes(this._capabilities, identifier, new Set(dirtyKeys)); + notifyAttributes(this._capabilities, identifier, dirtyKeys); } return dirtyKeys || []; @@ -1489,7 +1464,7 @@ export default class JSONAPICache implements Cache { const cached = this.__peek(identifier, false); cached.isDeleted = isDeleted; // > Note: Graph removal for isNew handled by unloadRecord - this._capabilities.notifyChange(identifier, 'state', null); + this._capabilities.notifyChange(identifier, 'state'); } /** @@ -1685,18 +1660,14 @@ function getDefaultValue( } } -function notifyAttributes( - storeWrapper: CacheCapabilitiesManager, - identifier: StableRecordIdentifier, - keys?: Set -) { +function notifyAttributes(storeWrapper: CacheCapabilitiesManager, identifier: StableRecordIdentifier, keys?: string[]) { if (!keys) { - storeWrapper.notifyChange(identifier, 'attributes', null); + storeWrapper.notifyChange(identifier, 'attributes'); return; } - for (const key of keys) { - storeWrapper.notifyChange(identifier, 'attributes', key); + for (let i = 0; i < keys.length; i++) { + storeWrapper.notifyChange(identifier, 'attributes', keys[i]); } } @@ -1705,35 +1676,35 @@ function notifyAttributes( There seems to be a potential bug here, where we will return keys that are not in the schema */ -function calculateChangedKeys( - cached: CachedResource, - updates: Exclude -): Set { - const changedKeys = new Set(); - const keys = Object.keys(updates); - const length = keys.length; - const localAttrs = cached.localAttrs; - - const original: Record = Object.assign( - Object.create(null) as Record, - cached.remoteAttrs, - cached.inflightAttrs - ); +function calculateChangedKeys(cached: CachedResource, updates?: ExistingResourceObject['attributes']): string[] { + const changedKeys: string[] = []; + + if (updates) { + const keys = Object.keys(updates); + const length = keys.length; + const localAttrs = cached.localAttrs; + + const original: Record = Object.assign( + Object.create(null) as Record, + cached.remoteAttrs, + cached.inflightAttrs + ); - for (let i = 0; i < length; i++) { - const key = keys[i]; - const value = updates[key]; + for (let i = 0; i < length; i++) { + const key = keys[i]; + const value = updates[key]; - // A value in localAttrs means the user has a local change to - // this attribute. We never override this value when merging - // updates from the backend so we should not sent a change - // notification if the server value differs from the original. - if (localAttrs && localAttrs[key] !== undefined) { - continue; - } + // A value in localAttrs means the user has a local change to + // this attribute. We never override this value when merging + // updates from the backend so we should not sent a change + // notification if the server value differs from the original. + if (localAttrs && localAttrs[key] !== undefined) { + continue; + } - if (original[key] !== value) { - changedKeys.add(key); + if (original[key] !== value) { + changedKeys.push(key); + } } } @@ -1826,7 +1797,7 @@ function isRelationship(field: FieldSchema): field is LegacyRelationshipSchema | return RelationshipKinds.has(field.kind); } -function patchLocalAttributes(cached: CachedResource, changedRemoteKeys?: Set): boolean { +function patchLocalAttributes(cached: CachedResource): boolean { const { localAttrs, remoteAttrs, inflightAttrs, defaultAttrs, changes } = cached; if (!localAttrs) { cached.changes = null; @@ -1846,11 +1817,6 @@ function patchLocalAttributes(cached: CachedResource, changedRemoteKeys?: Set", "repository": { @@ -85,7 +85,7 @@ } }, "dependencies": { - "@embroider/macros": "^1.16.10", + "@embroider/macros": "^1.16.6", "@warp-drive/build-config": "workspace:*" }, "peerDependencies": { diff --git a/packages/legacy-compat/src/builders/utils.ts b/packages/legacy-compat/src/builders/utils.ts index 2cc2c71689f..dc29dd7bb26 100644 --- a/packages/legacy-compat/src/builders/utils.ts +++ b/packages/legacy-compat/src/builders/utils.ts @@ -1,7 +1,7 @@ import { deprecate } from '@ember/debug'; import { dasherize } from '@ember-data/request-utils/string'; -import { DEPRECATE_NON_STRICT_TYPES } from '@warp-drive/build-config/deprecations'; +import { DEPRECATE_NON_STRICT_TYPES, DISABLE_6X_DEPRECATIONS } from '@warp-drive/build-config/deprecations'; import type { ResourceIdentifierObject } from '@warp-drive/core-types/spec/json-api-raw'; export function isMaybeIdentifier( @@ -21,7 +21,7 @@ export function normalizeModelName(type: string): string { deprecate( `The resource type '${type}' is not normalized. Update your application code to use '${result}' instead of '${type}'.`, - result === type, + /* inline-macro-config */ DISABLE_6X_DEPRECATIONS ? true : result === type, { id: 'ember-data:deprecate-non-strict-types', until: '6.0', diff --git a/packages/legacy-compat/src/index.ts b/packages/legacy-compat/src/index.ts index 62a23740ef7..c9077b86e46 100644 --- a/packages/legacy-compat/src/index.ts +++ b/packages/legacy-compat/src/index.ts @@ -1,12 +1,14 @@ import { getOwner } from '@ember/application'; +import { deprecate } from '@ember/debug'; import type Store from '@ember-data/store'; import { recordIdentifierFor } from '@ember-data/store'; -import { _deprecatingNormalize } from '@ember-data/store/-private'; +import { DEPRECATE_JSON_API_FALLBACK } from '@warp-drive/build-config/deprecations'; import { assert } from '@warp-drive/build-config/macros'; import type { ObjectValue } from '@warp-drive/core-types/json/raw'; import { FetchManager, upgradeStore } from './-private'; +import { normalizeModelName } from './builders/utils'; import type { AdapterPayload, MinimumAdapterInterface } from './legacy-network-handler/minimum-adapter-interface'; import type { MinimumSerializerInterface, @@ -68,7 +70,7 @@ export function adapterFor(this: Store, modelName: string, _allowMissing?: true) this._adapterCache = this._adapterCache || (Object.create(null) as Record); - const normalizedModelName = _deprecatingNormalize(modelName); + const normalizedModelName = normalizeModelName(modelName); const { _adapterCache } = this; let adapter: (MinimumAdapterInterface & { store: Store }) | undefined = _adapterCache[normalizedModelName]; @@ -93,6 +95,28 @@ export function adapterFor(this: Store, modelName: string, _allowMissing?: true) return adapter; } + if (DEPRECATE_JSON_API_FALLBACK) { + // final fallback, no model specific adapter, no application adapter, no + // `adapter` property on store: use json-api adapter + adapter = _adapterCache['-json-api'] || owner.lookup('adapter:-json-api'); + if (adapter !== undefined) { + deprecate( + `Your application is utilizing a deprecated hidden fallback adapter (-json-api). Please implement an application adapter to function as your fallback.`, + false, + { + id: 'ember-data:deprecate-secret-adapter-fallback', + for: 'ember-data', + until: '5.0', + since: { available: '4.5', enabled: '4.5' }, + } + ); + _adapterCache[normalizedModelName] = adapter; + _adapterCache['-json-api'] = adapter; + + return adapter; + } + } + assert( `No adapter was found for '${modelName}' and no 'application' adapter was found as a fallback.`, _allowMissing @@ -129,7 +153,7 @@ export function serializerFor(this: Store, modelName: string): MinimumSerializer upgradeStore(this); this._serializerCache = this._serializerCache || (Object.create(null) as Record); - const normalizedModelName = _deprecatingNormalize(modelName); + const normalizedModelName = normalizeModelName(modelName); const { _serializerCache } = this; let serializer: (MinimumSerializerInterface & { store: Store }) | undefined = _serializerCache[normalizedModelName]; @@ -190,7 +214,7 @@ export function normalize(this: Store, modelName: string, payload: ObjectValue) `Passing classes to store methods has been removed. Please pass a dasherized string instead of ${typeof modelName}`, typeof modelName === 'string' ); - const normalizedModelName = _deprecatingNormalize(modelName); + const normalizedModelName = normalizeModelName(modelName); const serializer = this.serializerFor(normalizedModelName); const schema = this.modelFor(normalizedModelName); assert( @@ -265,7 +289,7 @@ export function pushPayload(this: Store, modelName: string, inputPayload: Object ); const payload: ObjectValue = inputPayload || (modelName as unknown as ObjectValue); - const normalizedModelName = inputPayload ? _deprecatingNormalize(modelName) : 'application'; + const normalizedModelName = inputPayload ? normalizeModelName(modelName) : 'application'; const serializer = this.serializerFor(normalizedModelName); assert( diff --git a/packages/legacy-compat/src/legacy-network-handler/fetch-manager.ts b/packages/legacy-compat/src/legacy-network-handler/fetch-manager.ts index 1e30751209e..68ae09dd30c 100644 --- a/packages/legacy-compat/src/legacy-network-handler/fetch-manager.ts +++ b/packages/legacy-compat/src/legacy-network-handler/fetch-manager.ts @@ -1,4 +1,4 @@ -import { warn } from '@ember/debug'; +import { deprecate, warn } from '@ember/debug'; import { dependencySatisfies, importSync, macroCondition } from '@embroider/macros'; @@ -13,6 +13,7 @@ import type { } from '@ember-data/store/-private'; import { coerceId } from '@ember-data/store/-private'; import type { FindRecordOptions, ModelSchema } from '@ember-data/store/types'; +import { DEPRECATE_RSVP_PROMISE } from '@warp-drive/build-config/deprecations'; import { DEBUG, TESTING } from '@warp-drive/build-config/env'; import { assert } from '@warp-drive/build-config/macros'; import { getOrSetGlobal } from '@warp-drive/core-types/-private'; @@ -28,6 +29,7 @@ import type { AdapterPayload, MinimumAdapterInterface } from './minimum-adapter- import type { MinimumSerializerInterface } from './minimum-serializer-interface'; import { normalizeResponseHelper } from './serializer-response'; import { Snapshot } from './snapshot'; +import { _objectIsAlive } from './utils'; type Deferred = ReturnType>; type AdapterErrors = Error & { errors?: string[]; isAdapterError?: true }; @@ -611,6 +613,7 @@ function _flushPendingSave(store: Store, pending: PendingSaveItem) { const modelName = snapshot.modelName; const modelClass = store.modelFor(modelName); + const record = store._instanceCache.getRecord(identifier); assert(`You tried to update a record but you have no adapter (for ${modelName})`, adapter); assert( @@ -627,6 +630,24 @@ function _flushPendingSave(store: Store, pending: PendingSaveItem) { ); promise = promise.then((adapterPayload) => { + if (!_objectIsAlive(record)) { + if (DEPRECATE_RSVP_PROMISE) { + deprecate( + `A Promise while saving ${modelName} did not resolve by the time your model was destroyed. This will error in a future release.`, + false, + { + id: 'ember-data:rsvp-unresolved-async', + until: '5.0', + for: '@ember-data/store', + since: { + available: '4.5', + enabled: '4.5', + }, + } + ); + } + } + if (adapterPayload) { return normalizeResponseHelper(serializer, store, modelClass, adapterPayload, snapshot.id, operation); } diff --git a/packages/legacy-compat/src/legacy-network-handler/legacy-data-fetch.ts b/packages/legacy-compat/src/legacy-network-handler/legacy-data-fetch.ts index d1618279415..6864f61fa05 100644 --- a/packages/legacy-compat/src/legacy-network-handler/legacy-data-fetch.ts +++ b/packages/legacy-compat/src/legacy-network-handler/legacy-data-fetch.ts @@ -1,8 +1,12 @@ +import { deprecate } from '@ember/debug'; + import type Store from '@ember-data/store'; -import type { BaseFinderOptions } from '@ember-data/store/types'; +import type { BaseFinderOptions, ModelSchema } from '@ember-data/store/types'; +import { DEPRECATE_RELATIONSHIPS_WITHOUT_INVERSE, DEPRECATE_RSVP_PROMISE } from '@warp-drive/build-config/deprecations'; import { DEBUG } from '@warp-drive/build-config/env'; import { assert } from '@warp-drive/build-config/macros'; import type { StableRecordIdentifier } from '@warp-drive/core-types'; +import type { StableExistingRecordIdentifier } from '@warp-drive/core-types/identifier'; import type { LegacyRelationshipSchema as RelationshipSchema } from '@warp-drive/core-types/schema/fields'; import type { ExistingResourceObject, JsonApiDocument } from '@warp-drive/core-types/spec/json-api-raw'; @@ -10,6 +14,7 @@ import { upgradeStore } from '../-private'; import { iterateData, payloadIsNotBlank } from './legacy-data-utils'; import type { MinimumAdapterInterface } from './minimum-adapter-interface'; import { normalizeResponseHelper } from './serializer-response'; +import { _bind, _guard, _objectIsAlive, guardDestroyedStore } from './utils'; export function _findHasMany( adapter: MinimumAdapterInterface, @@ -18,9 +23,9 @@ export function _findHasMany( link: string | null | { href: string }, relationship: RelationshipSchema, options: BaseFinderOptions -) { +): Promise { upgradeStore(store); - const promise = Promise.resolve().then(() => { + let promise: Promise = Promise.resolve().then(() => { const snapshot = store._fetchManager.createSnapshot(identifier, options); const useLink = !link || typeof link === 'string'; const relatedLink = useLink ? link : link.href; @@ -35,7 +40,28 @@ export function _findHasMany( return adapter.findHasMany(store, snapshot, relatedLink, relationship); }); - return promise.then((adapterPayload) => { + promise = guardDestroyedStore(promise, store); + promise = promise.then((adapterPayload) => { + const record = store._instanceCache.getRecord(identifier); + + if (!_objectIsAlive(record)) { + if (DEPRECATE_RSVP_PROMISE) { + deprecate( + `A Promise for fetching ${relationship.type} did not resolve by the time your model was destroyed. This will error in a future release.`, + false, + { + id: 'ember-data:rsvp-unresolved-async', + until: '5.0', + for: '@ember-data/store', + since: { + available: '4.5', + enabled: '4.5', + }, + } + ); + } + } + assert( `You made a 'findHasMany' request for a ${identifier.type}'s '${ relationship.name @@ -59,6 +85,14 @@ export function _findHasMany( payload = syncRelationshipDataFromLink(store, payload, identifier as ResourceIdentity, relationship); return store._push(payload, true); }, null); + + if (DEPRECATE_RSVP_PROMISE) { + const record = store._instanceCache.getRecord(identifier); + + promise = _guard(promise, _bind(_objectIsAlive, record)); + } + + return promise as Promise; } export function _findBelongsTo( @@ -69,7 +103,7 @@ export function _findBelongsTo( options: BaseFinderOptions ) { upgradeStore(store); - const promise = Promise.resolve().then(() => { + let promise = Promise.resolve().then(() => { const adapter = store.adapterFor(identifier.type); assert(`You tried to load a belongsTo relationship but you have no adapter (for ${identifier.type})`, adapter); assert( @@ -86,7 +120,33 @@ export function _findBelongsTo( return adapter.findBelongsTo(store, snapshot, relatedLink, relationship); }); + if (DEPRECATE_RSVP_PROMISE) { + const record = store._instanceCache.getRecord(identifier); + promise = guardDestroyedStore(promise, store); + promise = _guard(promise, _bind(_objectIsAlive, record)); + } + return promise.then((adapterPayload) => { + if (DEPRECATE_RSVP_PROMISE) { + const record = store._instanceCache.getRecord(identifier); + + if (!_objectIsAlive(record)) { + deprecate( + `A Promise for fetching ${relationship.type} did not resolve by the time your model was destroyed. This will error in a future release.`, + false, + { + id: 'ember-data:rsvp-unresolved-async', + until: '5.0', + for: '@ember-data/store', + since: { + available: '4.5', + enabled: '4.5', + }, + } + ); + } + } + const modelClass = store.modelFor(relationship.type); const serializer = store.serializerFor(relationship.type); let payload = normalizeResponseHelper(serializer, store, modelClass, adapterPayload, null, 'findBelongsTo'); @@ -226,11 +286,24 @@ function ensureRelationshipIsSetToParent( } } +type LegacyRelationshipDefinition = { _inverseKey: (store: Store, modelClass: ModelSchema) => string | null }; + +function metaIsRelationshipDefinition(meta: unknown): meta is LegacyRelationshipDefinition { + return typeof meta === 'object' && !!meta && '_inverseKey' in meta && typeof meta._inverseKey === 'function'; +} + function inverseForRelationship(store: Store, identifier: { type: string; id?: string }, key: string) { const definition = store.schema.fields(identifier).get(key); if (!definition) { return null; } + + if (DEPRECATE_RELATIONSHIPS_WITHOUT_INVERSE) { + if (metaIsRelationshipDefinition(definition)) { + const modelClass = store.modelFor(identifier.type); + return definition._inverseKey(store, modelClass); + } + } assert( `Expected the field definition to be a relationship`, definition.kind === 'hasMany' || definition.kind === 'belongsTo' diff --git a/packages/legacy-compat/src/legacy-network-handler/snapshot-record-array.ts b/packages/legacy-compat/src/legacy-network-handler/snapshot-record-array.ts index 1121ec58fc8..a6e996c9bd9 100644 --- a/packages/legacy-compat/src/legacy-network-handler/snapshot-record-array.ts +++ b/packages/legacy-compat/src/legacy-network-handler/snapshot-record-array.ts @@ -1,14 +1,19 @@ /** @module @ember-data/legacy-compat */ + +import { deprecate } from '@ember/debug'; + import type Store from '@ember-data/store'; import type { LiveArray } from '@ember-data/store/-private'; import { SOURCE } from '@ember-data/store/-private'; import type { FindAllOptions, ModelSchema } from '@ember-data/store/types'; +import { DEPRECATE_SNAPSHOT_MODEL_CLASS_ACCESS } from '@warp-drive/build-config/deprecations'; import type { StableRecordIdentifier } from '@warp-drive/core-types'; import { upgradeStore } from '../-private'; import type { Snapshot } from './snapshot'; + /** SnapshotRecordArray is not directly instantiable. Instances are provided to consuming application's @@ -180,3 +185,29 @@ export class SnapshotRecordArray { return this._snapshots; } } + +if (DEPRECATE_SNAPSHOT_MODEL_CLASS_ACCESS) { + /** + The type of the underlying records for the snapshots in the array, as a Model + + @deprecated + @property type + @public + @type {Model} + */ + Object.defineProperty(SnapshotRecordArray.prototype, 'type', { + get() { + deprecate( + `Using SnapshotRecordArray.type to access the ModelClass for a record is deprecated. Use store.modelFor() instead.`, + false, + { + id: 'ember-data:deprecate-snapshot-model-class-access', + until: '5.0', + for: 'ember-data', + since: { available: '4.5.0', enabled: '4.5.0' }, + } + ); + return (this as SnapshotRecordArray)._recordArray.type as ModelSchema; + }, + }); +} diff --git a/packages/legacy-compat/src/legacy-network-handler/snapshot.ts b/packages/legacy-compat/src/legacy-network-handler/snapshot.ts index 1198930fc9a..becb2a7524d 100644 --- a/packages/legacy-compat/src/legacy-network-handler/snapshot.ts +++ b/packages/legacy-compat/src/legacy-network-handler/snapshot.ts @@ -1,11 +1,14 @@ /** @module @ember-data/store */ +import { deprecate } from '@ember/debug'; + import { dependencySatisfies, importSync } from '@embroider/macros'; import type { CollectionEdge, ResourceEdge } from '@ember-data/graph/-private'; import type Store from '@ember-data/store'; -import type { FindRecordOptions } from '@ember-data/store/types'; +import type { FindRecordOptions, ModelSchema } from '@ember-data/store/types'; +import { DEPRECATE_SNAPSHOT_MODEL_CLASS_ACCESS } from '@warp-drive/build-config/deprecations'; import { DEBUG } from '@warp-drive/build-config/env'; import { assert } from '@warp-drive/build-config/macros'; import type { StableRecordIdentifier } from '@warp-drive/core-types'; @@ -46,6 +49,16 @@ export class Snapshot { declare adapterOptions?: Record; declare _store: Store; + /** + The type of the underlying record for this snapshot, as a Model. + + @property type + @public + @deprecated + @type {Model} + */ + declare type: ModelSchema; + /** * @method constructor * @constructor @@ -561,3 +574,21 @@ export class Snapshot { return serializer.serialize(this, options); } } + +if (DEPRECATE_SNAPSHOT_MODEL_CLASS_ACCESS) { + Object.defineProperty(Snapshot.prototype, 'type', { + get(this: Snapshot) { + deprecate( + `Using Snapshot.type to access the ModelClass for a record is deprecated. Use store.modelFor() instead.`, + false, + { + id: 'ember-data:deprecate-snapshot-model-class-access', + until: '5.0', + for: 'ember-data', + since: { available: '4.5.0', enabled: '4.5.0' }, + } + ); + return this._store.modelFor(this.identifier.type); + }, + }); +} diff --git a/packages/legacy-compat/src/legacy-network-handler/utils.ts b/packages/legacy-compat/src/legacy-network-handler/utils.ts new file mode 100644 index 00000000000..69c64fd7e71 --- /dev/null +++ b/packages/legacy-compat/src/legacy-network-handler/utils.ts @@ -0,0 +1,57 @@ +import { deprecate } from '@ember/debug'; + +import type Store from '@ember-data/store'; +import { DEPRECATE_RSVP_PROMISE } from '@warp-drive/build-config/deprecations'; + +function isObject(value: unknown): value is T { + return value !== null && typeof value === 'object'; +} + +export function _objectIsAlive(object: unknown): boolean { + return isObject<{ isDestroyed: boolean; isDestroying: boolean }>(object) + ? !(object.isDestroyed || object.isDestroying) + : false; +} + +export function guardDestroyedStore(promise: Promise, store: Store): Promise { + return promise.then((_v) => { + if (!_objectIsAlive(store)) { + if (DEPRECATE_RSVP_PROMISE) { + deprecate( + `A Promise did not resolve by the time the store was destroyed. This will error in a future release.`, + false, + { + id: 'ember-data:rsvp-unresolved-async', + until: '5.0', + for: '@ember-data/store', + since: { + available: '4.5', + enabled: '4.5', + }, + } + ); + } + } + + return _v; + }); +} + +export function _bind boolean>(fn: T, ...args: unknown[]) { + return function () { + // eslint-disable-next-line prefer-spread + return fn.apply(undefined, args); + }; +} + +export function _guard(promise: Promise, test: () => boolean): Promise { + const guarded = promise.finally(() => { + if (!test()) { + // @ts-expect-error this is a private RSVPPromise API that won't always be there + // eslint-disable-next-line @typescript-eslint/no-unused-expressions, @typescript-eslint/no-unsafe-member-access + guarded._subscribers ? (guarded._subscribers.length = 0) : null; + } + }); + + return guarded; +} diff --git a/packages/model/eslint.config.mjs b/packages/model/eslint.config.mjs index 9ca51cdf5b4..c42683457ca 100644 --- a/packages/model/eslint.config.mjs +++ b/packages/model/eslint.config.mjs @@ -2,6 +2,7 @@ import { globalIgnores } from '@warp-drive/internal-config/eslint/ignore.js'; import * as node from '@warp-drive/internal-config/eslint/node.js'; import * as typescript from '@warp-drive/internal-config/eslint/typescript.js'; +import { externals } from './vite.config.mjs'; /** @type {import('eslint').Linter.FlatConfig[]} */ export default [ @@ -11,17 +12,7 @@ export default [ // browser (js/ts) ================ typescript.browser({ srcDirs: ['src'], - allowedImports: [ - '@ember/array', - '@ember/array/proxy', - '@ember/debug', - '@ember/object/internals', - '@ember/object/proxy', - '@ember/object/computed', - '@ember/object', - '@ember/application', - '@ember/object/promise-proxy-mixin', - ], + allowedImports: externals, }), // node (module) ================ diff --git a/packages/model/package.json b/packages/model/package.json index d08bf043f8d..71c34256d9c 100644 --- a/packages/model/package.json +++ b/packages/model/package.json @@ -1,6 +1,6 @@ { "name": "@ember-data/model", - "version": "5.4.0-alpha.136", + "version": "4.13.0-alpha.3", "description": "A basic Ember implementation of a resource presentation layer for use with @ember-data/store", "keywords": [ "ember-addon" @@ -96,7 +96,7 @@ }, "dependencies": { "@ember/edition-utils": "^1.2.0", - "@embroider/macros": "^1.16.10", + "@embroider/macros": "^1.16.6", "ember-cli-string-utils": "^1.1.0", "ember-cli-test-info": "^1.0.0", "inflection": "~3.0.0", @@ -118,7 +118,7 @@ "@glimmer/component": "^1.1.2", "@warp-drive/core-types": "workspace:*", "@warp-drive/internal-config": "workspace:*", - "decorator-transforms": "^2.3.0", + "decorator-transforms": "^2.2.2", "ember-source": "~5.12.0", "expect-type": "^0.20.0", "pnpm-sync-dependencies-meta-injected": "0.0.14", diff --git a/packages/model/src/-private/belongs-to.ts b/packages/model/src/-private/belongs-to.ts index 2f3d5890956..95bbacb4889 100644 --- a/packages/model/src/-private/belongs-to.ts +++ b/packages/model/src/-private/belongs-to.ts @@ -1,6 +1,14 @@ -import { warn } from '@ember/debug'; +import { deprecate, warn } from '@ember/debug'; import { computed } from '@ember/object'; +import { dasherize, singularize } from '@ember-data/request-utils/string'; +import { + DEPRECATE_NON_STRICT_TYPES, + DEPRECATE_RELATIONSHIPS_WITHOUT_ASYNC, + DEPRECATE_RELATIONSHIPS_WITHOUT_INVERSE, + DEPRECATE_RELATIONSHIPS_WITHOUT_TYPE, + DISABLE_6X_DEPRECATIONS, +} from '@warp-drive/build-config/deprecations'; import { DEBUG } from '@warp-drive/build-config/env'; import { assert } from '@warp-drive/build-config/macros'; import type { TypeFromInstance } from '@warp-drive/core-types/record'; @@ -8,7 +16,7 @@ import { RecordStore } from '@warp-drive/core-types/symbols'; import { lookupLegacySupport } from './legacy-relationships-support'; import type { MinimalLegacyRecord } from './model-methods'; -import { isElementDescriptor, normalizeModelName } from './util'; +import { isElementDescriptor } from './util'; /** @module @ember-data/model */ @@ -20,7 +28,6 @@ export type RelationshipOptions = { inverse: null | (IsUnknown extends true ? string : keyof NoNull & string); polymorphic?: boolean; as?: string; - linksMode?: true; resetOnRemoteUpdate?: boolean; }; @@ -34,22 +41,116 @@ export type NoNull = Exclude; // eslint-disable-next-line @typescript-eslint/no-unused-vars export type RelationshipDecorator = (target: This, key: string, desc?: PropertyDescriptor) => void; // BelongsToDecoratorObject; +function normalizeType(type: string) { + if (DEPRECATE_RELATIONSHIPS_WITHOUT_TYPE) { + if (!type) { + return; + } + } + + if (DEPRECATE_NON_STRICT_TYPES) { + const result = singularize(dasherize(type)); + + deprecate( + `The resource type '${type}' is not normalized. Update your application code to use '${result}' instead of '${type}'.`, + /* inline-macro-config */ DISABLE_6X_DEPRECATIONS ? true : result === type, + { + id: 'ember-data:deprecate-non-strict-types', + until: '6.0', + for: 'ember-data', + since: { + available: '4.13', + enabled: '5.3', + }, + } + ); + + return result; + } + + return type; +} + function _belongsTo( type: string, options: RelationshipOptions ): RelationshipDecorator { - assert( - `Expected options.async from @belongsTo('${type}', options) to be a boolean`, - options && typeof options.async === 'boolean' - ); - assert( - `Expected options.inverse from @belongsTo('${type}', options) to be either null or the string type of the related resource.`, - options.inverse === null || (typeof options.inverse === 'string' && options.inverse.length > 0) - ); + let opts = options; + let rawType: string | undefined = type; + if (DEPRECATE_RELATIONSHIPS_WITHOUT_TYPE) { + if (typeof type !== 'string' || !type.length) { + deprecate('belongsTo() must specify the string type of the related resource as the first parameter', false, { + id: 'ember-data:deprecate-non-strict-relationships', + for: 'ember-data', + until: '5.0', + since: { enabled: '4.7', available: '4.7' }, + }); + + if (typeof type === 'object') { + opts = type; + rawType = undefined; + } else { + opts = options; + rawType = type; + } + + assert( + 'The first argument to belongsTo must be a string representing a model type key, not an instance of ' + + typeof rawType + + ". E.g., to define a relation to the Person model, use belongsTo('person')", + typeof rawType === 'string' || typeof rawType === 'undefined' + ); + } + } + + if (DEPRECATE_RELATIONSHIPS_WITHOUT_ASYNC) { + if (!opts || typeof opts.async !== 'boolean') { + opts = opts || {}; + if (!('async' in opts)) { + // @ts-expect-error the inbound signature is strict to convince the user to use the non-deprecated signature + opts.async = true as Async; + } + deprecate('belongsTo(, ) must specify options.async as either `true` or `false`.', false, { + id: 'ember-data:deprecate-non-strict-relationships', + for: 'ember-data', + until: '5.0', + since: { enabled: '4.7', available: '4.7' }, + }); + } else { + assert(`Expected belongsTo options.async to be a boolean`, opts && typeof opts.async === 'boolean'); + } + } else { + assert(`Expected belongsTo options.async to be a boolean`, opts && typeof opts.async === 'boolean'); + } + + if (DEPRECATE_RELATIONSHIPS_WITHOUT_INVERSE) { + if (opts.inverse !== null && (typeof opts.inverse !== 'string' || opts.inverse.length === 0)) { + deprecate( + 'belongsTo(, ) must specify options.inverse as either `null` or the name of the field on the related resource type.', + false, + { + id: 'ember-data:deprecate-non-strict-relationships', + for: 'ember-data', + until: '5.0', + since: { enabled: '4.7', available: '4.7' }, + } + ); + } else { + assert( + `Expected belongsTo options.inverse to be either null or the string type of the related resource.`, + opts.inverse === null || (typeof opts.inverse === 'string' && opts.inverse.length > 0) + ); + } + } else { + assert( + `Expected belongsTo options.inverse to be either null or the string type of the related resource.`, + opts.inverse === null || (typeof opts.inverse === 'string' && opts.inverse.length > 0) + ); + } const meta = { - type: normalizeModelName(type), - options: options, + type: normalizeType(type), + options: opts, kind: 'belongsTo', name: '', }; @@ -286,11 +387,17 @@ export function belongsTo( type?: TypeFromInstance>, options?: RelationshipOptions ): RelationshipDecorator { - if (DEBUG) { + if (!DEPRECATE_RELATIONSHIPS_WITHOUT_TYPE) { assert( - `belongsTo must be invoked with a type and options. Did you mean \`@belongsTo(${type}, { async: false, inverse: null })\`?`, + `belongsTo must be invoked with a type and options. Did you mean \`@belongsTo(, { async: false, inverse: null })\`?`, !isElementDescriptor(arguments as unknown as unknown[]) ); + return _belongsTo(type!, options!); + } else { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return isElementDescriptor(arguments as unknown as any[]) + ? // @ts-expect-error the inbound signature is strict to convince the user to use the non-deprecated signature + (_belongsTo()(...arguments) as RelationshipDecorator) + : _belongsTo(type!, options!); } - return _belongsTo(type!, options!); } diff --git a/packages/model/src/-private/debug/assert-polymorphic-type.ts b/packages/model/src/-private/debug/assert-polymorphic-type.ts index 49c451e0f99..bf9f57b79ee 100644 --- a/packages/model/src/-private/debug/assert-polymorphic-type.ts +++ b/packages/model/src/-private/debug/assert-polymorphic-type.ts @@ -1,8 +1,31 @@ +import type Mixin from '@ember/object/mixin'; + import type { UpgradedMeta } from '@ember-data/graph/-private'; import type Store from '@ember-data/store'; +import type { ModelSchema } from '@ember-data/store/types'; +import { DEPRECATE_NON_EXPLICIT_POLYMORPHISM } from '@warp-drive/build-config/deprecations'; import { DEBUG } from '@warp-drive/build-config/env'; import { assert } from '@warp-drive/build-config/macros'; import type { StableRecordIdentifier } from '@warp-drive/core-types'; +import type { FieldSchema, LegacyRelationshipSchema } from '@warp-drive/core-types/schema/fields'; + +import { Model } from '../model'; + +// A pile of soft-lies to deal with mixin APIs +type ModelWithMixinApis = Model & { + __isMixin?: boolean; + __mixin: Mixin; + PrototypeMixin: Mixin; + detect: (mixin: Model | Mixin | ModelWithMixinApis) => boolean; + prototype: Model; + [Symbol.hasInstance](model: Model): true; +}; + +function assertModelSchemaIsModel( + schema: ModelSchema | Model | ModelWithMixinApis +): asserts schema is ModelWithMixinApis { + assert(`Expected Schema to be an instance of Model`, schema instanceof Model); +} /* Assert that `addedRecord` has a valid type so it can be added to the @@ -24,6 +47,26 @@ let assertPolymorphicType: ( ) => void; if (DEBUG) { + const checkPolymorphic = function checkPolymorphic(modelClass: ModelSchema, addedModelClass: ModelSchema) { + assertModelSchemaIsModel(modelClass); + assertModelSchemaIsModel(addedModelClass); + + if (modelClass.__isMixin) { + return ( + modelClass.__mixin.detect(addedModelClass.PrototypeMixin) || + // handle native class extension e.g. `class Post extends Model.extend(Commentable) {}` + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + modelClass.__mixin.detect(Object.getPrototypeOf(addedModelClass).PrototypeMixin) + ); + } + + return addedModelClass.prototype instanceof modelClass || modelClass.detect(addedModelClass); + }; + + const isRelationshipField = function isRelationshipField(meta: FieldSchema): meta is LegacyRelationshipSchema { + return meta.kind === 'hasMany' || meta.kind === 'belongsTo'; + }; + // eslint-disable-next-line @typescript-eslint/no-shadow assertPolymorphicType = function assertPolymorphicType( parentIdentifier: StableRecordIdentifier, @@ -34,16 +77,45 @@ if (DEBUG) { if (parentDefinition.inverseIsImplicit) { return; } + let asserted = false; if (parentDefinition.isPolymorphic) { const meta = store.schema.fields(addedIdentifier)?.get(parentDefinition.inverseKey); assert( - `Expected the schema for the field ${parentDefinition.inverseKey} on ${addedIdentifier.type} to be for a legacy relationship`, - !meta || meta.kind === 'belongsTo' || meta.kind === 'hasMany' - ); - assert( - `The schema for the relationship '${parentDefinition.inverseKey}' on '${addedIdentifier.type}' type does not implement '${parentDefinition.type}' and thus cannot be assigned to the '${parentDefinition.key}' relationship in '${parentIdentifier.type}'. The definition should specify 'as: "${parentDefinition.type}"' in options.`, - meta?.options.as === parentDefinition.type + `Expected to find a relationship field schema for ${parentDefinition.inverseKey} on ${addedIdentifier.type} but none was found`, + meta && isRelationshipField(meta) ); + + if (!DEPRECATE_NON_EXPLICIT_POLYMORPHISM) { + assert( + `The schema for the relationship '${parentDefinition.inverseKey}' on '${addedIdentifier.type}' type does not implement '${parentDefinition.type}' and thus cannot be assigned to the '${parentDefinition.key}' relationship in '${parentIdentifier.type}'. The definition should specify 'as: "${parentDefinition.type}"' in options.`, + meta.options.as === parentDefinition.type + ); + } else if ((meta.options.as?.length ?? 0) > 0) { + asserted = true; + assert( + `The schema for the relationship '${parentDefinition.inverseKey}' on '${addedIdentifier.type}' type does not implement '${parentDefinition.type}' and thus cannot be assigned to the '${parentDefinition.key}' relationship in '${parentIdentifier.type}'. The definition should specify 'as: "${parentDefinition.type}"' in options.`, + meta.options.as === parentDefinition.type + ); + } + + if (DEPRECATE_NON_EXPLICIT_POLYMORPHISM) { + if (!asserted) { + store = (store as unknown as { _store: Store })._store + ? (store as unknown as { _store: Store })._store + : store; // allow usage with storeWrapper + const addedModelName = addedIdentifier.type; + const parentModelName = parentIdentifier.type; + const key = parentDefinition.key; + const relationshipModelName = parentDefinition.type; + const relationshipClass = store.modelFor(relationshipModelName); + const addedClass = store.modelFor(addedModelName); + + const assertionMessage = `The '${addedModelName}' type does not implement '${relationshipModelName}' and thus cannot be assigned to the '${key}' relationship in '${parentModelName}'. Make it a descendant of '${relationshipModelName}' or use a mixin of the same name.`; + const isPolymorphic = checkPolymorphic(relationshipClass, addedClass); + + assert(assertionMessage, isPolymorphic); + } + } } }; } diff --git a/packages/model/src/-private/deprecated-promise-proxy.ts b/packages/model/src/-private/deprecated-promise-proxy.ts new file mode 100644 index 00000000000..0adb72058ae --- /dev/null +++ b/packages/model/src/-private/deprecated-promise-proxy.ts @@ -0,0 +1,73 @@ +import { deprecate } from '@ember/debug'; +import { get } from '@ember/object'; + +import { DEBUG } from '@warp-drive/build-config/env'; + +import { PromiseObject } from './promise-proxy-base'; + +function promiseObject(promise: Promise): PromiseObject { + return PromiseObject.create({ promise }) as PromiseObject; +} + +// constructor is accessed in some internals but not including it in the copyright for the deprecation +const ALLOWABLE_METHODS = ['constructor', 'then', 'catch', 'finally']; +const ALLOWABLE_PROPS = ['__ec_yieldable__', '__ec_cancel__']; +const PROXIED_OBJECT_PROPS = ['content', 'isPending', 'isSettled', 'isRejected', 'isFulfilled', 'promise', 'reason']; + +const ProxySymbolString = String(Symbol.for('PROXY_CONTENT')); + +export function deprecatedPromiseObject(promise: Promise): PromiseObject { + const promiseObjectProxy: PromiseObject = promiseObject(promise); + if (!DEBUG) { + return promiseObjectProxy; + } + const handler = { + get(target: object, prop: string, receiver: object): unknown { + if (typeof prop === 'symbol') { + if (String(prop) === ProxySymbolString) { + return; + } + return Reflect.get(target, prop, receiver); + } + + if (prop === 'constructor') { + return target.constructor; + } + + if (ALLOWABLE_PROPS.includes(prop)) { + return target[prop]; + } + + if (!ALLOWABLE_METHODS.includes(prop)) { + deprecate( + `Accessing ${prop} is deprecated. The return type is being changed from PromiseObjectProxy to a Promise. The only available methods to access on this promise are .then, .catch and .finally`, + false, + { + id: 'ember-data:model-save-promise', + until: '5.0', + for: '@ember-data/store', + since: { + available: '4.4', + enabled: '4.4', + }, + } + ); + } else { + return (target[prop] as () => unknown).bind(target); + } + + if (PROXIED_OBJECT_PROPS.includes(prop)) { + return target[prop]; + } + + const value: unknown = get(target, prop); + if (value && typeof value === 'function' && typeof value.bind === 'function') { + return value.bind(receiver); + } + + return undefined; + }, + }; + + return new Proxy(promiseObjectProxy, handler) as PromiseObject; +} diff --git a/packages/model/src/-private/has-many.ts b/packages/model/src/-private/has-many.ts index 9a6fcb286c3..305ae73431c 100644 --- a/packages/model/src/-private/has-many.ts +++ b/packages/model/src/-private/has-many.ts @@ -1,11 +1,17 @@ /** @module @ember-data/model */ -import { deprecate } from '@ember/debug'; +import { deprecate, inspect } from '@ember/debug'; import { computed } from '@ember/object'; import { dasherize, singularize } from '@ember-data/request-utils/string'; -import { DEPRECATE_NON_STRICT_TYPES } from '@warp-drive/build-config/deprecations'; +import { + DEPRECATE_NON_STRICT_TYPES, + DEPRECATE_RELATIONSHIPS_WITHOUT_ASYNC, + DEPRECATE_RELATIONSHIPS_WITHOUT_INVERSE, + DEPRECATE_RELATIONSHIPS_WITHOUT_TYPE, + DISABLE_6X_DEPRECATIONS, +} from '@warp-drive/build-config/deprecations'; import { DEBUG } from '@warp-drive/build-config/env'; import { assert } from '@warp-drive/build-config/macros'; import type { TypeFromInstance } from '@warp-drive/core-types/record'; @@ -17,12 +23,18 @@ import type { MinimalLegacyRecord } from './model-methods'; import { isElementDescriptor } from './util'; function normalizeType(type: string) { + if (DEPRECATE_RELATIONSHIPS_WITHOUT_TYPE) { + if (!type) { + return; + } + } + if (DEPRECATE_NON_STRICT_TYPES) { const result = singularize(dasherize(type)); deprecate( `The resource type '${type}' is not normalized. Update your application code to use '${result}' instead of '${type}'.`, - result === type, + /* inline-macro-config */ DISABLE_6X_DEPRECATIONS ? true : result === type, { id: 'ember-data:deprecate-non-strict-types', until: '6.0', @@ -44,7 +56,66 @@ function _hasMany( type: string, options: RelationshipOptions ): RelationshipDecorator { - assert(`Expected hasMany options.async to be a boolean`, options && typeof options.async === 'boolean'); + if (DEPRECATE_RELATIONSHIPS_WITHOUT_TYPE) { + if (typeof type !== 'string' || !type.length) { + deprecate( + 'hasMany(, ) must specify the string type of the related resource as the first parameter', + false, + { + id: 'ember-data:deprecate-non-strict-relationships', + for: 'ember-data', + until: '5.0', + since: { enabled: '4.7', available: '4.7' }, + } + ); + if (typeof type === 'object') { + options = type; + type = undefined as unknown as string; + } + + assert( + `The first argument to hasMany must be a string representing a model type key, not an instance of ${inspect( + type + )}. E.g., to define a relation to the Comment model, use hasMany('comment')`, + typeof type === 'string' || typeof type === 'undefined' + ); + } + } + + if (DEPRECATE_RELATIONSHIPS_WITHOUT_ASYNC) { + if (!options || typeof options.async !== 'boolean') { + options = options || {}; + if (!('async' in options)) { + // @ts-expect-error the inbound signature is strict to convince the user to use the non-deprecated signature + options.async = true; + } + deprecate('hasMany(, ) must specify options.async as either `true` or `false`.', false, { + id: 'ember-data:deprecate-non-strict-relationships', + for: 'ember-data', + until: '5.0', + since: { enabled: '4.7', available: '4.7' }, + }); + } else { + assert(`Expected hasMany options.async to be a boolean`, options && typeof options.async === 'boolean'); + } + } else { + assert(`Expected hasMany options.async to be a boolean`, options && typeof options.async === 'boolean'); + } + + if (DEPRECATE_RELATIONSHIPS_WITHOUT_INVERSE) { + if (options.inverse !== null && (typeof options.inverse !== 'string' || options.inverse.length === 0)) { + deprecate( + 'hasMany(, ) must specify options.inverse as either `null` or the name of the field on the related resource type.', + false, + { + id: 'ember-data:deprecate-non-strict-relationships', + for: 'ember-data', + until: '5.0', + since: { enabled: '4.7', available: '4.7' }, + } + ); + } + } // Metadata about relationships is stored on the meta of // the relationship. This is used for introspection and @@ -266,11 +337,17 @@ export function hasMany( type?: TypeFromInstance>, options?: RelationshipOptions ): RelationshipDecorator { - if (DEBUG) { + if (!DEPRECATE_RELATIONSHIPS_WITHOUT_TYPE) { assert( - `hasMany must be invoked with a type and options. Did you mean \`@hasMany(${type}, { async: false, inverse: null })\`?`, + `hasMany must be invoked with a type and options. Did you mean \`@hasMany(, { async: false, inverse: null })\`?`, !isElementDescriptor(arguments as unknown as unknown[]) ); + return _hasMany(type!, options!); + } else { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return isElementDescriptor(arguments as unknown as any[]) + ? // @ts-expect-error the inbound signature is strict to convince the user to use the non-deprecated signature + (_hasMany()(...arguments) as RelationshipDecorator) + : _hasMany(type!, options!); } - return _hasMany(type!, options!); } diff --git a/packages/model/src/-private/legacy-relationships-support.ts b/packages/model/src/-private/legacy-relationships-support.ts index a7288a1ae2d..ef6fe4831f4 100644 --- a/packages/model/src/-private/legacy-relationships-support.ts +++ b/packages/model/src/-private/legacy-relationships-support.ts @@ -1,9 +1,10 @@ +import { deprecate } from '@ember/debug'; + import { dependencySatisfies, importSync, macroCondition } from '@embroider/macros'; import type { CollectionEdge, Graph, GraphEdge, ResourceEdge, UpgradedMeta } from '@ember-data/graph/-private'; import { upgradeStore } from '@ember-data/legacy-compat/-private'; import type Store from '@ember-data/store'; -import type { Document } from '@ember-data/store'; import type { LiveArray } from '@ember-data/store/-private'; import { fastPush, @@ -14,6 +15,7 @@ import { storeFor, } from '@ember-data/store/-private'; import type { BaseFinderOptions } from '@ember-data/store/types'; +import { DEPRECATE_PROMISE_PROXIES } from '@warp-drive/build-config/deprecations'; import { DEBUG } from '@warp-drive/build-config/env'; import { assert } from '@warp-drive/build-config/macros'; import type { StableRecordIdentifier } from '@warp-drive/core-types'; @@ -22,7 +24,6 @@ import type { Cache } from '@warp-drive/core-types/cache'; import type { CollectionRelationship } from '@warp-drive/core-types/cache/relationship'; import type { LocalRelationshipOperation } from '@warp-drive/core-types/graph'; import type { OpaqueRecordInstance, TypeFromInstanceOrString } from '@warp-drive/core-types/record'; -import { EnableHydration } from '@warp-drive/core-types/request'; import type { CollectionResourceRelationship, InnerRelationshipDocument, @@ -471,22 +472,12 @@ export class LegacySupport { assert(`Expected collection to be an array`, !identifiers || Array.isArray(identifiers)); assert(`Expected stable identifiers`, !identifiers || identifiers.every(isStableIdentifier)); - const req = field.options.linksMode - ? { - url: getRelatedLink(resource), - op: 'findHasMany', - method: 'GET' as const, - records: identifiers || [], - data: request, - [EnableHydration]: false, - } - : { - op: 'findHasMany', - records: identifiers || [], - data: request, - cacheOptions: { [Symbol.for('wd:skip-cache')]: true }, - }; - return this.store.request(req) as unknown as Promise; + return this.store.request({ + op: 'findHasMany', + records: identifiers || [], + data: request, + cacheOptions: { [Symbol.for('wd:skip-cache')]: true }, + }) as unknown as Promise; } const preferLocalCache = hasReceivedData && !isEmpty; @@ -563,28 +554,14 @@ export class LegacySupport { // fetch via link if (shouldFindViaLink) { - const req = field.options.linksMode - ? { - url: getRelatedLink(resource), - op: 'findBelongsTo', - method: 'GET' as const, - records: identifier ? [identifier] : [], - data: request, - [EnableHydration]: false, - } - : { - op: 'findBelongsTo', - records: identifier ? [identifier] : [], - data: request, - cacheOptions: { [Symbol.for('wd:skip-cache')]: true }, - }; - const future = this.store.request(req); + const future = this.store.request({ + op: 'findBelongsTo', + records: identifier ? [identifier] : [], + data: request, + cacheOptions: { [Symbol.for('wd:skip-cache')]: true }, + }); this._pending[key] = future - .then((doc) => - field.options.linksMode - ? (doc.content as unknown as Document).data! - : doc.content - ) + .then((doc) => doc.content) .finally(() => { this._pending[key] = undefined; }); @@ -659,13 +636,6 @@ export class LegacySupport { } } -function getRelatedLink(resource: SingleResourceRelationship | CollectionResourceRelationship): string { - const related = resource.links?.related; - assert(`Expected a related link`, related); - - return typeof related === 'object' ? related.href : related; -} - function handleCompletedRelationshipRequest( recordExt: LegacySupport, key: string, @@ -750,6 +720,30 @@ function extractIdentifierFromRecord(record: PromiseProxyRecord | OpaqueRecordIn return null; } + if (DEPRECATE_PROMISE_PROXIES) { + if (isPromiseRecord(record)) { + const content = record.content; + assert( + 'You passed in a promise that did not originate from an EmberData relationship. You can only pass promises that come from a belongsTo or hasMany relationship to the get call.', + content !== undefined + ); + deprecate( + `You passed in a PromiseProxy to a Relationship API that now expects a resolved value. await the value before setting it.`, + false, + { + id: 'ember-data:deprecate-promise-proxies', + until: '5.0', + since: { + enabled: '4.7', + available: '4.7', + }, + for: 'ember-data', + } + ); + return content ? recordIdentifierFor(content) : null; + } + } + return recordIdentifierFor(record); } @@ -791,3 +785,7 @@ export function areAllInverseRecordsLoaded(store: Store, resource: InnerRelation function isBelongsTo(relationship: GraphEdge): relationship is ResourceEdge { return relationship.definition.kind === 'belongsTo'; } + +function isPromiseRecord(record: PromiseProxyRecord | OpaqueRecordInstance): record is PromiseProxyRecord { + return typeof record === 'object' && !!record && 'then' in record; +} diff --git a/packages/model/src/-private/many-array.ts b/packages/model/src/-private/many-array.ts index c514e4860ce..01c73832264 100644 --- a/packages/model/src/-private/many-array.ts +++ b/packages/model/src/-private/many-array.ts @@ -17,7 +17,11 @@ import { import type { BaseFinderOptions, ModelSchema } from '@ember-data/store/types'; import type { Signal } from '@ember-data/tracking/-private'; import { addToTransaction } from '@ember-data/tracking/-private'; -import { DEPRECATE_MANY_ARRAY_DUPLICATES } from '@warp-drive/build-config/deprecations'; +import { + DEPRECATE_MANY_ARRAY_DUPLICATES, + DEPRECATE_PROMISE_PROXIES, + DISABLE_6X_DEPRECATIONS, +} from '@warp-drive/build-config/deprecations'; import { assert } from '@warp-drive/build-config/macros'; import type { StableRecordIdentifier } from '@warp-drive/core-types'; import type { Cache } from '@warp-drive/core-types/cache'; @@ -317,11 +321,12 @@ export class RelatedCollection extends LiveArray { // dedupe const current = new Set(adds); const unique = Array.from(current); + const uniqueIdentifiers = Array.from(new Set(newValues)); const newArgs = ([start, deleteCount] as unknown[]).concat(unique); const result = Reflect.apply(target[prop], receiver, newArgs) as OpaqueRecordInstance[]; - mutateReplaceRelatedRecords(this, extractIdentifiersFromRecords(unique), _SIGNAL); + mutateReplaceRelatedRecords(this, uniqueIdentifiers, _SIGNAL); return result; } @@ -485,10 +490,39 @@ function extractIdentifiersFromRecords(records: OpaqueRecordInstance[]): StableR } function extractIdentifierFromRecord(recordOrPromiseRecord: PromiseProxyRecord | OpaqueRecordInstance) { + if (DEPRECATE_PROMISE_PROXIES) { + if (isPromiseRecord(recordOrPromiseRecord)) { + const content = recordOrPromiseRecord.content; + assert( + 'You passed in a promise that did not originate from an EmberData relationship. You can only pass promises that come from a belongsTo relationship.', + content !== undefined && content !== null + ); + deprecate( + `You passed in a PromiseProxy to a Relationship API that now expects a resolved value. await the value before setting it.`, + false, + { + id: 'ember-data:deprecate-promise-proxies', + until: '5.0', + since: { + enabled: '4.7', + available: '4.7', + }, + for: 'ember-data', + } + ); + assertRecordPassedToHasMany(content); + return recordIdentifierFor(content); + } + } + assertRecordPassedToHasMany(recordOrPromiseRecord); return recordIdentifierFor(recordOrPromiseRecord); } +function isPromiseRecord(record: PromiseProxyRecord | OpaqueRecordInstance): record is PromiseProxyRecord { + return Boolean(typeof record === 'object' && record && 'then' in record); +} + function assertNoDuplicates( collection: RelatedCollection, target: StableRecordIdentifier[], @@ -511,7 +545,7 @@ function assertNoDuplicates( .map((r) => (isStableIdentifier(r) ? r.lid : recordIdentifierFor(r).lid)) .sort((a, b) => a.localeCompare(b)) .join('\n\t- ')}`, - false, + /* inline-macro-config */ DISABLE_6X_DEPRECATIONS, { id: 'ember-data:deprecate-many-array-duplicates', for: 'ember-data', diff --git a/packages/model/src/-private/model-methods.ts b/packages/model/src/-private/model-methods.ts index 8af6b3cba97..ed8b690f087 100644 --- a/packages/model/src/-private/model-methods.ts +++ b/packages/model/src/-private/model-methods.ts @@ -5,10 +5,12 @@ import { upgradeStore } from '@ember-data/legacy-compat/-private'; import type Store from '@ember-data/store'; import { recordIdentifierFor } from '@ember-data/store'; import { peekCache } from '@ember-data/store/-private'; +import { DEPRECATE_SAVE_PROMISE_ACCESS } from '@warp-drive/build-config/deprecations'; import { assert } from '@warp-drive/build-config/macros'; import type { ChangedAttributesHash } from '@warp-drive/core-types/cache'; import { RecordStore } from '@warp-drive/core-types/symbols'; +import { deprecatedPromiseObject } from './deprecated-promise-proxy'; import type { Errors } from './errors'; import { lookupLegacySupport } from './legacy-relationships-support'; import type RecordState from './record-state'; @@ -88,6 +90,10 @@ export function reload(this: T, options: Record(this: T, options?: Record(this: T, options?: return Promise.resolve(this); } return this.save(options).then((_) => { + // run(() => { this.unloadRecord(); + // }); return this; }); } diff --git a/packages/model/src/-private/model.ts b/packages/model/src/-private/model.ts index c003b428c6a..ea2eff41537 100644 --- a/packages/model/src/-private/model.ts +++ b/packages/model/src/-private/model.ts @@ -2,6 +2,7 @@ @module @ember-data/model */ +import { deprecate, warn } from '@ember/debug'; import EmberObject from '@ember/object'; import type { Snapshot } from '@ember-data/legacy-compat/-private'; @@ -11,6 +12,12 @@ import { recordIdentifierFor, storeFor } from '@ember-data/store'; import { coerceId } from '@ember-data/store/-private'; import { compat } from '@ember-data/tracking'; import { defineSignal } from '@ember-data/tracking/-private'; +import { + DEPRECATE_EARLY_STATIC, + DEPRECATE_MODEL_REOPEN, + DEPRECATE_NON_EXPLICIT_POLYMORPHISM, + DEPRECATE_RELATIONSHIPS_WITHOUT_INVERSE, +} from '@warp-drive/build-config/deprecations'; import { DEBUG } from '@warp-drive/build-config/env'; import { assert } from '@warp-drive/build-config/macros'; import type { StableRecordIdentifier } from '@warp-drive/core-types'; @@ -38,6 +45,7 @@ import notifyChanges from './notify-changes'; import RecordState, { notifySignal, tagged } from './record-state'; import type BelongsToReference from './references/belongs-to'; import type HasManyReference from './references/has-many'; +import { relationshipFromMeta } from './relationship-meta'; import type { _MaybeBelongsToFields, isSubClass, @@ -1168,22 +1176,49 @@ class Model extends EmberObject implements MinimalLegacyRecord { @param {store} store an instance of Store @return {Model} the type of the relationship, or undefined */ - static typeForRelationship(name: string, store: Store) { - assert( - `Accessing schema information on Models without looking up the model via the store is disallowed.`, - this.modelName - ); + static typeForRelationship(name: string, store: Store): typeof Model | undefined { + if (DEPRECATE_EARLY_STATIC) { + deprecate( + `Accessing schema information on Models without looking up the model via the store is deprecated. Use store.modelFor (or better Snapshots or the store.getSchemaDefinitionService() apis) instead.`, + Boolean(this.modelName), + { + id: 'ember-data:deprecate-early-static', + for: 'ember-data', + until: '5.0', + since: { available: '4.7', enabled: '4.7' }, + } + ); + } else { + assert( + `Accessing schema information on Models without looking up the model via the store is disallowed.`, + this.modelName + ); + } const relationship = this.relationshipsByName.get(name); + // @ts-expect-error return relationship && store.modelFor(relationship.type); } @computeOnce static get inverseMap(): Record { - assert( - `Accessing schema information on Models without looking up the model via the store is disallowed.`, - this.modelName - ); + if (DEPRECATE_EARLY_STATIC) { + deprecate( + `Accessing schema information on Models without looking up the model via the store is deprecated. Use store.modelFor (or better Snapshots or the store.getSchemaDefinitionService() apis) instead.`, + Boolean(this.modelName), + { + id: 'ember-data:deprecate-early-static', + for: 'ember-data', + until: '5.0', + since: { available: '4.7', enabled: '4.7' }, + } + ); + } else { + assert( + `Accessing schema information on Models without looking up the model via the store is disallowed.`, + this.modelName + ); + } return Object.create(null) as Record; } @@ -1221,10 +1256,23 @@ class Model extends EmberObject implements MinimalLegacyRecord { @return {Object} the inverse relationship, or null */ static inverseFor(name: string, store: Store): LegacyRelationshipSchema | null { - assert( - `Accessing schema information on Models without looking up the model via the store is disallowed.`, - this.modelName - ); + if (DEPRECATE_EARLY_STATIC) { + deprecate( + `Accessing schema information on Models without looking up the model via the store is deprecated. Use store.modelFor (or better Snapshots or the store.getSchemaDefinitionService() apis) instead.`, + Boolean(this.modelName), + { + id: 'ember-data:deprecate-early-static', + for: 'ember-data', + until: '5.0', + since: { available: '4.7', enabled: '4.7' }, + } + ); + } else { + assert( + `Accessing schema information on Models without looking up the model via the store is disallowed.`, + this.modelName + ); + } const inverseMap = this.inverseMap; if (inverseMap[name]) { return inverseMap[name]; @@ -1237,10 +1285,27 @@ class Model extends EmberObject implements MinimalLegacyRecord { //Calculate the inverse, ignoring the cache static _findInverseFor(name: string, store: Store): LegacyRelationshipSchema | null { - assert( - `Accessing schema information on Models without looking up the model via the store is disallowed.`, - this.modelName - ); + if (DEPRECATE_EARLY_STATIC) { + deprecate( + `Accessing schema information on Models without looking up the model via the store is deprecated. Use store.modelFor (or better Snapshots or the store.getSchemaDefinitionService() apis) instead.`, + Boolean(this.modelName), + { + id: 'ember-data:deprecate-early-static', + for: 'ember-data', + until: '5.0', + since: { available: '4.7', enabled: '4.7' }, + } + ); + } else { + assert( + `Accessing schema information on Models without looking up the model via the store is disallowed.`, + this.modelName + ); + } + + if (DEPRECATE_NON_EXPLICIT_POLYMORPHISM) { + return legacyFindInverseFor(this, name, store); + } const relationship = this.relationshipsByName.get(name)!; assert(`No relationship named '${name}' on '${this.modelName}' exists.`, relationship); @@ -1322,10 +1387,23 @@ class Model extends EmberObject implements MinimalLegacyRecord { @computeOnce static get relationships(): Map { - assert( - `Accessing schema information on Models without looking up the model via the store is disallowed.`, - this.modelName - ); + if (DEPRECATE_EARLY_STATIC) { + deprecate( + `Accessing schema information on Models without looking up the model via the store is deprecated. Use store.modelFor (or better Snapshots or the store.getSchemaDefinitionService() apis) instead.`, + Boolean(this.modelName), + { + id: 'ember-data:deprecate-early-static', + for: 'ember-data', + until: '5.0', + since: { available: '4.7', enabled: '4.7' }, + } + ); + } else { + assert( + `Accessing schema information on Models without looking up the model via the store is disallowed.`, + this.modelName + ); + } const map = new Map(); const relationshipsByName = this.relationshipsByName; @@ -1380,10 +1458,23 @@ class Model extends EmberObject implements MinimalLegacyRecord { */ @computeOnce static get relationshipNames() { - assert( - `Accessing schema information on Models without looking up the model via the store is disallowed.`, - this.modelName - ); + if (DEPRECATE_EARLY_STATIC) { + deprecate( + `Accessing schema information on Models without looking up the model via the store is deprecated. Use store.modelFor (or better Snapshots or the store.getSchemaDefinitionService() apis) instead.`, + Boolean(this.modelName), + { + id: 'ember-data:deprecate-early-static', + for: 'ember-data', + until: '5.0', + since: { available: '4.7', enabled: '4.7' }, + } + ); + } else { + assert( + `Accessing schema information on Models without looking up the model via the store is disallowed.`, + this.modelName + ); + } const names: { hasMany: string[]; belongsTo: string[] } = { hasMany: [], belongsTo: [], @@ -1433,10 +1524,23 @@ class Model extends EmberObject implements MinimalLegacyRecord { */ @computeOnce static get relatedTypes(): string[] { - assert( - `Accessing schema information on Models without looking up the model via the store is disallowed.`, - this.modelName - ); + if (DEPRECATE_EARLY_STATIC) { + deprecate( + `Accessing schema information on Models without looking up the model via the store is deprecated. Use store.modelFor (or better Snapshots or the store.getSchemaDefinitionService() apis) instead.`, + Boolean(this.modelName), + { + id: 'ember-data:deprecate-early-static', + for: 'ember-data', + until: '5.0', + since: { available: '4.7', enabled: '4.7' }, + } + ); + } else { + assert( + `Accessing schema information on Models without looking up the model via the store is disallowed.`, + this.modelName + ); + } const types: string[] = []; @@ -1496,10 +1600,23 @@ class Model extends EmberObject implements MinimalLegacyRecord { */ @computeOnce static get relationshipsByName(): Map { - assert( - `Accessing schema information on Models without looking up the model via the store is disallowed.`, - this.modelName - ); + if (DEPRECATE_EARLY_STATIC) { + deprecate( + `Accessing schema information on Models without looking up the model via the store is deprecated. Use store.modelFor (or better Snapshots or the store.getSchemaDefinitionService() apis) instead.`, + Boolean(this.modelName), + { + id: 'ember-data:deprecate-early-static', + for: 'ember-data', + until: '5.0', + since: { available: '4.7', enabled: '4.7' }, + } + ); + } else { + assert( + `Accessing schema information on Models without looking up the model via the store is disallowed.`, + this.modelName + ); + } const map = new Map(); const rels = this.relationshipsObject; const relationships = Object.keys(rels); @@ -1516,10 +1633,23 @@ class Model extends EmberObject implements MinimalLegacyRecord { @computeOnce static get relationshipsObject(): Record { - assert( - `Accessing schema information on Models without looking up the model via the store is disallowed.`, - this.modelName - ); + if (DEPRECATE_EARLY_STATIC) { + deprecate( + `Accessing schema information on Models without looking up the model via the store is deprecated. Use store.modelFor (or better Snapshots or the store.getSchemaDefinitionService() apis) instead.`, + Boolean(this.modelName), + { + id: 'ember-data:deprecate-early-static', + for: 'ember-data', + until: '5.0', + since: { available: '4.7', enabled: '4.7' }, + } + ); + } else { + assert( + `Accessing schema information on Models without looking up the model via the store is disallowed.`, + this.modelName + ); + } const relationships = Object.create(null) as Record; const modelName = this.modelName; @@ -1530,7 +1660,10 @@ class Model extends EmberObject implements MinimalLegacyRecord { // TODO deprecate key being here (meta as unknown as { key: string }).key = name; meta.name = name; - relationships[name] = meta; + const parentModelName = meta.options?.as ?? modelName; + relationships[name] = DEPRECATE_RELATIONSHIPS_WITHOUT_INVERSE + ? relationshipFromMeta(meta, parentModelName) + : meta; assert(`Expected options in meta`, meta.options && typeof meta.options === 'object'); assert( @@ -1584,10 +1717,23 @@ class Model extends EmberObject implements MinimalLegacyRecord { */ @computeOnce static get fields(): Map { - assert( - `Accessing schema information on Models without looking up the model via the store is disallowed.`, - this.modelName - ); + if (DEPRECATE_EARLY_STATIC) { + deprecate( + `Accessing schema information on Models without looking up the model via the store is deprecated. Use store.modelFor (or better Snapshots or the store.getSchemaDefinitionService() apis) instead.`, + Boolean(this.modelName), + { + id: 'ember-data:deprecate-early-static', + for: 'ember-data', + until: '5.0', + since: { available: '4.7', enabled: '4.7' }, + } + ); + } else { + assert( + `Accessing schema information on Models without looking up the model via the store is disallowed.`, + this.modelName + ); + } const map = new Map(); this.eachComputedProperty((name, meta) => { @@ -1620,10 +1766,23 @@ class Model extends EmberObject implements MinimalLegacyRecord { ) => void, binding?: T ): void { - assert( - `Accessing schema information on Models without looking up the model via the store is disallowed.`, - this.modelName - ); + if (DEPRECATE_EARLY_STATIC) { + deprecate( + `Accessing schema information on Models without looking up the model via the store is deprecated. Use store.modelFor (or better Snapshots or the store.getSchemaDefinitionService() apis) instead.`, + Boolean(this.modelName), + { + id: 'ember-data:deprecate-early-static', + for: 'ember-data', + until: '5.0', + since: { available: '4.7', enabled: '4.7' }, + } + ); + } else { + assert( + `Accessing schema information on Models without looking up the model via the store is disallowed.`, + this.modelName + ); + } this.relationshipsByName.forEach((relationship, name) => { callback.call(binding, name as MaybeRelationshipFields, relationship); @@ -1643,10 +1802,23 @@ class Model extends EmberObject implements MinimalLegacyRecord { @param {any} binding the value to which the callback's `this` should be bound */ static eachRelatedType(callback: (this: T | undefined, type: string) => void, binding?: T) { - assert( - `Accessing schema information on Models without looking up the model via the store is disallowed.`, - this.modelName - ); + if (DEPRECATE_EARLY_STATIC) { + deprecate( + `Accessing schema information on Models without looking up the model via the store is deprecated. Use store.modelFor (or better Snapshots or the store.getSchemaDefinitionService() apis) instead.`, + Boolean(this.modelName), + { + id: 'ember-data:deprecate-early-static', + for: 'ember-data', + until: '5.0', + since: { available: '4.7', enabled: '4.7' }, + } + ); + } else { + assert( + `Accessing schema information on Models without looking up the model via the store is disallowed.`, + this.modelName + ); + } const relationshipTypes = this.relatedTypes; @@ -1666,10 +1838,23 @@ class Model extends EmberObject implements MinimalLegacyRecord { knownSide: LegacyRelationshipSchema, store: Store ): 'oneToOne' | 'oneToMany' | 'manyToOne' | 'manyToMany' | 'oneToNone' | 'manyToNone' { - assert( - `Accessing schema information on Models without looking up the model via the store is disallowed.`, - this.modelName - ); + if (DEPRECATE_EARLY_STATIC) { + deprecate( + `Accessing schema information on Models without looking up the model via the store is deprecated. Use store.modelFor (or better Snapshots or the store.getSchemaDefinitionService() apis) instead.`, + Boolean(this.modelName), + { + id: 'ember-data:deprecate-early-static', + for: 'ember-data', + until: '5.0', + since: { available: '4.7', enabled: '4.7' }, + } + ); + } else { + assert( + `Accessing schema information on Models without looking up the model via the store is disallowed.`, + this.modelName + ); + } const knownKey = knownSide.name; const knownKind = knownSide.kind; @@ -1730,10 +1915,23 @@ class Model extends EmberObject implements MinimalLegacyRecord { */ @computeOnce static get attributes(): Map { - assert( - `Accessing schema information on Models without looking up the model via the store is disallowed.`, - this.modelName - ); + if (DEPRECATE_EARLY_STATIC) { + deprecate( + `Accessing schema information on Models without looking up the model via the store is deprecated. Use store.modelFor (or better Snapshots or the store.getSchemaDefinitionService() apis) instead.`, + Boolean(this.modelName), + { + id: 'ember-data:deprecate-early-static', + for: 'ember-data', + until: '5.0', + since: { available: '4.7', enabled: '4.7' }, + } + ); + } else { + assert( + `Accessing schema information on Models without looking up the model via the store is disallowed.`, + this.modelName + ); + } const map = new Map(); @@ -1795,10 +1993,23 @@ class Model extends EmberObject implements MinimalLegacyRecord { */ @computeOnce static get transformedAttributes() { - assert( - `Accessing schema information on Models without looking up the model via the store is disallowed.`, - this.modelName - ); + if (DEPRECATE_EARLY_STATIC) { + deprecate( + `Accessing schema information on Models without looking up the model via the store is deprecated. Use store.modelFor (or better Snapshots or the store.getSchemaDefinitionService() apis) instead.`, + Boolean(this.modelName), + { + id: 'ember-data:deprecate-early-static', + for: 'ember-data', + until: '5.0', + since: { available: '4.7', enabled: '4.7' }, + } + ); + } else { + assert( + `Accessing schema information on Models without looking up the model via the store is disallowed.`, + this.modelName + ); + } const map = new Map(); @@ -1859,10 +2070,23 @@ class Model extends EmberObject implements MinimalLegacyRecord { callback: (this: T | undefined, key: MaybeAttrFields, attribute: LegacyAttributeField) => void, binding?: T ): void { - assert( - `Accessing schema information on Models without looking up the model via the store is disallowed.`, - this.modelName - ); + if (DEPRECATE_EARLY_STATIC) { + deprecate( + `Accessing schema information on Models without looking up the model via the store is deprecated. Use store.modelFor (or better Snapshots or the store.getSchemaDefinitionService() apis) instead.`, + Boolean(this.modelName), + { + id: 'ember-data:deprecate-early-static', + for: 'ember-data', + until: '5.0', + since: { available: '4.7', enabled: '4.7' }, + } + ); + } else { + assert( + `Accessing schema information on Models without looking up the model via the store is disallowed.`, + this.modelName + ); + } this.attributes.forEach((meta, name) => { callback.call(binding, name as MaybeAttrFields, meta); @@ -1918,10 +2142,23 @@ class Model extends EmberObject implements MinimalLegacyRecord { callback: (this: T | undefined, key: Exclude, type: string) => void, binding?: T ): void { - assert( - `Accessing schema information on Models without looking up the model via the store is disallowed.`, - this.modelName - ); + if (DEPRECATE_EARLY_STATIC) { + deprecate( + `Accessing schema information on Models without looking up the model via the store is deprecated. Use store.modelFor (or better Snapshots or the store.getSchemaDefinitionService() apis) instead.`, + Boolean(this.modelName), + { + id: 'ember-data:deprecate-early-static', + for: 'ember-data', + until: '5.0', + since: { available: '4.7', enabled: '4.7' }, + } + ); + } else { + assert( + `Accessing schema information on Models without looking up the model via the store is disallowed.`, + this.modelName + ); + } this.transformedAttributes.forEach((type: string, name) => { callback.call(binding, name as Exclude, type); @@ -1936,10 +2173,23 @@ class Model extends EmberObject implements MinimalLegacyRecord { @static */ static toString() { - assert( - `Accessing schema information on Models without looking up the model via the store is disallowed.`, - this.modelName - ); + if (DEPRECATE_EARLY_STATIC) { + deprecate( + `Accessing schema information on Models without looking up the model via the store is deprecated. Use store.modelFor (or better Snapshots or the store.getSchemaDefinitionService() apis) instead.`, + Boolean(this.modelName), + { + id: 'ember-data:deprecate-early-static', + for: 'ember-data', + until: '5.0', + since: { available: '4.7', enabled: '4.7' }, + } + ); + } else { + assert( + `Accessing schema information on Models without looking up the model via the store is disallowed.`, + this.modelName + ); + } return `model:${this.modelName}`; } @@ -2016,8 +2266,40 @@ if (DEBUG) { } }; - delete (Model as unknown as { reopen: unknown }).reopen; - delete (Model as unknown as { reopenClass: unknown }).reopenClass; + if (DEPRECATE_MODEL_REOPEN) { + // eslint-disable-next-line @typescript-eslint/unbound-method + const originalReopen = Model.reopen; + const originalReopenClass = Model.reopenClass; + + // @ts-expect-error Intentional override + Model.reopen = function deprecatedReopen() { + deprecate(`Model.reopen is deprecated. Use Foo extends Model to extend your class instead.`, false, { + id: 'ember-data:deprecate-model-reopen', + for: 'ember-data', + until: '5.0', + since: { available: '4.7', enabled: '4.7' }, + }); + return originalReopen.call(this, ...arguments); + }; + + // @ts-expect-error Intentional override + Model.reopenClass = function deprecatedReopenClass() { + deprecate( + `Model.reopenClass is deprecated. Use Foo extends Model to add static methods and properties to your class instead.`, + false, + { + id: 'ember-data:deprecate-model-reopenclass', + for: 'ember-data', + until: '5.0', + since: { available: '4.7', enabled: '4.7' }, + } + ); + return originalReopenClass.call(this, ...arguments); + }; + } else { + delete (Model as unknown as { reopen: unknown }).reopen; + delete (Model as unknown as { reopenClass: unknown }).reopenClass; + } } export { Model }; @@ -2030,3 +2312,218 @@ function isRelationshipSchema(meta: unknown): meta is LegacyRelationshipSchema { function isAttributeSchema(meta: unknown): meta is LegacyAttributeField { return typeof meta === 'object' && meta !== null && 'kind' in meta && meta.kind === 'attribute'; } + +function findPossibleInverses( + Klass: typeof Model, + inverseType: typeof Model, + name: string, + relationshipsSoFar?: LegacyRelationshipSchema[] +) { + const possibleRelationships = relationshipsSoFar || []; + + const relationshipMap = inverseType.relationships; + if (!relationshipMap) { + return possibleRelationships; + } + + const relationshipsForType = relationshipMap.get(Klass.modelName); + const relationships = Array.isArray(relationshipsForType) + ? relationshipsForType.filter((relationship) => { + const optionsForRelationship = relationship.options; + + if (!optionsForRelationship.inverse && optionsForRelationship.inverse !== null) { + return true; + } + + return name === optionsForRelationship.inverse; + }) + : null; + + if (relationships) { + // eslint-disable-next-line prefer-spread + possibleRelationships.push.apply(possibleRelationships, relationships); + } + + //Recurse to support polymorphism + if (Klass.superclass) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + findPossibleInverses(Klass.superclass, inverseType, name, possibleRelationships); + } + + return possibleRelationships; +} + +function legacyFindInverseFor(Klass: typeof Model, name: string, store: Store) { + const relationship = Klass.relationshipsByName.get(name); + assert(`No relationship named '${name}' on '${Klass.modelName}' exists.`, relationship); + + const { options } = relationship; + const isPolymorphic = options.polymorphic; + + //If inverse is manually specified to be null, like `comments: hasMany('message', { inverse: null })` + const isExplicitInverseNull = options.inverse === null; + const isAbstractType = !isExplicitInverseNull && isPolymorphic && !store.schema.hasResource(relationship); + + if (isExplicitInverseNull || isAbstractType) { + assert( + `No schema for the abstract type '${relationship.type}' for the polymorphic relationship '${name}' on '${Klass.modelName}' was provided by the SchemaDefinitionService.`, + !isPolymorphic || isExplicitInverseNull + ); + return null; + } + + let fieldOnInverse: string | null | undefined; + let inverseKind: 'belongsTo' | 'hasMany'; + let inverseRelationship: LegacyRelationshipSchema | undefined; + let inverseOptions: LegacyRelationshipSchema['options'] | undefined; + const inverseSchema = Klass.typeForRelationship(name, store); + assert(`No model was found for '${relationship.type}'`, inverseSchema); + + // if the type does not exist and we are not polymorphic + //If inverse is specified manually, return the inverse + if (options.inverse !== undefined) { + fieldOnInverse = options.inverse!; + inverseRelationship = inverseSchema?.relationshipsByName.get(fieldOnInverse); + + assert( + `We found no field named '${fieldOnInverse}' on the schema for '${inverseSchema.modelName}' to be the inverse of the '${name}' relationship on '${Klass.modelName}'. This is most likely due to a missing field on your model definition.`, + inverseRelationship + ); + + // TODO probably just return the whole inverse here + + inverseKind = inverseRelationship.kind; + + inverseOptions = inverseRelationship.options; + } else { + //No inverse was specified manually, we need to use a heuristic to guess one + const parentModelName = relationship.options?.as ?? Klass.modelName; + if (relationship.type === parentModelName) { + warn( + `Detected a reflexive relationship named '${name}' on the schema for '${relationship.type}' without an inverse option. Look at https://guides.emberjs.com/current/models/relationships/#toc_reflexive-relations for how to explicitly specify inverses.`, + false, + { + id: 'ds.model.reflexive-relationship-without-inverse', + } + ); + } + + let possibleRelationships = findPossibleInverses(Klass, inverseSchema, name); + + if (possibleRelationships.length === 0) { + return null; + } + + if (DEBUG) { + const filteredRelationships = possibleRelationships.filter((possibleRelationship) => { + const optionsForRelationship = possibleRelationship.options; + return name === optionsForRelationship.inverse; + }); + + assert( + "You defined the '" + + name + + "' relationship on " + + String(Klass) + + ', but you defined the inverse relationships of type ' + + inverseSchema.toString() + + ' multiple times. Look at https://guides.emberjs.com/current/models/relationships/#toc_explicit-inverses for how to explicitly specify inverses', + filteredRelationships.length < 2 + ); + } + + const explicitRelationship = possibleRelationships.find((rel) => rel.options?.inverse === name); + if (explicitRelationship) { + possibleRelationships = [explicitRelationship]; + } + + assert( + "You defined the '" + + name + + "' relationship on " + + String(Klass) + + ', but multiple possible inverse relationships of type ' + + String(Klass) + + ' were found on ' + + String(inverseSchema) + + '. Look at https://guides.emberjs.com/current/models/relationships/#toc_explicit-inverses for how to explicitly specify inverses', + possibleRelationships.length === 1 + ); + + fieldOnInverse = possibleRelationships[0].name; + inverseKind = possibleRelationships[0].kind; + inverseOptions = possibleRelationships[0].options; + } + + assert(`inverseOptions should be set by now`, inverseOptions); + + // ensure inverse is properly configured + if (DEBUG) { + if (isPolymorphic) { + if (DEPRECATE_NON_EXPLICIT_POLYMORPHISM) { + if (!inverseOptions.as) { + deprecate( + `Relationships that satisfy polymorphic relationships MUST define which abstract-type they are satisfying using 'as'. The field '${fieldOnInverse}' on type '${inverseSchema.modelName}' is misconfigured.`, + false, + { + id: 'ember-data:non-explicit-relationships', + since: { enabled: '4.7', available: '4.7' }, + until: '5.0', + for: 'ember-data', + } + ); + } + } else { + assert( + `Relationships that satisfy polymorphic relationships MUST define which abstract-type they are satisfying using 'as'. The field '${fieldOnInverse}' on type '${inverseSchema.modelName}' is misconfigured.`, + inverseOptions.as + ); + assert( + `options.as should match the expected type of the polymorphic relationship. Expected field '${fieldOnInverse}' on type '${inverseSchema.modelName}' to specify '${relationship.type}' but found '${inverseOptions.as}'`, + !!inverseOptions.as && relationship.type === inverseOptions.as + ); + } + } + } + + // ensure we are properly configured + if (DEBUG) { + if (inverseOptions.polymorphic) { + if (DEPRECATE_NON_EXPLICIT_POLYMORPHISM) { + if (!options.as) { + deprecate( + `Relationships that satisfy polymorphic relationships MUST define which abstract-type they are satisfying using 'as'. The field '${name}' on type '${Klass.modelName}' is misconfigured.`, + false, + { + id: 'ember-data:non-explicit-relationships', + since: { enabled: '4.7', available: '4.7' }, + until: '5.0', + for: 'ember-data', + } + ); + } + } else { + assert( + `Relationships that satisfy polymorphic relationships MUST define which abstract-type they are satisfying using 'as'. The field '${name}' on type '${Klass.modelName}' is misconfigured.`, + options.as + ); + assert( + `options.as should match the expected type of the polymorphic relationship. Expected field '${name}' on type '${Klass.modelName}' to specify '${inverseRelationship!.type}' but found '${options.as}'`, + !!options.as && inverseRelationship!.type === options.as + ); + } + } + } + + assert( + `The ${inverseSchema.modelName}:${fieldOnInverse} relationship declares 'inverse: null', but it was resolved as the inverse for ${Klass.modelName}:${name}.`, + inverseOptions.inverse !== null + ); + + return { + type: inverseSchema.modelName, + name: fieldOnInverse, + kind: inverseKind, + options: inverseOptions, + }; +} diff --git a/packages/model/src/-private/notify-changes.ts b/packages/model/src/-private/notify-changes.ts index ee7c85fefe3..c97c80b4644 100644 --- a/packages/model/src/-private/notify-changes.ts +++ b/packages/model/src/-private/notify-changes.ts @@ -16,33 +16,26 @@ export default function notifyChanges( record: Model, store: Store ) { - switch (value) { - case 'added': - case 'attributes': - if (key) { - notifyAttribute(store, identifier, key, record); - } else { - record.eachAttribute((name) => { - notifyAttribute(store, identifier, name, record); - }); - } - break; - - case 'relationships': - if (key) { - const meta = (record.constructor as typeof Model).relationshipsByName.get(key); - assert(`Expected to find a relationship for ${key} on ${identifier.type}`, meta); - notifyRelationship(identifier, key, record, meta); - } else { - record.eachRelationship((name, meta) => { - notifyRelationship(identifier, name, record, meta); - }); - } - break; - - case 'identity': - record.notifyPropertyChange('id'); - break; + if (value === 'attributes') { + if (key) { + notifyAttribute(store, identifier, key, record); + } else { + record.eachAttribute((name) => { + notifyAttribute(store, identifier, name, record); + }); + } + } else if (value === 'relationships') { + if (key) { + const meta = (record.constructor as typeof Model).relationshipsByName.get(key); + assert(`Expected to find a relationship for ${key} on ${identifier.type}`, meta); + notifyRelationship(identifier, key, record, meta); + } else { + record.eachRelationship((name, meta) => { + notifyRelationship(identifier, name, record, meta); + }); + } + } else if (value === 'identity') { + record.notifyPropertyChange('id'); } } diff --git a/packages/model/src/-private/promise-many-array.ts b/packages/model/src/-private/promise-many-array.ts index 8aa0873ac0b..7fe925d9023 100644 --- a/packages/model/src/-private/promise-many-array.ts +++ b/packages/model/src/-private/promise-many-array.ts @@ -1,7 +1,18 @@ +import ArrayMixin, { NativeArray } from '@ember/array'; +import type ArrayProxy from '@ember/array/proxy'; +import { deprecate } from '@ember/debug'; +import Ember from 'ember'; + +import type { CreateRecordProperties } from '@ember-data/store/-private'; import type { BaseFinderOptions } from '@ember-data/store/types'; import { compat } from '@ember-data/tracking'; import { defineSignal } from '@ember-data/tracking/-private'; -import { DEPRECATE_COMPUTED_CHAINS } from '@warp-drive/build-config/deprecations'; +import { + DEPRECATE_A_USAGE, + DEPRECATE_COMPUTED_CHAINS, + DEPRECATE_PROMISE_MANY_ARRAY_BEHAVIORS, +} from '@warp-drive/build-config/deprecations'; +import { DEBUG } from '@warp-drive/build-config/env'; import { assert } from '@warp-drive/build-config/macros'; import type { RelatedCollection as ManyArray } from './many-array'; @@ -31,16 +42,46 @@ export interface HasManyProxyCreateArgs { @class PromiseManyArray @public */ +export interface PromiseManyArray extends Omit, 'destroy' | 'forEach'> { + createRecord(hash: CreateRecordProperties): T; + reload(options: Omit): PromiseManyArray; +} export class PromiseManyArray { declare promise: Promise> | null; declare isDestroyed: boolean; + // @deprecated (isDestroyed is not deprecated) + declare isDestroying: boolean; declare content: ManyArray | null; constructor(promise: Promise>, content?: ManyArray) { this._update(promise, content); this.isDestroyed = false; + this.isDestroying = false; + + if (DEPRECATE_A_USAGE) { + const meta = Ember.meta(this); + meta.hasMixin = (mixin: object) => { + deprecate(`Do not use A() on an EmberData PromiseManyArray`, false, { + id: 'ember-data:no-a-with-array-like', + until: '5.0', + since: { enabled: '4.7', available: '4.7' }, + for: 'ember-data', + }); + if (mixin === NativeArray || mixin === ArrayMixin) { + return true; + } + return false; + }; + } else if (DEBUG) { + const meta = Ember.meta(this); + meta.hasMixin = (mixin: object) => { + assert(`Do not use A() on an EmberData PromiseManyArray`); + }; + } } + //---- Methods/Properties on ArrayProxy that we will keep as our API + /** * Retrieve the length of the content * @property length @@ -156,6 +197,7 @@ export class PromiseManyArray { //---- Methods on EmberObject that we should keep destroy() { + this.isDestroying = true; this.isDestroyed = true; this.content = null; this.promise = null; @@ -217,7 +259,7 @@ if (DEPRECATE_COMPUTED_CHAINS) { return this.content?.length && this.content; }, }; - compat(desc); + compat(PromiseManyArray.prototype, '[]', desc); // ember-source < 3.23 (e.g. 3.20 lts) // requires that the tag `'[]'` be notified @@ -227,6 +269,62 @@ if (DEPRECATE_COMPUTED_CHAINS) { Object.defineProperty(PromiseManyArray.prototype, '[]', desc); } +if (DEPRECATE_PROMISE_MANY_ARRAY_BEHAVIORS) { + PromiseManyArray.prototype.createRecord = function createRecord( + this: PromiseManyArray, + hash: CreateRecordProperties + ) { + deprecate( + `The createRecord method on ember-data's PromiseManyArray is deprecated. await the promise and work with the ManyArray directly.`, + false, + { + id: 'ember-data:deprecate-promise-many-array-behaviors', + until: '5.0', + since: { enabled: '4.7', available: '4.7' }, + for: 'ember-data', + } + ); + assert('You are trying to createRecord on an async manyArray before it has been created', this.content); + return this.content.createRecord(hash); + }; + + Object.defineProperty(PromiseManyArray.prototype, 'firstObject', { + get() { + deprecate( + `The firstObject property on ember-data's PromiseManyArray is deprecated. await the promise and work with the ManyArray directly.`, + false, + { + id: 'ember-data:deprecate-promise-many-array-behaviors', + until: '5.0', + since: { enabled: '4.7', available: '4.7' }, + for: 'ember-data', + } + ); + + // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access + return this.content ? this.content.firstObject : undefined; + }, + }); + + Object.defineProperty(PromiseManyArray.prototype, 'lastObject', { + get() { + deprecate( + `The lastObject property on ember-data's PromiseManyArray is deprecated. await the promise and work with the ManyArray directly.`, + false, + { + id: 'ember-data:deprecate-promise-many-array-behaviors', + until: '5.0', + since: { enabled: '4.7', available: '4.7' }, + for: 'ember-data', + } + ); + + // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access + return this.content ? this.content.lastObject : undefined; + }, + }); +} + function tapPromise(proxy: PromiseManyArray, promise: Promise>) { proxy.isPending = true; proxy.isSettled = false; @@ -249,3 +347,105 @@ function tapPromise(proxy: PromiseManyArray, promise: Promise } ); } + +if (DEPRECATE_PROMISE_MANY_ARRAY_BEHAVIORS) { + const EmberObjectMethods = [ + 'addObserver', + 'cacheFor', + 'decrementProperty', + 'get', + 'getProperties', + 'incrementProperty', + 'notifyPropertyChange', + 'removeObserver', + 'set', + 'setProperties', + 'toggleProperty', + ]; + EmberObjectMethods.forEach((method) => { + PromiseManyArray.prototype[method] = function delegatedMethod(...args) { + deprecate( + `The ${method} method on ember-data's PromiseManyArray is deprecated. await the promise and work with the ManyArray directly.`, + false, + { + id: 'ember-data:deprecate-promise-many-array-behaviors', + until: '5.0', + since: { enabled: '4.7', available: '4.7' }, + for: 'ember-data', + } + ); + + // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-call + return Ember[method](this, ...args); + }; + }); + + const InheritedProxyMethods = [ + 'addArrayObserver', + 'addObject', + 'addObjects', + 'any', + 'arrayContentDidChange', + 'arrayContentWillChange', + 'clear', + 'compact', + 'every', + 'filter', + 'filterBy', + 'find', + 'findBy', + 'getEach', + 'includes', + 'indexOf', + 'insertAt', + 'invoke', + 'isAny', + 'isEvery', + 'lastIndexOf', + 'map', + 'mapBy', + // TODO update RFC to note objectAt was deprecated (forEach was left for iteration) + 'objectAt', + 'objectsAt', + 'popObject', + 'pushObject', + 'pushObjects', + 'reduce', + 'reject', + 'rejectBy', + 'removeArrayObserver', + 'removeAt', + 'removeObject', + 'removeObjects', + 'replace', + 'reverseObjects', + 'setEach', + 'setObjects', + 'shiftObject', + 'slice', + 'sortBy', + 'toArray', + 'uniq', + 'uniqBy', + 'unshiftObject', + 'unshiftObjects', + 'without', + ]; + InheritedProxyMethods.forEach((method) => { + PromiseManyArray.prototype[method] = function proxiedMethod(...args) { + deprecate( + `The ${method} method on ember-data's PromiseManyArray is deprecated. await the promise and work with the ManyArray directly.`, + false, + { + id: 'ember-data:deprecate-promise-many-array-behaviors', + until: '5.0', + since: { enabled: '4.7', available: '4.7' }, + for: 'ember-data', + } + ); + assert(`Cannot call ${method} before content is assigned.`, this.content); + // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-call + return this.content[method](...args); + }; + }); +} diff --git a/packages/model/src/-private/references/belongs-to.ts b/packages/model/src/-private/references/belongs-to.ts index f5363c091f9..c3b227115e5 100644 --- a/packages/model/src/-private/references/belongs-to.ts +++ b/packages/model/src/-private/references/belongs-to.ts @@ -1,8 +1,11 @@ +import { deprecate } from '@ember/debug'; + import type { Graph, ResourceEdge } from '@ember-data/graph/-private'; import type Store from '@ember-data/store'; import type { NotificationType } from '@ember-data/store'; import { cached, compat } from '@ember-data/tracking'; import { defineSignal } from '@ember-data/tracking/-private'; +import { DEPRECATE_PROMISE_PROXIES } from '@warp-drive/build-config/deprecations'; import { DEBUG } from '@warp-drive/build-config/env'; import type { StableRecordIdentifier } from '@warp-drive/core-types'; import type { StableExistingRecordIdentifier } from '@warp-drive/core-types/identifier'; @@ -477,7 +480,32 @@ export default class BelongsToReference< @param {Boolean} [skipFetch] if `true`, do not attempt to fetch unloaded records @return {Promise} */ - async push(doc: SingleResourceDocument, skipFetch?: boolean): Promise { + async push( + maybeDoc: SingleResourceDocument | Promise, + skipFetch?: boolean + ): Promise { + let doc: SingleResourceDocument = maybeDoc as SingleResourceDocument; + if (DEPRECATE_PROMISE_PROXIES) { + if ((maybeDoc as { then: unknown }).then) { + doc = await maybeDoc; + if (doc !== maybeDoc) { + deprecate( + `You passed in a Promise to a Reference API that now expects a resolved value. await the value before setting it.`, + false, + { + id: 'ember-data:deprecate-promise-proxies', + until: '5.0', + since: { + enabled: '4.7', + available: '4.7', + }, + for: 'ember-data', + } + ); + } + } + } + const { store } = this; const isResourceData = doc.data && isMaybeResource(doc.data); const added = isResourceData diff --git a/packages/model/src/-private/references/has-many.ts b/packages/model/src/-private/references/has-many.ts index 6a44d3ec4d7..b41ecfed519 100644 --- a/packages/model/src/-private/references/has-many.ts +++ b/packages/model/src/-private/references/has-many.ts @@ -1,9 +1,12 @@ +import { deprecate } from '@ember/debug'; + import type { CollectionEdge, Graph } from '@ember-data/graph/-private'; import type Store from '@ember-data/store'; import type { NotificationType } from '@ember-data/store'; import type { BaseFinderOptions } from '@ember-data/store/types'; import { cached, compat } from '@ember-data/tracking'; import { defineSignal } from '@ember-data/tracking/-private'; +import { DEPRECATE_PROMISE_PROXIES } from '@warp-drive/build-config/deprecations'; import { DEBUG } from '@warp-drive/build-config/env'; import { assert } from '@warp-drive/build-config/macros'; import type { StableRecordIdentifier } from '@warp-drive/core-types'; @@ -149,9 +152,8 @@ export default class HasManyReference< @cached @compat get identifiers(): StableRecordIdentifier>[] { - ensureRefCanSubscribe(this); // eslint-disable-next-line @typescript-eslint/no-unused-expressions - this._ref; + this._ref; // consume the tracked prop const resource = this._resource(); @@ -495,9 +497,31 @@ export default class HasManyReference< @return {Promise} */ async push( - doc: ExistingResourceObject[] | CollectionResourceDocument, + maybeDoc: ExistingResourceObject[] | CollectionResourceDocument, skipFetch?: boolean ): Promise | void> { + let doc = maybeDoc; + if (DEPRECATE_PROMISE_PROXIES) { + if ((maybeDoc as unknown as { then: unknown }).then) { + doc = await (maybeDoc as unknown as Promise); + if (doc !== maybeDoc) { + deprecate( + `You passed in a Promise to a Reference API that now expects a resolved value. await the value before setting it.`, + false, + { + id: 'ember-data:deprecate-promise-proxies', + until: '5.0', + since: { + enabled: '4.7', + available: '4.7', + }, + for: 'ember-data', + } + ); + } + } + } + const { store } = this; const dataDoc = Array.isArray(doc) ? { data: doc } : doc; const isResourceData = Array.isArray(dataDoc.data) && dataDoc.data.length > 0 && isMaybeResource(dataDoc.data[0]); @@ -605,7 +629,11 @@ export default class HasManyReference< this.___identifier )!; - if (!ensureRefCanSubscribe(this)) { + const loaded = this._isLoaded(); + + if (!loaded) { + // subscribe to changes + // for when we are not loaded yet // eslint-disable-next-line @typescript-eslint/no-unused-expressions this._ref; return null; @@ -754,22 +782,3 @@ export function isMaybeResource(object: ExistingResourceObject | ResourceIdentif const keys = Object.keys(object).filter((k) => k !== 'id' && k !== 'type' && k !== 'lid'); return keys.length > 0; } - -function ensureRefCanSubscribe(rel: HasManyReference) { - const loaded = rel._isLoaded(); - - if (!loaded) { - // subscribe to changes - // for when we are not loaded yet - // - // because the graph optimizes the case where a relationship has never been subscribed, - // we force accessed to be true here. When we make the graph public we should create a - // subscribe/unsubscribe API - const edge = rel.graph.get(rel.___identifier, rel.key); - assert(`Expected a hasMany relationship for ${rel.___identifier.type}:${rel.key}`, 'accessed' in edge); - edge.accessed = true; - - return false; - } - return true; -} diff --git a/packages/model/src/-private/relationship-meta.ts b/packages/model/src/-private/relationship-meta.ts new file mode 100644 index 00000000000..e03a181c389 --- /dev/null +++ b/packages/model/src/-private/relationship-meta.ts @@ -0,0 +1,93 @@ +import { dasherize, singularize } from '@ember-data/request-utils/string'; +import type Store from '@ember-data/store'; +import { DEBUG } from '@warp-drive/build-config/env'; +import type { LegacyRelationshipSchema } from '@warp-drive/core-types/schema/fields'; + +import type { Model } from './model'; + +function typeForRelationshipMeta(meta: LegacyRelationshipSchema): string { + let modelName = dasherize(meta.type || meta.name); + + if (meta.kind === 'hasMany') { + modelName = singularize(modelName); + } + + return modelName; +} + +function shouldFindInverse(relationshipMeta: LegacyRelationshipSchema): boolean { + const options = relationshipMeta.options; + return !(options && options.inverse === null); +} + +class RelationshipDefinition { + declare _type: string; + declare __inverseKey: string | null; + declare __hasCalculatedInverse: boolean; + declare parentModelName: string; + declare inverseIsAsync: string | null; + declare meta: LegacyRelationshipSchema; + + constructor(meta: LegacyRelationshipSchema, parentModelName: string) { + this._type = ''; + this.__inverseKey = ''; + this.__hasCalculatedInverse = false; + this.parentModelName = parentModelName; + this.meta = meta; + } + + get kind(): 'belongsTo' | 'hasMany' { + return this.meta.kind; + } + get type(): string { + if (this._type) { + return this._type; + } + this._type = typeForRelationshipMeta(this.meta); + return this._type; + } + get options() { + return this.meta.options; + } + get name(): string { + return this.meta.name; + } + + _inverseKey(store: Store, modelClass: typeof Model): string | null { + if (this.__hasCalculatedInverse === false) { + this._calculateInverse(store, modelClass); + } + return this.__inverseKey; + } + + _calculateInverse(store: Store, modelClass: typeof Model): void { + this.__hasCalculatedInverse = true; + let inverseKey: string | null = null; + let inverse: LegacyRelationshipSchema | null = null; + + if (shouldFindInverse(this.meta)) { + inverse = modelClass.inverseFor(this.name, store); + } + // TODO make this error again for the non-polymorphic case + if (DEBUG) { + if (!this.options.polymorphic) { + modelClass.typeForRelationship(this.name, store); + } + } + + if (inverse) { + inverseKey = inverse.name; + } else { + inverseKey = null; + } + this.__inverseKey = inverseKey; + } +} +export type { RelationshipDefinition }; + +export function relationshipFromMeta( + meta: LegacyRelationshipSchema, + parentModelName: string +): LegacyRelationshipSchema { + return new RelationshipDefinition(meta, parentModelName) as unknown as LegacyRelationshipSchema; +} diff --git a/packages/model/src/-private/schema-provider.ts b/packages/model/src/-private/schema-provider.ts index 88fe9563043..d906b9b29f2 100644 --- a/packages/model/src/-private/schema-provider.ts +++ b/packages/model/src/-private/schema-provider.ts @@ -3,7 +3,11 @@ import { deprecate } from '@ember/debug'; import type Store from '@ember-data/store'; import type { SchemaService } from '@ember-data/store/types'; -import { ENABLE_LEGACY_SCHEMA_SERVICE } from '@warp-drive/build-config/deprecations'; +import { + DEPRECATE_STRING_ARG_SCHEMAS, + DISABLE_6X_DEPRECATIONS, + ENABLE_LEGACY_SCHEMA_SERVICE, +} from '@warp-drive/build-config/deprecations'; import { assert } from '@warp-drive/build-config/macros'; import type { RecordIdentifier, StableRecordIdentifier } from '@warp-drive/core-types/identifier'; import type { ObjectValue } from '@warp-drive/core-types/json/raw'; @@ -164,31 +168,60 @@ export class ModelSchemaProvider implements SchemaService { if (ENABLE_LEGACY_SCHEMA_SERVICE) { ModelSchemaProvider.prototype.doesTypeExist = function (type: string): boolean { - deprecate(`Use \`schema.hasResource({ type })\` instead of \`schema.doesTypeExist(type)\``, false, { - id: 'ember-data:schema-service-updates', - until: '6.0', - for: 'ember-data', - since: { - available: '4.13', - enabled: '5.4', - }, - }); + deprecate( + `Use \`schema.hasResource({ type })\` instead of \`schema.doesTypeExist(type)\``, + /* inline-macro-config */ DISABLE_6X_DEPRECATIONS, + { + id: 'ember-data:schema-service-updates', + until: '6.0', + for: 'ember-data', + since: { + available: '4.13', + enabled: '5.4', + }, + } + ); return this.hasResource({ type }); }; ModelSchemaProvider.prototype.attributesDefinitionFor = function ( resource: RecordIdentifier | { type: string } ): AttributesSchema { - deprecate(`Use \`schema.fields({ type })\` instead of \`schema.attributesDefinitionFor({ type })\``, false, { - id: 'ember-data:schema-service-updates', - until: '6.0', - for: 'ember-data', - since: { - available: '4.13', - enabled: '5.4', - }, - }); - const type = normalizeModelName(resource.type); + let rawType: string; + if (DEPRECATE_STRING_ARG_SCHEMAS) { + if (typeof resource === 'string') { + deprecate( + `relationshipsDefinitionFor expects either a record identifier or an argument of shape { type: string }, received a string.`, + false, + { + id: 'ember-data:deprecate-string-arg-schemas', + for: 'ember-data', + until: '5.0', + since: { enabled: '4.5', available: '4.5' }, + } + ); + rawType = resource; + } else { + rawType = resource.type; + } + } else { + rawType = resource.type; + } + + deprecate( + `Use \`schema.fields({ type })\` instead of \`schema.attributesDefinitionFor({ type })\``, + /* inline-macro-config */ DISABLE_6X_DEPRECATIONS, + { + id: 'ember-data:schema-service-updates', + until: '6.0', + for: 'ember-data', + since: { + available: '4.13', + enabled: '5.4', + }, + } + ); + const type = normalizeModelName(rawType); if (!this._schemas.has(type)) { this._loadModelSchema(type); @@ -200,16 +233,41 @@ if (ENABLE_LEGACY_SCHEMA_SERVICE) { ModelSchemaProvider.prototype.relationshipsDefinitionFor = function ( resource: RecordIdentifier | { type: string } ): RelationshipsSchema { - deprecate(`Use \`schema.fields({ type })\` instead of \`schema.relationshipsDefinitionFor({ type })\``, false, { - id: 'ember-data:schema-service-updates', - until: '6.0', - for: 'ember-data', - since: { - available: '4.13', - enabled: '5.4', - }, - }); - const type = normalizeModelName(resource.type); + let rawType: string; + if (DEPRECATE_STRING_ARG_SCHEMAS) { + if (typeof resource === 'string') { + deprecate( + `relationshipsDefinitionFor expects either a record identifier or an argument of shape { type: string }, received a string.`, + false, + { + id: 'ember-data:deprecate-string-arg-schemas', + for: 'ember-data', + until: '5.0', + since: { enabled: '4.5', available: '4.5' }, + } + ); + rawType = resource; + } else { + rawType = resource.type; + } + } else { + rawType = resource.type; + } + + deprecate( + `Use \`schema.fields({ type })\` instead of \`schema.relationshipsDefinitionFor({ type })\``, + /* inline-macro-config */ DISABLE_6X_DEPRECATIONS, + { + id: 'ember-data:schema-service-updates', + until: '6.0', + for: 'ember-data', + since: { + available: '4.13', + enabled: '5.4', + }, + } + ); + const type = normalizeModelName(rawType); if (!this._schemas.has(type)) { this._loadModelSchema(type); diff --git a/packages/model/src/-private/util.ts b/packages/model/src/-private/util.ts index b6f924f360d..b9f0380dde3 100644 --- a/packages/model/src/-private/util.ts +++ b/packages/model/src/-private/util.ts @@ -1,7 +1,7 @@ import { deprecate } from '@ember/debug'; import { dasherize } from '@ember-data/request-utils/string'; -import { DEPRECATE_NON_STRICT_TYPES } from '@warp-drive/build-config/deprecations'; +import { DEPRECATE_NON_STRICT_TYPES, DISABLE_6X_DEPRECATIONS } from '@warp-drive/build-config/deprecations'; export type DecoratorPropertyDescriptor = (PropertyDescriptor & { initializer?: () => unknown }) | undefined; @@ -31,7 +31,7 @@ export function normalizeModelName(type: string): string { deprecate( `The resource type '${type}' is not normalized. Update your application code to use '${result}' instead of '${type}'.`, - result === type, + /* inline-macro-config */ DISABLE_6X_DEPRECATIONS ? true : result === type, { id: 'ember-data:deprecate-non-strict-types', until: '6.0', diff --git a/packages/model/src/migration-support.ts b/packages/model/src/migration-support.ts index e839365cc62..4ade9960611 100644 --- a/packages/model/src/migration-support.ts +++ b/packages/model/src/migration-support.ts @@ -6,7 +6,6 @@ import { assert } from '@warp-drive/build-config/macros'; import type { StableRecordIdentifier } from '@warp-drive/core-types'; import { getOrSetGlobal } from '@warp-drive/core-types/-private'; import type { ObjectValue } from '@warp-drive/core-types/json/raw'; -import type { TypedRecordInstance } from '@warp-drive/core-types/record'; import type { Derivation, HashFn, Transformation } from '@warp-drive/core-types/schema/concepts'; import type { ArrayField, @@ -38,12 +37,6 @@ import { import RecordState from './-private/record-state'; import { buildSchema } from './hooks'; -export type WithLegacyDerivations = T & - MinimalLegacyRecord & { - belongsTo: typeof belongsTo; - hasMany: typeof hasMany; - }; - type AttributesSchema = ReturnType>; type RelationshipsSchema = ReturnType>; diff --git a/packages/model/vite.config.mjs b/packages/model/vite.config.mjs index 0bd74020510..c06cb9e5c61 100644 --- a/packages/model/vite.config.mjs +++ b/packages/model/vite.config.mjs @@ -1,6 +1,7 @@ import { createConfig } from '@warp-drive/internal-config/vite/config.js'; export const externals = [ + 'ember', '@ember/service', '@ember/debug', '@ember/object/computed', diff --git a/packages/request-utils/package.json b/packages/request-utils/package.json index af46288c246..24349279d96 100644 --- a/packages/request-utils/package.json +++ b/packages/request-utils/package.json @@ -1,7 +1,7 @@ { "name": "@ember-data/request-utils", "description": "Request Building Utilities for use with EmberData", - "version": "5.4.0-alpha.136", + "version": "4.13.0-alpha.3", "private": false, "license": "MIT", "author": "Chris Thoburn ", @@ -66,7 +66,7 @@ } }, "dependencies": { - "@embroider/macros": "^1.16.10", + "@embroider/macros": "^1.16.6", "@warp-drive/build-config": "workspace:*" }, "devDependencies": { diff --git a/packages/request-utils/src/deprecation-support.ts b/packages/request-utils/src/deprecation-support.ts index 15a4d9ab05c..d0229dc3906 100644 --- a/packages/request-utils/src/deprecation-support.ts +++ b/packages/request-utils/src/deprecation-support.ts @@ -2,7 +2,7 @@ import { deprecate } from '@ember/debug'; import { dependencySatisfies, importSync, macroCondition } from '@embroider/macros'; -import { DEPRECATE_EMBER_INFLECTOR } from '@warp-drive/build-config/deprecations'; +import { DEPRECATE_EMBER_INFLECTOR, DISABLE_6X_DEPRECATIONS } from '@warp-drive/build-config/deprecations'; import { defaultRules as WarpDriveDefaults } from './-private/string/inflections'; import { irregular, plural, singular, uncountable } from './string'; @@ -85,7 +85,7 @@ if (DEPRECATE_EMBER_INFLECTOR) { deprecate( `WarpDrive/EmberData no longer uses ember-inflector for pluralization.\nPlease \`import { plural } from '@ember-data/request-utils/string';\` instead to register a custom pluralization rule for use with EmberData.`, - false, + /* inline-macro-config */ DISABLE_6X_DEPRECATIONS, { id: 'warp-drive.ember-inflector', until: '6.0.0', @@ -109,7 +109,7 @@ if (DEPRECATE_EMBER_INFLECTOR) { deprecate( `WarpDrive/EmberData no longer uses ember-inflector for singularization.\nPlease \`import { singular } from '@ember-data/request-utils/string';\` instead to register a custom singularization rule for use with EmberData.`, - false, + /* inline-macro-config */ DISABLE_6X_DEPRECATIONS, { id: 'warp-drive.ember-inflector', until: '6.0.0', @@ -141,7 +141,7 @@ if (DEPRECATE_EMBER_INFLECTOR) { deprecate( `WarpDrive/EmberData no longer uses ember-inflector for irregular rules.\nPlease \`import { irregular } from '@ember-data/request-utils/string';\` instead to register a custom irregular rule for use with EmberData for '${actualSingle}' <=> '${plur}'.`, - false, + /* inline-macro-config */ DISABLE_6X_DEPRECATIONS, { id: 'warp-drive.ember-inflector', until: '6.0.0', @@ -165,7 +165,7 @@ if (DEPRECATE_EMBER_INFLECTOR) { deprecate( `WarpDrive/EmberData no longer uses ember-inflector for uncountable rules.\nPlease \`import { uncountable } from '@ember-data/request-utils/string';\` instead to register a custom uncountable rule for '${word}' for use with EmberData.`, - false, + /* inline-macro-config */ DISABLE_6X_DEPRECATIONS, { id: 'warp-drive.ember-inflector', until: '6.0.0', @@ -184,7 +184,7 @@ if (DEPRECATE_EMBER_INFLECTOR) { deprecate( `WarpDrive/EmberData no longer uses ember-inflector for pluralization.\nPlease \`import { plural } from '@ember-data/request-utils/string';\` instead to register a custom pluralization rule for use with EmberData.`, - false, + /* inline-macro-config */ DISABLE_6X_DEPRECATIONS, { id: 'warp-drive.ember-inflector', until: '6.0.0', @@ -205,7 +205,7 @@ if (DEPRECATE_EMBER_INFLECTOR) { deprecate( `WarpDrive/EmberData no longer uses ember-inflector for singularization.\nPlease \`import { singular } from '@ember-data/request-utils/string';\` instead to register a custom singularization rule for use with EmberData.`, - false, + /* inline-macro-config */ DISABLE_6X_DEPRECATIONS, { id: 'warp-drive.ember-inflector', until: '6.0.0', @@ -226,7 +226,7 @@ if (DEPRECATE_EMBER_INFLECTOR) { deprecate( `WarpDrive/EmberData no longer uses ember-inflector for irregular rules.\nPlease \`import { irregular } from '@ember-data/request-utils/string';\` instead to register a custom irregular rule for use with EmberData.`, - false, + /* inline-macro-config */ DISABLE_6X_DEPRECATIONS, { id: 'warp-drive.ember-inflector', until: '6.0.0', @@ -247,7 +247,7 @@ if (DEPRECATE_EMBER_INFLECTOR) { deprecate( `WarpDrive/EmberData no longer uses ember-inflector for uncountable rules.\nPlease \`import { uncountable } from '@ember-data/request-utils/string';\` instead to register a custom uncountable rule for use with EmberData.`, - false, + /* inline-macro-config */ DISABLE_6X_DEPRECATIONS, { id: 'warp-drive.ember-inflector', until: '6.0.0', diff --git a/packages/request-utils/src/index.ts b/packages/request-utils/src/index.ts index 337195d6c4e..6a52199e6b0 100644 --- a/packages/request-utils/src/index.ts +++ b/packages/request-utils/src/index.ts @@ -1,5 +1,6 @@ import { deprecate } from '@ember/debug'; +import { DISABLE_6X_DEPRECATIONS } from '@warp-drive/build-config/deprecations'; import { assert } from '@warp-drive/build-config/macros'; import type { StableRecordIdentifier } from '@warp-drive/core-types'; import type { Cache } from '@warp-drive/core-types/cache'; @@ -753,7 +754,7 @@ export class CachePolicy { const _config = arguments.length === 1 ? config : (arguments[1] as unknown as PolicyConfig); deprecate( `Passing a Store to the CachePolicy is deprecated, please pass only a config instead.`, - arguments.length === 1, + /* inline-macro-config */ DISABLE_6X_DEPRECATIONS ? true : arguments.length === 1, { id: 'ember-data:request-utils:lifetimes-service-store-arg', since: { @@ -934,7 +935,7 @@ export class LifetimesService extends CachePolicy { constructor(config: PolicyConfig) { deprecate( `\`import { LifetimesService } from '@ember-data/request-utils';\` is deprecated, please use \`import { CachePolicy } from '@ember-data/request-utils';\` instead.`, - false, + /* inline-macro-config */ DISABLE_6X_DEPRECATIONS, { id: 'ember-data:deprecate-lifetimes-service-import', since: { diff --git a/packages/request/README.md b/packages/request/README.md index 01fa9c1aca0..72d246df68c 100644 --- a/packages/request/README.md +++ b/packages/request/README.md @@ -63,8 +63,8 @@ import Fetch from '@ember-data/request/fetch'; import { apiUrl } from './config'; // ... create manager and add our Fetch handler -const manager = new RequestManager() - .use([Fetch]); +const manager = new RequestManager(); +manager.use([Fetch]); // ... execute a request const response = await manager.request({ @@ -441,9 +441,13 @@ import RequestManager from '@ember-data/request'; import Fetch from '@ember-data/request/fetch'; class extends Store { - requestManager = new RequestManager() - .use([Fetch]) - .useCache(CacheHandler); + requestManager = new RequestManager(); + + constructor(args) { + super(args); + this.requestManager.use([Fetch]); + this.requestManager.useCache(CacheHandler); + } } ``` @@ -459,16 +463,19 @@ Additional handlers or a service injection like the above would need to be done consuming application in order to make broader use of `RequestManager`. ```ts -import Store from 'ember-data/store'; -import { CacheHandler } from '@ember-data/store'; +import Store, { CacheHandler } from 'ember-data/store'; import RequestManager from '@ember-data/request'; import Fetch from '@ember-data/request/fetch'; import { LegacyNetworkHandler } from '@ember-data/legacy-compat'; export default class extends Store { - requestManager = new RequestManager() - .use([LegacyNetworkHandler, Fetch]) - .useCache(CacheHandler); + requestManager = new RequestManager(); + + constructor(args) { + super(args); + this.requestManager.use([LegacyNetworkHandler, Fetch]); + this.requestManager.useCache(CacheHandler); + } } ``` diff --git a/packages/request/package.json b/packages/request/package.json index 13833d8c79d..f0f56ea9cf4 100644 --- a/packages/request/package.json +++ b/packages/request/package.json @@ -1,7 +1,7 @@ { "name": "@ember-data/request", "description": "⚡️ A simple, small and fast framework-agnostic library to make `fetch` happen", - "version": "5.4.0-alpha.136", + "version": "4.13.0-alpha.3", "license": "MIT", "author": "Chris Thoburn ", "repository": { @@ -44,7 +44,7 @@ }, "dependencies": { "@ember/test-waiters": "^3.1.0 || ^4.0.0", - "@embroider/macros": "^1.16.10", + "@embroider/macros": "^1.16.6", "@warp-drive/build-config": "workspace:*" }, "devDependencies": { diff --git a/packages/request/src/-private/manager.ts b/packages/request/src/-private/manager.ts index aa05ed5e807..df5ed2669b9 100644 --- a/packages/request/src/-private/manager.ts +++ b/packages/request/src/-private/manager.ts @@ -54,8 +54,8 @@ import Fetch from '@ember-data/request/fetch'; import { apiUrl } from './config'; // ... create manager and add our Fetch handler -const manager = new RequestManager() - .use([Fetch]); +const manager = new RequestManager(); +manager.use([Fetch]); // ... execute a request const response = await manager.request({ @@ -383,9 +383,13 @@ import RequestManager from '@ember-data/request'; import Fetch from '@ember-data/request/fetch'; class extends Store { - requestManager = new RequestManager() - .use([Fetch]) - .useCache(CacheHandler); + requestManager = new RequestManager(); + + constructor(args) { + super(args); + this.requestManager.use([Fetch]); + this.requestManager.useCache(CacheHandler); + } } ``` @@ -401,16 +405,19 @@ Additional handlers or a service injection like the above would need to be done consuming application in order to make broader use of `RequestManager`. ```ts -import Store from 'ember-data/store'; -import { CacheHandler } from '@ember-data/store'; +import Store, { CacheHandler } from 'ember-data/store'; import RequestManager from '@ember-data/request'; import Fetch from '@ember-data/request/fetch'; import { LegacyNetworkHandler } from '@ember-data/legacy-compat'; export default class extends Store { - requestManager = new RequestManager() - .use([LegacyNetworkHandler, Fetch]) - .useCache(CacheHandler); + requestManager = new RequestManager(); + + constructor(args) { + super(args); + this.requestManager.use([LegacyNetworkHandler, Fetch]); + this.requestManager.useCache(CacheHandler); + } } ``` diff --git a/packages/request/src/-private/types.ts b/packages/request/src/-private/types.ts index dc506ee6866..27d0243ffdf 100644 --- a/packages/request/src/-private/types.ts +++ b/packages/request/src/-private/types.ts @@ -205,8 +205,9 @@ In the case of the `Future` being returned, `Stream` proxying is automatic and i Request handlers are registered by configuring the manager via `use` ```ts -const manager = new RequestManager() - .use([Handler1, Handler2]); +const manager = new RequestManager(); + +manager.use([Handler1, Handler2]); ``` Handlers will be invoked in the order they are registered ("fifo", first-in first-out), and may only be registered up until the first request is made. It is recommended but not required to register all handlers at one time in order to ensure explicitly visible handler ordering. diff --git a/packages/rest/package.json b/packages/rest/package.json index 6abbbd29537..a5e51eae31c 100644 --- a/packages/rest/package.json +++ b/packages/rest/package.json @@ -1,7 +1,7 @@ { "name": "@ember-data/rest", "description": "REST Format Support for EmberData", - "version": "5.4.0-alpha.136", + "version": "4.13.0-alpha.3", "private": false, "license": "MIT", "author": "Chris Thoburn ", @@ -22,7 +22,7 @@ "extends": "../../package.json" }, "dependencies": { - "@embroider/macros": "^1.16.10", + "@embroider/macros": "^1.16.6", "@warp-drive/build-config": "workspace:*" }, "peerDependencies": { diff --git a/packages/schema-record/package.json b/packages/schema-record/package.json index 3574be9f876..254325d5d3c 100644 --- a/packages/schema-record/package.json +++ b/packages/schema-record/package.json @@ -1,6 +1,6 @@ { "name": "@warp-drive/schema-record", - "version": "5.4.0-alpha.136", + "version": "4.13.0-alpha.3", "description": "Schema Driven Resource Presentation for WarpDrive and EmberData", "keywords": [ "ember-addon" @@ -78,7 +78,7 @@ } }, "dependencies": { - "@embroider/macros": "^1.16.10", + "@embroider/macros": "^1.16.6", "@warp-drive/build-config": "workspace:*" }, "devDependencies": { diff --git a/packages/schema-record/src/record.ts b/packages/schema-record/src/record.ts index 6d5bf01f6b2..c6ee6388bcf 100644 --- a/packages/schema-record/src/record.ts +++ b/packages/schema-record/src/record.ts @@ -10,7 +10,6 @@ import type { StableRecordIdentifier } from '@warp-drive/core-types'; import type { ArrayValue, ObjectValue, Value } from '@warp-drive/core-types/json/raw'; import { STRUCTURED } from '@warp-drive/core-types/request'; import type { FieldSchema } from '@warp-drive/core-types/schema/fields'; -import type { SingleResourceRelationship } from '@warp-drive/core-types/spec/json-api-raw'; import { RecordStore } from '@warp-drive/core-types/symbols'; import { @@ -311,13 +310,6 @@ export class SchemaRecord { Mode[Editable] ); case 'belongsTo': - if (field.options.linksMode) { - entangleSignal(signals, receiver, field.name); - const rawValue = cache.getRelationship(identifier, field.name) as SingleResourceRelationship; - - // eslint-disable-next-line @typescript-eslint/no-unsafe-return - return rawValue.data ? store.peekRecord(rawValue.data) : null; - } if (!HAS_MODEL_PACKAGE) { assert( `Cannot use belongsTo fields in your schema unless @ember-data/model is installed to provide legacy model support. ${field.name} should likely be migrated to be a resource field.` diff --git a/packages/serializer/README.md b/packages/serializer/README.md index 3a5fb86afc9..3aa85809d9b 100644 --- a/packages/serializer/README.md +++ b/packages/serializer/README.md @@ -60,9 +60,13 @@ import RequestManager from '@ember-data/request'; import { LegacyNetworkHandler } from '@ember-data/legacy-compat'; export default class extends Store { - requestManager = new RequestManager() - .use([LegacyNetworkHandler]) - .useCache(CacheHandler); + requestManager = new RequestManager(); + + constructor(args) { + super(args); + this.requestManager.use([LegacyNetworkHandler]); + this.requestManager.useCache(CacheHandler); + } } ``` diff --git a/packages/serializer/package.json b/packages/serializer/package.json index a15ee758d82..5a78b81880d 100644 --- a/packages/serializer/package.json +++ b/packages/serializer/package.json @@ -1,6 +1,6 @@ { "name": "@ember-data/serializer", - "version": "5.4.0-alpha.136", + "version": "4.13.0-alpha.3", "description": "Provides Legacy JSON, JSON:API and REST Implementations of the Serializer Interface for use with @ember-data/store", "keywords": [ "ember-addon" @@ -81,7 +81,7 @@ } }, "dependencies": { - "@embroider/macros": "^1.16.10", + "@embroider/macros": "^1.16.6", "ember-cli-test-info": "^1.0.0", "ember-cli-string-utils": "^1.1.0", "ember-cli-path-utils": "^1.0.0", @@ -103,7 +103,7 @@ "@glimmer/component": "^1.1.2", "@warp-drive/core-types": "workspace:*", "@warp-drive/internal-config": "workspace:*", - "decorator-transforms": "^2.3.0", + "decorator-transforms": "^2.2.2", "ember-source": "~5.12.0", "pnpm-sync-dependencies-meta-injected": "0.0.14", "typescript": "^5.7.2", diff --git a/packages/store/README.md b/packages/store/README.md index 8daa70a0c62..64456d52d5c 100644 --- a/packages/store/README.md +++ b/packages/store/README.md @@ -108,9 +108,12 @@ import RequestManager from '@ember-data/request'; import Fetch from '@ember-data/request/fetch'; export default class extends Store { - requestManager = new RequestManager() - .use([Fetch]) - .useCache(CacheHandler); + constructor() { + super(...arguments); + this.requestManager = new RequestManager(); + this.requestManager.use([Fetch]); + this.requestManager.useCache(CacheHandler); + } } ``` diff --git a/packages/store/package.json b/packages/store/package.json index f89129e5ace..c2a485e2c64 100644 --- a/packages/store/package.json +++ b/packages/store/package.json @@ -1,6 +1,6 @@ { "name": "@ember-data/store", - "version": "5.4.0-alpha.136", + "version": "4.13.0-alpha.3", "description": "The core of EmberData. Provides the Store service which coordinates the cache with the network and presentation layers.", "keywords": [ "ember-addon" @@ -55,7 +55,7 @@ } }, "dependencies": { - "@embroider/macros": "^1.16.10", + "@embroider/macros": "^1.16.6", "@warp-drive/build-config": "workspace:*" }, "peerDependencies": { @@ -76,7 +76,7 @@ "@glimmer/component": "^1.1.2", "@warp-drive/core-types": "workspace:*", "@warp-drive/internal-config": "workspace:*", - "decorator-transforms": "^2.3.0", + "decorator-transforms": "^2.2.2", "ember-source": "~5.12.0", "expect-type": "^0.20.0", "pnpm-sync-dependencies-meta-injected": "0.0.14", diff --git a/packages/store/src/-private.ts b/packages/store/src/-private.ts index be4cea8214b..9100d7b2a94 100644 --- a/packages/store/src/-private.ts +++ b/packages/store/src/-private.ts @@ -1,6 +1,11 @@ /** @module @ember-data/store */ +import { assert, deprecate } from '@ember/debug'; + +import { DEPRECATE_HELPERS } from '@warp-drive/build-config/deprecations'; + +import { normalizeModelName as _normalize } from './-private/utils/normalize-model-name'; export { Store, storeFor } from './-private/store-service'; @@ -47,7 +52,33 @@ export { peekCache, removeRecordDataFor } from './-private/caches/cache-utils'; // @ember-data/model needs these temporarily export { setRecordIdentifier, StoreMap } from './-private/caches/instance-cache'; export { setCacheFor } from './-private/caches/cache-utils'; -export { normalizeModelName as _deprecatingNormalize } from './-private/utils/normalize-model-name'; export type { StoreRequestInput } from './-private/cache-handler/handler'; -export { log, logGroup } from './-private/debug/utils'; +/** + This method normalizes a modelName into the format EmberData uses + internally by dasherizing it. + + @method normalizeModelName + @static + @public + @deprecated + @for @ember-data/store + @param {String} modelName + @return {String} normalizedModelName +*/ +export function normalizeModelName(modelName: string) { + if (DEPRECATE_HELPERS) { + deprecate( + `the helper function normalizeModelName is deprecated. You should use model names that are already normalized, or use string helpers of your own. This function is primarily an alias for dasherize from @ember/string.`, + false, + { + id: 'ember-data:deprecate-normalize-modelname-helper', + for: 'ember-data', + until: '5.0', + since: { available: '4.7', enabled: '4.7' }, + } + ); + return _normalize(modelName); + } + assert(`normalizeModelName support has been removed`); +} diff --git a/packages/store/src/-private/cache-handler/handler.ts b/packages/store/src/-private/cache-handler/handler.ts index 59dddbe4594..6ec9006e5e7 100644 --- a/packages/store/src/-private/cache-handler/handler.ts +++ b/packages/store/src/-private/cache-handler/handler.ts @@ -44,7 +44,7 @@ export type LooseStoreRequestInfo = Omit< export type StoreRequestInput = ImmutableRequestInfo | LooseStoreRequestInfo; export interface StoreRequestContext extends RequestContext { - request: ImmutableRequestInfo & { store: Store }; + request: ImmutableRequestInfo & { store: Store; [EnableHydration]?: boolean }; } /** diff --git a/packages/store/src/-private/caches/identifier-cache.ts b/packages/store/src/-private/caches/identifier-cache.ts index 2dec77bea22..b6602336689 100644 --- a/packages/store/src/-private/caches/identifier-cache.ts +++ b/packages/store/src/-private/caches/identifier-cache.ts @@ -39,10 +39,11 @@ import { hasId, hasLid, hasType } from './resource-utils'; type ResourceData = unknown; +const IDENTIFIERS = getOrSetGlobal('IDENTIFIERS', new Set()); const DOCUMENTS = getOrSetGlobal('DOCUMENTS', new Set()); export function isStableIdentifier(identifier: unknown): identifier is StableRecordIdentifier { - return (identifier as StableRecordIdentifier)[CACHE_OWNER] !== undefined; + return (identifier as StableRecordIdentifier)[CACHE_OWNER] !== undefined || IDENTIFIERS.has(identifier); } export function isDocumentIdentifier(identifier: unknown): identifier is StableDocumentIdentifier { @@ -621,6 +622,7 @@ export class IdentifierCache { identifier[DEBUG_STALE_CACHE_OWNER] = identifier[CACHE_OWNER]; } identifier[CACHE_OWNER] = undefined; + IDENTIFIERS.delete(identifier); this._forget(identifier, 'record'); if (LOG_IDENTIFIERS) { // eslint-disable-next-line no-console @@ -647,6 +649,8 @@ function makeStableRecordIdentifier( bucket: IdentifierBucket, clientOriginated: boolean ): StableRecordIdentifier { + IDENTIFIERS.add(recordIdentifier); + if (DEBUG) { // we enforce immutability in dev // but preserve our ability to do controlled updates to the reference @@ -689,6 +693,7 @@ function makeStableRecordIdentifier( }); wrapper[DEBUG_CLIENT_ORIGINATED] = clientOriginated; wrapper[DEBUG_IDENTIFIER_BUCKET] = bucket; + IDENTIFIERS.add(wrapper); DEBUG_MAP.set(wrapper, recordIdentifier); wrapper = freeze(wrapper); return wrapper; diff --git a/packages/store/src/-private/caches/instance-cache.ts b/packages/store/src/-private/caches/instance-cache.ts index 46482bed496..1fa2a0bd4f6 100644 --- a/packages/store/src/-private/caches/instance-cache.ts +++ b/packages/store/src/-private/caches/instance-cache.ts @@ -16,7 +16,6 @@ import type { } from '@warp-drive/core-types/spec/json-api-raw'; import type { OpaqueRecordInstance } from '../../-types/q/record-instance'; -import { log, logGroup } from '../debug/utils'; import RecordReference from '../legacy-model-support/record-reference'; import { CacheCapabilitiesManager } from '../managers/cache-capabilities-manager'; import type { CacheManager } from '../managers/cache-manager'; @@ -207,11 +206,8 @@ export class InstanceCache { this.__instances.record.set(identifier, record); if (LOG_INSTANCE_CACHE) { - logGroup('reactive-ui', '', identifier.type, identifier.lid, 'created', ''); // eslint-disable-next-line no-console - console.log({ properties }); - // eslint-disable-next-line no-console - console.groupEnd(); + console.log(`InstanceCache: created Record for ${String(identifier)}`, properties); } } @@ -264,7 +260,8 @@ export class InstanceCache { removeRecordDataFor(identifier); this.store._requestCache._clearEntries(identifier); if (LOG_INSTANCE_CACHE) { - log('reactive-ui', '', identifier.type, identifier.lid, 'disconnected', ''); + // eslint-disable-next-line no-console + console.log(`InstanceCache: disconnected ${String(identifier)}`); } } diff --git a/packages/store/src/-private/managers/cache-capabilities-manager.ts b/packages/store/src/-private/managers/cache-capabilities-manager.ts index 941c6ba60ea..d614eca9799 100644 --- a/packages/store/src/-private/managers/cache-capabilities-manager.ts +++ b/packages/store/src/-private/managers/cache-capabilities-manager.ts @@ -72,13 +72,13 @@ export class CacheCapabilitiesManager implements StoreWrapper { }); } - notifyChange(identifier: StableRecordIdentifier, namespace: 'added' | 'removed', key: null): void; - notifyChange(identifier: StableDocumentIdentifier, namespace: 'added' | 'updated' | 'removed', key: null): void; - notifyChange(identifier: StableRecordIdentifier, namespace: NotificationType, key: string | null): void; + notifyChange(identifier: StableRecordIdentifier, namespace: 'added' | 'removed'): void; + notifyChange(identifier: StableDocumentIdentifier, namespace: 'added' | 'updated' | 'removed'): void; + notifyChange(identifier: StableRecordIdentifier, namespace: NotificationType, key?: string): void; notifyChange( identifier: StableRecordIdentifier | StableDocumentIdentifier, namespace: NotificationType | 'added' | 'removed' | 'updated', - key: string | null + key?: string ): void { assert(`Expected a stable identifier`, isStableIdentifier(identifier) || isDocumentIdentifier(identifier)); diff --git a/packages/store/src/-private/managers/notification-manager.ts b/packages/store/src/-private/managers/notification-manager.ts index b74fe6eaaf9..ddd36534d9a 100644 --- a/packages/store/src/-private/managers/notification-manager.ts +++ b/packages/store/src/-private/managers/notification-manager.ts @@ -4,13 +4,12 @@ import { _backburner } from '@ember/runloop'; -import { LOG_METRIC_COUNTS, LOG_NOTIFICATIONS } from '@warp-drive/build-config/debugging'; +import { LOG_NOTIFICATIONS } from '@warp-drive/build-config/debugging'; import { DEBUG } from '@warp-drive/build-config/env'; import { assert } from '@warp-drive/build-config/macros'; import type { StableDocumentIdentifier, StableRecordIdentifier } from '@warp-drive/core-types/identifier'; import { isDocumentIdentifier, isStableIdentifier } from '../caches/identifier-cache'; -import { log } from '../debug/utils'; import type { Store } from '../store-service'; export type UnsubscribeToken = object; @@ -20,7 +19,9 @@ const CacheOperations = new Set(['added', 'removed', 'state', 'updated', 'invali export type CacheOperation = 'added' | 'removed' | 'updated' | 'state'; export type DocumentCacheOperation = 'invalidated' | 'added' | 'removed' | 'updated' | 'state'; -function isCacheOperationValue(value: NotificationType | DocumentCacheOperation): value is DocumentCacheOperation { +function isCacheOperationValue( + value: NotificationType | CacheOperation | DocumentCacheOperation +): value is DocumentCacheOperation { return CacheOperations.has(value); } @@ -29,12 +30,11 @@ function runLoopIsFlushing(): boolean { return !!_backburner.currentInstance && _backburner._autorun !== true; } -export type NotificationType = 'attributes' | 'relationships' | 'identity' | 'errors' | 'meta' | CacheOperation; +export type NotificationType = 'attributes' | 'relationships' | 'identity' | 'errors' | 'meta' | 'state'; export interface NotificationCallback { (identifier: StableRecordIdentifier, notificationType: 'attributes' | 'relationships', key?: string): void; (identifier: StableRecordIdentifier, notificationType: 'errors' | 'meta' | 'identity' | 'state'): void; - (identifier: StableRecordIdentifier, notificationType: CacheOperation): void; // (identifier: StableRecordIdentifier, notificationType: NotificationType, key?: string): void; } @@ -48,15 +48,6 @@ export interface DocumentOperationCallback { (identifier: StableDocumentIdentifier, notificationType: DocumentCacheOperation): void; } -function count(label: string) { - // @ts-expect-error - // eslint-disable-next-line - globalThis.counts = globalThis.counts || {}; - // @ts-expect-error - // eslint-disable-next-line - globalThis.counts[label] = (globalThis.counts[label] || 0) + 1; -} - function _unsubscribe( tokens: Map, token: UnsubscribeToken, @@ -213,6 +204,11 @@ export default class NotificationManager { return false; } + if (LOG_NOTIFICATIONS) { + // eslint-disable-next-line no-console + console.log(`Buffering Notify: ${String(identifier.lid)}\t${value}\t${key || ''}`); + } + const hasSubscribers = Boolean(this._cache.get(identifier)?.size); if (isCacheOperationValue(value) || hasSubscribers) { @@ -223,23 +219,7 @@ export default class NotificationManager { } buffer.push([value, key]); - if (LOG_METRIC_COUNTS) { - count(`notify ${'type' in identifier ? identifier.type : ''} ${value} ${key}`); - } - if (!this._scheduleNotify()) { - if (LOG_NOTIFICATIONS) { - log( - 'notify', - 'buffered', - `${'type' in identifier ? identifier.type : 'document'}`, - identifier.lid, - `${value}`, - key || '' - ); - } - } - } else if (LOG_METRIC_COUNTS) { - count(`DISCARDED notify ${'type' in identifier ? identifier.type : ''} ${value} ${key}`); + this._scheduleNotify(); } return hasSubscribers; @@ -249,22 +229,21 @@ export default class NotificationManager { this._onFlushCB = cb; } - _scheduleNotify(): boolean { + _scheduleNotify() { const asyncFlush = this.store._enableAsyncFlush; if (this._hasFlush) { if (asyncFlush !== false && !runLoopIsFlushing()) { - return false; + return; } } if (asyncFlush && !runLoopIsFlushing()) { this._hasFlush = true; - return false; + return; } this._flush(); - return true; } _flush() { @@ -293,14 +272,8 @@ export default class NotificationManager { key?: string ): boolean { if (LOG_NOTIFICATIONS) { - log( - 'notify', - '', - `${'type' in identifier ? identifier.type : 'document'}`, - identifier.lid, - `${value}`, - key || '' - ); + // eslint-disable-next-line no-console + console.log(`Notifying: ${String(identifier)}\t${value}\t${key || ''}`); } // TODO for documents this will need to switch based on Identifier kind diff --git a/packages/store/src/-private/proxies/promise-proxies.ts b/packages/store/src/-private/proxies/promise-proxies.ts new file mode 100644 index 00000000000..bc2d0f69b10 --- /dev/null +++ b/packages/store/src/-private/proxies/promise-proxies.ts @@ -0,0 +1,225 @@ +import { deprecate } from '@ember/debug'; +import { get } from '@ember/object'; + +import { DEBUG } from '@warp-drive/build-config/env'; + +import type IdentifierArray from '../record-arrays/identifier-array'; +import { PromiseArrayProxy, PromiseObjectProxy } from './promise-proxy-base'; + +/** + @module @ember-data/store +*/ + +/** + A `PromiseArray` is an object that acts like both an `Ember.Array` + and a promise. When the promise is resolved the resulting value + will be set to the `PromiseArray`'s `content` property. This makes + it easy to create data bindings with the `PromiseArray` that will be + updated when the promise resolves. + + This class should not be imported and instantiated directly. + + For more information see the [Ember.PromiseProxyMixin + documentation](/ember/release/classes/PromiseProxyMixin). + + Example + + ```javascript + let promiseArray = PromiseArray.create({ + promise: $.getJSON('/some/remote/data.json') + }); + + promiseArray.length; // 0 + + promiseArray.then(function() { + promiseArray.length; // 100 + }); + ``` + + @class PromiseArray + @public + @extends Ember.ArrayProxy + @uses Ember.PromiseProxyMixin +*/ + +/** + A `PromiseObject` is an object that acts like both an `EmberObject` + and a promise. When the promise is resolved, then the resulting value + will be set to the `PromiseObject`'s `content` property. This makes + it easy to create data bindings with the `PromiseObject` that will + be updated when the promise resolves. + + This class should not be imported and instantiated directly. + + For more information see the [Ember.PromiseProxyMixin + documentation](/ember/release/classes/PromiseProxyMixin.html). + + Example + + ```javascript + let promiseObject = PromiseObject.create({ + promise: $.getJSON('/some/remote/data.json') + }); + + promiseObject.name; // null + + promiseObject.then(function() { + promiseObject.name; // 'Tomster' + }); + ``` + + @class PromiseObject + @public + @extends Ember.ObjectProxy + @uses Ember.PromiseProxyMixin +*/ +export { PromiseObjectProxy as PromiseObject }; + +function _promiseObject(promise: Promise): Promise { + return PromiseObjectProxy.create({ promise }) as Promise; +} + +function _promiseArray(promise: Promise>): Promise> { + // @ts-expect-error this bucket of lies allows us to avoid typing the promise proxy which would + // require us to override a lot of Ember's types. + return PromiseArrayProxy.create({ promise }) as unknown as Promise>; +} + +// constructor is accessed in some internals but not including it in the copyright for the deprecation +const ALLOWABLE_METHODS = ['constructor', 'then', 'catch', 'finally']; +const ALLOWABLE_PROPS = ['__ec_yieldable__', '__ec_cancel__'] as const; +const PROXIED_ARRAY_PROPS = [ + 'length', + '[]', + 'firstObject', + 'lastObject', + 'meta', + 'content', + 'isPending', + 'isSettled', + 'isRejected', + 'isFulfilled', + 'promise', + 'reason', +]; +const PROXIED_OBJECT_PROPS = ['content', 'isPending', 'isSettled', 'isRejected', 'isFulfilled', 'promise', 'reason']; + +function isAllowedProp(prop: string): prop is (typeof ALLOWABLE_PROPS)[number] { + return ALLOWABLE_PROPS.includes(prop as (typeof ALLOWABLE_PROPS)[number]); +} + +type SensitiveArray = { + __ec_yieldable__: unknown; + __ec_cancel__: unknown; +} & Promise>; + +type SensitiveObject = { + __ec_yieldable__: unknown; + __ec_cancel__: unknown; +} & Promise; + +export function promiseArray(promise: Promise>): Promise> { + const promiseObjectProxy = _promiseArray(promise); + if (!DEBUG) { + return promiseObjectProxy; + } + const handler = { + get(target: SensitiveArray, prop: string, receiver: object): unknown { + if (typeof prop === 'symbol') { + return Reflect.get(target, prop, receiver); + } + if (isAllowedProp(prop)) { + return target[prop]; + } + if (!ALLOWABLE_METHODS.includes(prop)) { + deprecate( + `Accessing ${prop} on this PromiseArray is deprecated. The return type is being changed from PromiseArray to a Promise. The only available methods to access on this promise are .then, .catch and .finally`, + false, + { + id: 'ember-data:deprecate-promise-proxies', + until: '5.0', + for: '@ember-data/store', + since: { + available: '4.7', + enabled: '4.7', + }, + } + ); + } + + // @ts-expect-error difficult to coerce target to the classic ember proxy + const value: unknown = target[prop]; + if (value && typeof value === 'function' && typeof value.bind === 'function') { + return value.bind(target); + } + + if (PROXIED_ARRAY_PROPS.includes(prop)) { + return value; + } + + return undefined; + }, + }; + + return new Proxy(promiseObjectProxy, handler); +} + +const ProxySymbolString = String(Symbol.for('PROXY_CONTENT')); + +export function promiseObject(promise: Promise): Promise { + const promiseObjectProxy = _promiseObject(promise); + if (!DEBUG) { + return promiseObjectProxy; + } + const handler = { + get(target: SensitiveObject, prop: string, receiver: object): unknown { + if (typeof prop === 'symbol') { + if (String(prop) === ProxySymbolString) { + return; + } + return Reflect.get(target, prop, receiver); + } + + if (prop === 'constructor') { + return target.constructor; + } + + if (isAllowedProp(prop)) { + return target[prop]; + } + + if (!ALLOWABLE_METHODS.includes(prop)) { + deprecate( + `Accessing ${prop} on this PromiseObject is deprecated. The return type is being changed from PromiseObject to a Promise. The only available methods to access on this promise are .then, .catch and .finally`, + false, + { + id: 'ember-data:deprecate-promise-proxies', + until: '5.0', + for: '@ember-data/store', + since: { + available: '4.7', + enabled: '4.7', + }, + } + ); + } else { + // @ts-expect-error difficult to coerce target to the classic ember proxy + return (target[prop] as () => unknown).bind(target); + } + + if (PROXIED_OBJECT_PROPS.includes(prop)) { + // @ts-expect-error difficult to coerce target to the classic ember proxy + return target[prop]; + } + + const value: unknown = get(target, prop); + if (value && typeof value === 'function' && typeof value.bind === 'function') { + return value.bind(receiver); + } + + return undefined; + }, + }; + + return new Proxy(promiseObjectProxy, handler); +} diff --git a/packages/store/src/-private/proxies/promise-proxy-base.d.ts b/packages/store/src/-private/proxies/promise-proxy-base.d.ts new file mode 100644 index 00000000000..8c54a04b1de --- /dev/null +++ b/packages/store/src/-private/proxies/promise-proxy-base.d.ts @@ -0,0 +1,65 @@ +import ArrayProxy from '@ember/array/proxy'; +import ObjectProxy from '@ember/object/proxy'; + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export interface PromiseArrayProxy extends ArrayProxy, Promise {} +export class PromiseArrayProxy extends ArrayProxy { + declare content: T; + + /* + * If the proxied promise is rejected this will contain the reason + * provided. + */ + declare reason: string | Error; + /* + * Once the proxied promise has settled this will become `false`. + */ + declare isPending: boolean; + /* + * Once the proxied promise has settled this will become `true`. + */ + declare isSettled: boolean; + /* + * Will become `true` if the proxied promise is rejected. + */ + declare isRejected: boolean; + /* + * Will become `true` if the proxied promise is fulfilled. + */ + declare isFulfilled: boolean; + /* + * The promise whose fulfillment value is being proxied by this object. + */ + declare promise: Promise; +} + +export interface PromiseObjectProxy extends ObjectProxy, Promise {} +export class PromiseObjectProxy extends ObjectProxy { + declare content?: T | null; + + /* + * If the proxied promise is rejected this will contain the reason + * provided. + */ + reason: string | Error; + /* + * Once the proxied promise has settled this will become `false`. + */ + isPending: boolean; + /* + * Once the proxied promise has settled this will become `true`. + */ + isSettled: boolean; + /* + * Will become `true` if the proxied promise is rejected. + */ + isRejected: boolean; + /* + * Will become `true` if the proxied promise is fulfilled. + */ + isFulfilled: boolean; + /* + * The promise whose fulfillment value is being proxied by this object. + */ + promise: Promise; +} diff --git a/packages/store/src/-private/proxies/promise-proxy-base.js b/packages/store/src/-private/proxies/promise-proxy-base.js new file mode 100644 index 00000000000..87c98734d04 --- /dev/null +++ b/packages/store/src/-private/proxies/promise-proxy-base.js @@ -0,0 +1,9 @@ +import ArrayProxy from '@ember/array/proxy'; +import { reads } from '@ember/object/computed'; +import PromiseProxyMixin from '@ember/object/promise-proxy-mixin'; +import ObjectProxy from '@ember/object/proxy'; + +export class PromiseArrayProxy extends ArrayProxy.extend(PromiseProxyMixin) { + @reads('content.meta') meta; +} +export const PromiseObjectProxy = ObjectProxy.extend(PromiseProxyMixin); diff --git a/packages/store/src/-private/record-arrays/identifier-array.ts b/packages/store/src/-private/record-arrays/identifier-array.ts index a5b1f79aa42..06b83a9633d 100644 --- a/packages/store/src/-private/record-arrays/identifier-array.ts +++ b/packages/store/src/-private/record-arrays/identifier-array.ts @@ -1,6 +1,11 @@ /** @module @ember-data/store */ +import { deprecate } from '@ember/debug'; +import { get, set } from '@ember/object'; +import { compare } from '@ember/utils'; +import Ember from 'ember'; + import { compat } from '@ember-data/tracking'; import type { Signal } from '@ember-data/tracking/-private'; import { @@ -10,7 +15,14 @@ import { defineSignal, subscribe, } from '@ember-data/tracking/-private'; -import { DEPRECATE_COMPUTED_CHAINS } from '@warp-drive/build-config/deprecations'; +import { + DEPRECATE_A_USAGE, + DEPRECATE_ARRAY_LIKE, + DEPRECATE_COMPUTED_CHAINS, + DEPRECATE_PROMISE_PROXIES, + DEPRECATE_SNAPSHOT_MODEL_CLASS_ACCESS, +} from '@warp-drive/build-config/deprecations'; +import { DEBUG } from '@warp-drive/build-config/env'; import { assert } from '@warp-drive/build-config/macros'; import { getOrSetGlobal } from '@warp-drive/core-types/-private'; import type { StableRecordIdentifier } from '@warp-drive/core-types/identifier'; @@ -22,6 +34,7 @@ import type { OpaqueRecordInstance } from '../../-types/q/record-instance'; import { isStableIdentifier } from '../caches/identifier-cache'; import { recordIdentifierFor } from '../caches/instance-cache'; import type { RecordArrayManager } from '../managers/record-array-manager'; +import { promiseArray } from '../proxies/promise-proxies'; import type { Store } from '../store-service'; import { NativeProxy } from './native-proxy-type-fix'; @@ -94,6 +107,19 @@ export type IdentifierArrayCreateOptions = { meta?: Record | null; }; +function deprecateArrayLike(className: string, fnName: string, replName: string) { + deprecate( + `The \`${fnName}\` method on the class ${className} is deprecated. Use the native array method \`${replName}\` instead.`, + false, + { + id: 'ember-data:deprecate-array-like', + until: '5.0', + since: { enabled: '4.7', available: '4.7' }, + for: 'ember-data', + } + ); +} + interface PrivateState { links: Links | PaginationLinks | null; meta: Record | null; @@ -183,6 +209,16 @@ export class IdentifierArray { declare links: Links | PaginationLinks | null; declare meta: Record | null; declare modelName?: TypeFromInstanceOrString; + + /** + The modelClass represented by this record array. + + @property type + @public + @deprecated + @type {subclass of Model} + */ + declare type: unknown; /** The store that created this record array. @@ -300,6 +336,7 @@ export class IdentifierArray { } const args: unknown[] = Array.prototype.slice.call(arguments); assert(`Cannot start a new array transaction while a previous transaction is underway`, !transaction); + transaction = true; const result = self[MUTATE]!(target, receiver, prop as string, args, _SIGNAL); transaction = false; @@ -313,6 +350,18 @@ export class IdentifierArray { } if (isSelfProp(self, prop)) { + if (DEPRECATE_ARRAY_LIKE) { + if (prop === 'firstObject') { + deprecateArrayLike(self.DEPRECATED_CLASS_NAME, prop as string, '[0]'); + // @ts-expect-error adding MutableArray method calling index signature + return receiver[0]; + } else if (prop === 'lastObject') { + deprecateArrayLike(self.DEPRECATED_CLASS_NAME, prop as string, 'at(-1)'); + // @ts-expect-error adding MutableArray method calling index signature + return receiver[receiver.length - 1]; + } + } + if (prop === NOTIFY || prop === ARRAY_SIGNAL || prop === SOURCE) { return self[prop]; } @@ -436,6 +485,28 @@ export class IdentifierArray { }, }) as IdentifierArray; + if (DEPRECATE_A_USAGE) { + const meta = Ember.meta(this); + meta.hasMixin = (mixin: object) => { + deprecate(`Do not call A() on EmberData RecordArrays`, false, { + id: 'ember-data:no-a-with-array-like', + until: '5.0', + since: { enabled: '4.7', available: '4.7' }, + for: 'ember-data', + }); + // @ts-expect-error ArrayMixin is more than a type + if (mixin === NativeArray || mixin === ArrayMixin) { + return true; + } + return false; + }; + } else if (DEBUG) { + const meta = Ember.meta(this); + meta.hasMixin = (mixin: object) => { + assert(`Do not call A() on EmberData RecordArrays`); + }; + } + createArrayTags(proxy, _SIGNAL); this[NOTIFY] = this[NOTIFY].bind(proxy); @@ -515,9 +586,14 @@ export class IdentifierArray { @public @return {Promise} promise */ - save(): Promise { + save(): Promise> { const promise = Promise.all(this.map((record) => this.store.saveRecord(record))).then(() => this); + if (DEPRECATE_PROMISE_PROXIES) { + // @ts-expect-error IdentifierArray is not a MutableArray + return promiseArray>(promise); + } + return promise; } } @@ -542,6 +618,31 @@ Object.defineProperty(IdentifierArray.prototype, '[]', desc); defineSignal(IdentifierArray.prototype, 'isUpdating', false); +export default IdentifierArray; + +if (DEPRECATE_SNAPSHOT_MODEL_CLASS_ACCESS) { + Object.defineProperty(IdentifierArray.prototype, 'type', { + get() { + deprecate( + `Using RecordArray.type to access the ModelClass for a record is deprecated. Use store.modelFor() instead.`, + false, + { + id: 'ember-data:deprecate-snapshot-model-class-access', + until: '5.0', + for: 'ember-data', + since: { available: '4.5.0', enabled: '4.5.0' }, + } + ); + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + if (!this.modelName) { + return null; + } + // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call + return this.store.modelFor(this.modelName); + }, + }); +} + export type CollectionCreateOptions = IdentifierArrayCreateOptions & { manager: RecordArrayManager; query: ImmutableRequestInfo | Record | null; @@ -570,6 +671,11 @@ export class Collection extends IdentifierArray { // both being valid options to pass through here. const promise = store.query(this.modelName, query as Record, { _recordArray: this }); + if (DEPRECATE_PROMISE_PROXIES) { + // @ts-expect-error Collection is not a MutableArray + return promiseArray(promise); + } + return promise; } @@ -583,7 +689,415 @@ export class Collection extends IdentifierArray { Collection.prototype.query = null; // Ensure instanceof works correctly -// Object.setPrototypeOf(IdentifierArray.prototype, Array.prototype); +//Object.setPrototypeOf(IdentifierArray.prototype, Array.prototype); + +if (DEPRECATE_ARRAY_LIKE) { + IdentifierArray.prototype.DEPRECATED_CLASS_NAME = 'RecordArray'; + Collection.prototype.DEPRECATED_CLASS_NAME = 'RecordArray'; + const EmberObjectMethods = [ + 'addObserver', + 'cacheFor', + 'decrementProperty', + 'get', + 'getProperties', + 'incrementProperty', + 'notifyPropertyChange', + 'removeObserver', + 'set', + 'setProperties', + 'toggleProperty', + ] as const; + EmberObjectMethods.forEach((method) => { + // @ts-expect-error adding MutableArray method + IdentifierArray.prototype[method] = function delegatedMethod(...args: unknown[]): unknown { + deprecate( + `The EmberObject ${method} method on the class ${this.DEPRECATED_CLASS_NAME} is deprecated. Use dot-notation javascript get/set access instead.`, + false, + { + id: 'ember-data:deprecate-array-like', + until: '5.0', + since: { enabled: '4.7', available: '4.7' }, + for: 'ember-data', + } + ); + // @ts-expect-error ember is missing types for some methods + return (Ember[method] as (...args: unknown[]) => unknown)(this, ...args); + }; + }); + + // @ts-expect-error adding MutableArray method + IdentifierArray.prototype.addObject = function (obj: OpaqueRecordInstance) { + deprecateArrayLike(this.DEPRECATED_CLASS_NAME, 'addObject', 'push'); + const index = this.indexOf(obj); + if (index === -1) { + this.push(obj); + } + return this; + }; + + // @ts-expect-error adding MutableArray method + IdentifierArray.prototype.addObjects = function (objs: OpaqueRecordInstance[]) { + deprecateArrayLike(this.DEPRECATED_CLASS_NAME, 'addObjects', 'push'); + objs.forEach((obj: OpaqueRecordInstance) => { + const index = this.indexOf(obj); + if (index === -1) { + this.push(obj); + } + }); + return this; + }; + + // @ts-expect-error adding MutableArray method + IdentifierArray.prototype.popObject = function () { + deprecateArrayLike(this.DEPRECATED_CLASS_NAME, 'popObject', 'pop'); + return this.pop() as OpaqueRecordInstance; + }; + + // @ts-expect-error adding MutableArray method + IdentifierArray.prototype.pushObject = function (obj: OpaqueRecordInstance) { + deprecateArrayLike(this.DEPRECATED_CLASS_NAME, 'pushObject', 'push'); + this.push(obj); + return obj; + }; + + // @ts-expect-error adding MutableArray method + IdentifierArray.prototype.pushObjects = function (objs: OpaqueRecordInstance[]) { + deprecateArrayLike(this.DEPRECATED_CLASS_NAME, 'pushObjects', 'push'); + this.push(...objs); + return this; + }; + + // @ts-expect-error adding MutableArray method + IdentifierArray.prototype.shiftObject = function () { + deprecateArrayLike(this.DEPRECATED_CLASS_NAME, 'shiftObject', 'shift'); + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return this.shift()!; + }; + + // @ts-expect-error adding MutableArray method + IdentifierArray.prototype.unshiftObject = function (obj: OpaqueRecordInstance) { + deprecateArrayLike(this.DEPRECATED_CLASS_NAME, 'unshiftObject', 'unshift'); + this.unshift(obj); + return obj; + }; + + // @ts-expect-error adding MutableArray method + IdentifierArray.prototype.unshiftObjects = function (objs: OpaqueRecordInstance[]) { + deprecateArrayLike(this.DEPRECATED_CLASS_NAME, 'unshiftObjects', 'unshift'); + this.unshift(...objs); + return this; + }; + + // @ts-expect-error adding MutableArray method + IdentifierArray.prototype.objectAt = function (index: number) { + deprecateArrayLike(this.DEPRECATED_CLASS_NAME, 'objectAt', 'at'); + //For negative index values go back from the end of the array + const arrIndex = Math.sign(index) === -1 ? this.length + index : index; + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return this[arrIndex]; + }; + + // @ts-expect-error adding MutableArray method + IdentifierArray.prototype.objectsAt = function (indices: number[]) { + deprecateArrayLike(this.DEPRECATED_CLASS_NAME, 'objectsAt', 'at'); + // @ts-expect-error adding MutableArray method + // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-call + return indices.map((index) => this.objectAt(index)!); + }; + + // @ts-expect-error adding MutableArray method + IdentifierArray.prototype.removeAt = function (index: number) { + deprecateArrayLike(this.DEPRECATED_CLASS_NAME, 'removeAt', 'splice'); + this.splice(index, 1); + return this; + }; + + // @ts-expect-error adding MutableArray method + IdentifierArray.prototype.insertAt = function (index: number, obj: OpaqueRecordInstance) { + deprecateArrayLike(this.DEPRECATED_CLASS_NAME, 'insertAt', 'splice'); + this.splice(index, 0, obj); + return this; + }; + + // @ts-expect-error adding MutableArray method + IdentifierArray.prototype.removeObject = function (obj: OpaqueRecordInstance) { + deprecateArrayLike(this.DEPRECATED_CLASS_NAME, 'removeObject', 'splice'); + const index = this.indexOf(obj); + if (index !== -1) { + this.splice(index, 1); + } + return this; + }; + + // @ts-expect-error adding MutableArray method + IdentifierArray.prototype.removeObjects = function (objs: OpaqueRecordInstance[]) { + deprecateArrayLike(this.DEPRECATED_CLASS_NAME, 'removeObjects', 'splice'); + objs.forEach((obj) => { + const index = this.indexOf(obj); + if (index !== -1) { + this.splice(index, 1); + } + }); + return this; + }; + + // @ts-expect-error adding MutableArray method + IdentifierArray.prototype.toArray = function () { + deprecateArrayLike(this.DEPRECATED_CLASS_NAME, 'toArray', 'slice'); + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return this.slice(); + }; + + // @ts-expect-error adding MutableArray method + IdentifierArray.prototype.replace = function (idx: number, amt: number, objects?: OpaqueRecordInstance[]) { + deprecateArrayLike(this.DEPRECATED_CLASS_NAME, 'replace', 'splice'); + if (objects) { + this.splice(idx, amt, ...objects); + } else { + this.splice(idx, amt); + } + }; + + // @ts-expect-error adding MutableArray method + IdentifierArray.prototype.clear = function () { + deprecateArrayLike(this.DEPRECATED_CLASS_NAME, 'clear', 'length = 0'); + this.splice(0, this.length); + return this; + }; + + // @ts-expect-error adding MutableArray method + IdentifierArray.prototype.setObjects = function (objects: OpaqueRecordInstance[]) { + deprecateArrayLike(this.DEPRECATED_CLASS_NAME, 'setObjects', '`arr.length = 0; arr.push(objects);`'); + assert( + `${this.DEPRECATED_CLASS_NAME}.setObjects expects to receive an array as its argument`, + Array.isArray(objects) + ); + this.splice(0, this.length); + this.push(...objects); + return this; + }; + + // @ts-expect-error adding MutableArray method + IdentifierArray.prototype.reverseObjects = function () { + deprecateArrayLike(this.DEPRECATED_CLASS_NAME, 'reverseObjects', 'reverse'); + this.reverse(); + return this; + }; + + // @ts-expect-error adding MutableArray method + IdentifierArray.prototype.compact = function () { + deprecateArrayLike(this.DEPRECATED_CLASS_NAME, 'compact', 'filter'); + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return this.filter((v) => v !== null && v !== undefined); + }; + + // @ts-expect-error adding MutableArray method + IdentifierArray.prototype.any = function (callback, target) { + deprecateArrayLike(this.DEPRECATED_CLASS_NAME, 'any', 'some'); + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + return this.some(callback, target); + }; + + // @ts-expect-error adding MutableArray method + IdentifierArray.prototype.isAny = function (prop, value) { + deprecateArrayLike(this.DEPRECATED_CLASS_NAME, 'isAny', 'some'); + const hasValue = arguments.length === 2; + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + return this.some((v) => (hasValue ? v[prop] === value : v[prop] === true)); + }; + + // @ts-expect-error adding MutableArray method + IdentifierArray.prototype.isEvery = function (prop, value) { + deprecateArrayLike(this.DEPRECATED_CLASS_NAME, 'isEvery', 'every'); + const hasValue = arguments.length === 2; + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + return this.every((v) => (hasValue ? v[prop] === value : v[prop] === true)); + }; + + // @ts-expect-error adding MutableArray method + IdentifierArray.prototype.getEach = function (key: string) { + deprecateArrayLike(this.DEPRECATED_CLASS_NAME, 'getEach', 'map'); + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return this.map((value) => get(value, key)); + }; + + // @ts-expect-error adding MutableArray method + IdentifierArray.prototype.mapBy = function (key: string) { + deprecateArrayLike(this.DEPRECATED_CLASS_NAME, 'mapBy', 'map'); + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return this.map((value) => get(value, key)); + }; + + // @ts-expect-error adding MutableArray method + IdentifierArray.prototype.findBy = function (key: string, value?: unknown) { + deprecateArrayLike(this.DEPRECATED_CLASS_NAME, 'findBy', 'find'); + if (arguments.length === 2) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return this.find((val) => { + return get(val, key) === value; + }); + } else { + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return this.find((val) => Boolean(get(val, key))); + } + }; + + // @ts-expect-error adding MutableArray method + IdentifierArray.prototype.filterBy = function (key: string, value?: unknown) { + deprecateArrayLike(this.DEPRECATED_CLASS_NAME, 'filterBy', 'filter'); + if (arguments.length === 2) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return this.filter((record) => { + return get(record, key) === value; + }); + } + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return this.filter((record) => { + return Boolean(get(record, key)); + }); + }; + + // @ts-expect-error adding MutableArray method + IdentifierArray.prototype.sortBy = function (...sortKeys: string[]) { + deprecateArrayLike(this.DEPRECATED_CLASS_NAME, 'sortBy', '.slice().sort'); + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return this.slice().sort((a, b) => { + for (let i = 0; i < sortKeys.length; i++) { + const key = sortKeys[i]; + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const propA = get(a, key); + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const propB = get(b, key); + // return 1 or -1 else continue to the next sortKey + const compareValue = compare(propA, propB); + + if (compareValue) { + return compareValue; + } + } + return 0; + }); + }; + + // @ts-expect-error + IdentifierArray.prototype.invoke = function (key: string, ...args: unknown[]) { + deprecateArrayLike(this.DEPRECATED_CLASS_NAME, 'invoke', 'forEach'); + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + return this.map((value) => (value[key] as (...args: unknown[]) => unknown)(...args)); + }; + + // @ts-expect-error + IdentifierArray.prototype.addArrayObserver = function () { + deprecateArrayLike( + this.DEPRECATED_CLASS_NAME, + 'addArrayObserver', + 'derived state or reacting at the change source' + ); + }; + + // @ts-expect-error + IdentifierArray.prototype.removeArrayObserver = function () { + deprecateArrayLike( + this.DEPRECATED_CLASS_NAME, + 'removeArrayObserver', + 'derived state or reacting at the change source' + ); + }; + + // @ts-expect-error + IdentifierArray.prototype.arrayContentWillChange = function () { + deprecateArrayLike( + this.DEPRECATED_CLASS_NAME, + 'arrayContentWillChange', + 'derived state or reacting at the change source' + ); + }; + + // @ts-expect-error + IdentifierArray.prototype.arrayContentDidChange = function () { + deprecateArrayLike( + this.DEPRECATED_CLASS_NAME, + 'arrayContentDidChange', + 'derived state or reacting at the change source.' + ); + }; + + // @ts-expect-error adding MutableArray method + IdentifierArray.prototype.reject = function (callback, target?: unknown) { + deprecateArrayLike(this.DEPRECATED_CLASS_NAME, 'reject', 'filter'); + assert('`reject` expects a function as first argument.', typeof callback === 'function'); + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return this.filter((...args) => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access + return !callback.apply(target, args); + }); + }; + + // @ts-expect-error adding MutableArray method + IdentifierArray.prototype.rejectBy = function (key: string, value?: unknown) { + deprecateArrayLike(this.DEPRECATED_CLASS_NAME, 'rejectBy', 'filter'); + if (arguments.length === 2) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return this.filter((record) => { + return get(record, key) !== value; + }); + } + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return this.filter((record) => { + return !get(record, key); + }); + }; + + // @ts-expect-error adding MutableArray method + IdentifierArray.prototype.setEach = function (key: string, value: unknown) { + deprecateArrayLike(this.DEPRECATED_CLASS_NAME, 'setEach', 'forEach'); + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + this.forEach((item) => set(item, key, value)); + }; + + // @ts-expect-error adding MutableArray method + IdentifierArray.prototype.uniq = function () { + deprecateArrayLike(this.DEPRECATED_CLASS_NAME, 'uniq', 'filter'); + // all current managed arrays are already enforced as unique + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return this.slice(); + }; + + // @ts-expect-error + IdentifierArray.prototype.uniqBy = function (key: string) { + deprecateArrayLike(this.DEPRECATED_CLASS_NAME, 'uniqBy', 'filter'); + // all current managed arrays are already enforced as unique + const seen = new Set(); + const result: OpaqueRecordInstance[] = []; + this.forEach((item) => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const value = get(item, key); + if (seen.has(value)) { + return; + } + seen.add(value); + result.push(item); + }); + return result; + }; + + // @ts-expect-error adding MutableArray method + IdentifierArray.prototype.without = function (value: OpaqueRecordInstance) { + deprecateArrayLike(this.DEPRECATED_CLASS_NAME, 'without', 'slice'); + const newArr = this.slice(); + const index = this.indexOf(value); + if (index !== -1) { + newArr.splice(index, 1); + } + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return newArr; + }; + + // @ts-expect-error + IdentifierArray.prototype.firstObject = null; + // @ts-expect-error + IdentifierArray.prototype.lastObject = null; +} type PromiseProxyRecord = { then(): void; content: OpaqueRecordInstance | null | undefined }; @@ -601,11 +1115,25 @@ function assertRecordPassedToHasMany(record: OpaqueRecordInstance | PromiseProxy ); } -function extractIdentifierFromRecord(record: PromiseProxyRecord | OpaqueRecordInstance | null) { - if (!record) { +function extractIdentifierFromRecord(recordOrPromiseRecord: PromiseProxyRecord | OpaqueRecordInstance | null) { + if (!recordOrPromiseRecord) { return null; } - assertRecordPassedToHasMany(record); - return recordIdentifierFor(record); + if (isPromiseRecord(recordOrPromiseRecord)) { + const content = recordOrPromiseRecord.content; + assert( + 'You passed in a promise that did not originate from an EmberData relationship. You can only pass promises that come from a belongsTo relationship.', + content !== undefined && content !== null + ); + assertRecordPassedToHasMany(content); + return recordIdentifierFor(content); + } + + assertRecordPassedToHasMany(recordOrPromiseRecord); + return recordIdentifierFor(recordOrPromiseRecord); +} + +function isPromiseRecord(record: PromiseProxyRecord | OpaqueRecordInstance): record is PromiseProxyRecord { + return Boolean(typeof record === 'object' && record && 'then' in record); } diff --git a/packages/store/src/-private/store-service.ts b/packages/store/src/-private/store-service.ts index a1c5cd98917..00323a16347 100644 --- a/packages/store/src/-private/store-service.ts +++ b/packages/store/src/-private/store-service.ts @@ -10,7 +10,11 @@ import type RequestManager from '@ember-data/request'; import type { Future } from '@ember-data/request'; import { LOG_PAYLOADS, LOG_REQUESTS } from '@warp-drive/build-config/debugging'; import { + DEPRECATE_HAS_RECORD, + DEPRECATE_PROMISE_PROXIES, DEPRECATE_STORE_EXTENDS_EMBER_OBJECT, + DEPRECATE_STORE_FIND, + DISABLE_6X_DEPRECATIONS, ENABLE_LEGACY_SCHEMA_SERVICE, } from '@warp-drive/build-config/deprecations'; import { DEBUG, TESTING } from '@warp-drive/build-config/env'; @@ -57,6 +61,7 @@ import { CacheManager } from './managers/cache-manager'; import NotificationManager from './managers/notification-manager'; import { RecordArrayManager } from './managers/record-array-manager'; import { RequestPromise, RequestStateService } from './network/request-cache'; +import { promiseArray, promiseObject } from './proxies/promise-proxies'; import type { Collection, IdentifierArray } from './record-arrays/identifier-array'; import { coerceId, ensureStringId } from './utils/coerce-id'; import { constructResource } from './utils/construct-resource'; @@ -215,7 +220,7 @@ const app = new EmberApp(defaults, { }); \`\`\` `, - false, + /* inline-macro-config */ DISABLE_6X_DEPRECATIONS, { id: 'ember-data:deprecate-store-extends-ember-object', until: '6.0', @@ -486,9 +491,12 @@ export class Store extends BaseClass { * import Fetch from '@ember-data/request/fetch'; * * class extends Store { - * requestManager = new RequestManager() - * .use([Fetch]) - * .useCache(CacheHandler); + * constructor() { + * super(...arguments); + * this.requestManager = new RequestManager(); + * this.requestManager.use([Fetch]); + * this.requestManager.useCache(CacheHandler); + * } * } * ``` * @@ -743,11 +751,11 @@ export class Store extends BaseClass { const opts: { store: Store; disableTestWaiter?: boolean; - [EnableHydration]: boolean; + [EnableHydration]: true; records?: StableRecordIdentifier[]; } = { store: this, - [EnableHydration]: requestConfig[EnableHydration] ?? true, + [EnableHydration]: true, }; if (requestConfig.records) { @@ -1018,6 +1026,60 @@ export class Store extends BaseClass { } } + /** + @method find + @param {String} modelName + @param {String|Integer} id + @param {Object} options + @return {Promise} promise + @deprecated + @private + */ + find(modelName: string, id: string | number, options?: FindRecordOptions): Promise { + if (DEBUG) { + assertDestroyingStore(this, 'find'); + } + // The default `model` hook in Route calls `find(modelName, id)`, + // that's why we have to keep this method around even though `findRecord` is + // the public way to get a record by modelName and id. + assert( + `Using store.find(type) has been removed. Use store.findAll(modelName) to retrieve all records for a given type.`, + arguments.length !== 1 + ); + assert( + `Calling store.find(modelName, id, { preload: preload }) is no longer supported. Use store.findRecord(modelName, id, { preload: preload }) instead.`, + !options + ); + assert(`You need to pass the model name and id to the store's find method`, arguments.length === 2); + assert( + `You cannot pass '${id}' as id to the store's find method`, + typeof id === 'string' || typeof id === 'number' + ); + assert( + `Calling store.find() with a query object is no longer supported. Use store.query() instead.`, + typeof id !== 'object' + ); + assert( + `Passing classes to store methods has been removed. Please pass a dasherized string instead of ${modelName}`, + typeof modelName === 'string' + ); + + if (DEPRECATE_STORE_FIND) { + deprecate( + `Using store.find is deprecated, use store.findRecord instead. Likely this means you are relying on the implicit store fetching behavior of routes unknowingly.`, + false, + { + id: 'ember-data:deprecate-store-find', + since: { available: '4.5', enabled: '4.5' }, + for: 'ember-data', + until: '5.0', + } + ); + return this.findRecord(modelName, id); + } + assert(`store.find has been removed. Use store.findRecord instead.`); + } + /** This method returns a record for a given identifier or type and id combination. @@ -1424,6 +1486,14 @@ export class Store extends BaseClass { cacheOptions: { [SkipCache]: true }, }); + if (DEPRECATE_PROMISE_PROXIES) { + return promiseObject( + promise.then((document) => { + return document.content; + }) + ); + } + return promise.then((document) => { return document.content; }); @@ -1574,6 +1644,54 @@ export class Store extends BaseClass { return isLoaded ? (this._instanceCache.getRecord(stableIdentifier) as T) : null; } + /** + This method returns true if a record for a given modelName and id is already + loaded in the store. Use this function to know beforehand if a findRecord() + will result in a request or that it will be a cache hit. + + Example + + ```javascript + store.hasRecordForId('post', 1); // false + store.findRecord('post', 1).then(function() { + store.hasRecordForId('post', 1); // true + }); + ``` + + @method hasRecordForId + @deprecated + @public + @param {String} modelName + @param {(String|Integer)} id + @return {Boolean} + */ + hasRecordForId(modelName: string, id: string | number): boolean { + if (DEPRECATE_HAS_RECORD) { + deprecate(`store.hasRecordForId has been deprecated in favor of store.peekRecord`, false, { + id: 'ember-data:deprecate-has-record-for-id', + since: { available: '4.5', enabled: '4.5' }, + until: '5.0', + for: 'ember-data', + }); + if (DEBUG) { + assertDestroyingStore(this, 'hasRecordForId'); + } + assert(`You need to pass a model name to the store's hasRecordForId method`, modelName); + assert( + `Passing classes to store methods has been removed. Please pass a dasherized string instead of ${modelName}`, + typeof modelName === 'string' + ); + + const type = normalizeModelName(modelName); + const trueId = ensureStringId(id); + const resource = { type, id: trueId }; + + const identifier = this.identifierCache.peekRecordIdentifier(resource); + return Boolean(identifier && this._instanceCache.recordIsLoaded(identifier)); + } + assert(`store.hasRecordForId has been removed`); + } + /** This method delegates a query to the adapter. This is the one place where adapter-level semantics are exposed to the application. @@ -1648,6 +1766,9 @@ export class Store extends BaseClass { cacheOptions: { [SkipCache]: true }, }); + if (DEPRECATE_PROMISE_PROXIES) { + return promiseArray(promise.then((document) => document.content)) as unknown as Promise; + } return promise.then((document) => document.content); } @@ -1776,6 +1897,10 @@ export class Store extends BaseClass { cacheOptions: { [SkipCache]: true }, }); + if (DEPRECATE_PROMISE_PROXIES) { + return promiseObject(promise.then((document) => document.content)); + } + return promise.then((document) => document.content); } @@ -1978,6 +2103,10 @@ export class Store extends BaseClass { cacheOptions: { [SkipCache]: true }, }); + if (DEPRECATE_PROMISE_PROXIES) { + return promiseArray(promise.then((document) => document.content)) as unknown as Promise>; + } + return promise.then((document) => document.content); } @@ -2389,39 +2518,51 @@ export class Store extends BaseClass { if (ENABLE_LEGACY_SCHEMA_SERVICE) { Store.prototype.getSchemaDefinitionService = function (): SchemaService { assert(`You must registerSchemaDefinitionService with the store to use custom model classes`, this._schema); - deprecate(`Use \`store.schema\` instead of \`store.getSchemaDefinitionService()\``, false, { - id: 'ember-data:schema-service-updates', - until: '6.0', - for: 'ember-data', - since: { - available: '4.13', - enabled: '5.4', - }, - }); + deprecate( + `Use \`store.schema\` instead of \`store.getSchemaDefinitionService()\``, + /* inline-macro-config */ DISABLE_6X_DEPRECATIONS, + { + id: 'ember-data:schema-service-updates', + until: '6.0', + for: 'ember-data', + since: { + available: '4.13', + enabled: '5.4', + }, + } + ); return this._schema; }; Store.prototype.registerSchemaDefinitionService = function (schema: SchemaService) { - deprecate(`Use \`store.createSchemaService\` instead of \`store.registerSchemaDefinitionService()\``, false, { - id: 'ember-data:schema-service-updates', - until: '6.0', - for: 'ember-data', - since: { - available: '4.13', - enabled: '5.4', - }, - }); + deprecate( + `Use \`store.createSchemaService\` instead of \`store.registerSchemaDefinitionService()\``, + /* inline-macro-config */ DISABLE_6X_DEPRECATIONS, + { + id: 'ember-data:schema-service-updates', + until: '6.0', + for: 'ember-data', + since: { + available: '4.13', + enabled: '5.4', + }, + } + ); this._schema = schema; }; Store.prototype.registerSchema = function (schema: SchemaService) { - deprecate(`Use \`store.createSchemaService\` instead of \`store.registerSchema()\``, false, { - id: 'ember-data:schema-service-updates', - until: '6.0', - for: 'ember-data', - since: { - available: '4.13', - enabled: '5.4', - }, - }); + deprecate( + `Use \`store.createSchemaService\` instead of \`store.registerSchema()\``, + /* inline-macro-config */ DISABLE_6X_DEPRECATIONS, + { + id: 'ember-data:schema-service-updates', + until: '6.0', + for: 'ember-data', + since: { + available: '4.13', + enabled: '5.4', + }, + } + ); this._schema = schema; }; } @@ -2531,5 +2672,33 @@ function extractIdentifierFromRecord(recordOrPromiseRecord: PromiseProxyRecord | } const extract = recordIdentifierFor; + if (DEPRECATE_PROMISE_PROXIES) { + if (isPromiseRecord(recordOrPromiseRecord)) { + const content = recordOrPromiseRecord.content; + assert( + 'You passed in a promise that did not originate from an EmberData relationship. You can only pass promises that come from a belongsTo or hasMany relationship to the get call.', + content !== undefined + ); + deprecate( + `You passed in a PromiseProxy to a Relationship API that now expects a resolved value. await the value before setting it.`, + false, + { + id: 'ember-data:deprecate-promise-proxies', + until: '5.0', + since: { + enabled: '4.7', + available: '4.7', + }, + for: 'ember-data', + } + ); + return content ? extract(content) : null; + } + } + return extract(recordOrPromiseRecord); } + +function isPromiseRecord(record: PromiseProxyRecord | OpaqueRecordInstance): record is PromiseProxyRecord { + return typeof record === 'object' && !!record && 'then' in record && typeof record.then === 'function'; +} diff --git a/packages/store/src/-private/store-service.type-test.ts b/packages/store/src/-private/store-service.type-test.ts index f98c61a029c..5ee28e360c2 100644 --- a/packages/store/src/-private/store-service.type-test.ts +++ b/packages/store/src/-private/store-service.type-test.ts @@ -33,17 +33,6 @@ import { Store } from './store-service'; ) ).toEqualTypeOf(); - const result2 = store.peekRecord({ type: 'user', id: '1' }); - - expectTypeOf(result2).toBeUnknown(); - expectTypeOf( - store.peekRecord({ - // @ts-expect-error since there is no brand, this should error - type: 'user', - id: '1', - }) - ).toEqualTypeOf(); - expectTypeOf(store.peekRecord('user', '1')).toEqualTypeOf(); expectTypeOf( store.peekRecord( @@ -52,15 +41,6 @@ import { Store } from './store-service'; '1' ) ).toEqualTypeOf(); - - expectTypeOf(store.peekRecord({ type: 'user', id: '1' })).toEqualTypeOf(); - expectTypeOf( - store.peekRecord({ - // @ts-expect-error should error since this does not match the brand - type: 'users', - id: '1', - }) - ).toEqualTypeOf(); } ////////////////////////////////// diff --git a/packages/store/src/-private/utils/coerce-id.ts b/packages/store/src/-private/utils/coerce-id.ts index ceb1f6f8dda..2be265ef878 100644 --- a/packages/store/src/-private/utils/coerce-id.ts +++ b/packages/store/src/-private/utils/coerce-id.ts @@ -4,7 +4,7 @@ import { deprecate } from '@ember/debug'; -import { DEPRECATE_NON_STRICT_ID } from '@warp-drive/build-config/deprecations'; +import { DEPRECATE_NON_STRICT_ID, DISABLE_6X_DEPRECATIONS } from '@warp-drive/build-config/deprecations'; import { assert } from '@warp-drive/build-config/macros'; // Used by the store to normalize IDs entering the store. Despite the fact @@ -28,7 +28,7 @@ export function coerceId(id: unknown): string | null { `The resource id '<${typeof id}> ${String( id )} ' is not normalized. Update your application code to use '${JSON.stringify(normalized)}' instead.`, - normalized === id, + /* inline-macro-config */ DISABLE_6X_DEPRECATIONS ? true : normalized === id, { id: 'ember-data:deprecate-non-strict-id', until: '6.0', diff --git a/packages/store/src/-private/utils/normalize-model-name.ts b/packages/store/src/-private/utils/normalize-model-name.ts index 91e12a8e829..dba54a8b1c1 100644 --- a/packages/store/src/-private/utils/normalize-model-name.ts +++ b/packages/store/src/-private/utils/normalize-model-name.ts @@ -1,7 +1,7 @@ import { deprecate } from '@ember/debug'; import { dasherize } from '@ember-data/request-utils/string'; -import { DEPRECATE_NON_STRICT_TYPES } from '@warp-drive/build-config/deprecations'; +import { DEPRECATE_NON_STRICT_TYPES, DISABLE_6X_DEPRECATIONS } from '@warp-drive/build-config/deprecations'; export function normalizeModelName(type: string): string { if (DEPRECATE_NON_STRICT_TYPES) { @@ -9,7 +9,7 @@ export function normalizeModelName(type: string): string { deprecate( `The resource type '${type}' is not normalized. Update your application code to use '${result}' instead of '${type}'.`, - result === type, + /* inline-macro-config */ DISABLE_6X_DEPRECATIONS ? true : result === type, { id: 'ember-data:deprecate-non-strict-types', until: '6.0', diff --git a/packages/store/src/-types/q/cache-capabilities-manager.ts b/packages/store/src/-types/q/cache-capabilities-manager.ts index e9ff955b4ec..43abf1eb8e1 100644 --- a/packages/store/src/-types/q/cache-capabilities-manager.ts +++ b/packages/store/src/-types/q/cache-capabilities-manager.ts @@ -108,12 +108,12 @@ export type CacheCapabilitiesManager = { * @param {string|undefined} key * @public */ - notifyChange(identifier: StableRecordIdentifier, namespace: 'added' | 'removed', key: null): void; - notifyChange(identifier: StableDocumentIdentifier, namespace: 'added' | 'updated' | 'removed', key: null): void; - notifyChange(identifier: StableRecordIdentifier, namespace: NotificationType, key: string | null): void; + notifyChange(identifier: StableRecordIdentifier, namespace: 'added' | 'removed'): void; + notifyChange(identifier: StableDocumentIdentifier, namespace: 'added' | 'updated' | 'removed'): void; + notifyChange(identifier: StableRecordIdentifier, namespace: NotificationType, key?: string): void; notifyChange( identifier: StableRecordIdentifier | StableDocumentIdentifier, namespace: NotificationType | 'added' | 'removed' | 'updated', - key: string | null + key?: string ): void; }; diff --git a/packages/store/src/index.ts b/packages/store/src/index.ts index 3370e82e74f..c30dab7d595 100644 --- a/packages/store/src/index.ts +++ b/packages/store/src/index.ts @@ -82,8 +82,11 @@ * import Fetch from '@ember-data/request/fetch'; * * export default class extends Store { - * requestManager = new RequestManager() - * .use([Fetch]); + * constructor() { + * super(...arguments); + * this.requestManager = new RequestManager(); + * this.requestManager.use([Fetch]); + * } * } * ``` * @@ -183,6 +186,7 @@ export { Store as default, type StoreRequestContext, CacheHandler, + normalizeModelName, type Document, type CachePolicy, type StoreRequestInput, diff --git a/packages/store/vite.config.mjs b/packages/store/vite.config.mjs index ca9aa0e126f..ba7aaf03493 100644 --- a/packages/store/vite.config.mjs +++ b/packages/store/vite.config.mjs @@ -1,6 +1,20 @@ import { createConfig } from '@warp-drive/internal-config/vite/config.js'; -export const externals = ['@ember/runloop', '@ember/object', '@ember/debug']; +export const externals = [ + 'ember', + '@ember/object/computed', + '@ember/object/promise-proxy-mixin', + '@ember/object/proxy', + '@ember/array/proxy', + '@ember/application', + '@ember/debug', + '@ember/owner', + '@ember/utils', + '@ember/runloop', + '@ember/object', + '@ember/debug', +]; + export const entryPoints = ['./src/index.ts', './src/types.ts', './src/-private.ts']; export default createConfig( diff --git a/packages/tracking/README.md b/packages/tracking/README.md index e26334bebca..9e0d2f4f934 100644 --- a/packages/tracking/README.md +++ b/packages/tracking/README.md @@ -33,3 +33,14 @@ pnpm add @ember-data/tracking - ![NPM LTS Version](https://img.shields.io/npm/v/%40ember-data/tracking/lts?label=%40lts&color=0096FF) - ![NPM LTS 4.12 Version](https://img.shields.io/npm/v/%40ember-data/tracking/lts-4-12?label=%40lts-4-12&color=bbbbbb) + +## About + +> Note: This is a V2 Addon, but we have intentionally configured it to act and report as a V1 Addon due +to bugs with ember-auto-import. +> +> We can remove the V1 tag if ember-auto-import will no longer attempt +to load V2 addons or if it is fixed to work with V1 addons with custom addon trees and also dedupes modules for test apps. +> +> You can still consume this as a normal library. +> In other projects. diff --git a/packages/tracking/package.json b/packages/tracking/package.json index 7153d31c9da..9cfcf5e0924 100644 --- a/packages/tracking/package.json +++ b/packages/tracking/package.json @@ -1,7 +1,7 @@ { "name": "@ember-data/tracking", "description": "Tracking Primitives for controlling change notification of Tracked properties when working with EmberData", - "version": "5.4.0-alpha.136", + "version": "4.13.0-alpha.3", "private": false, "license": "MIT", "author": "Chris Thoburn ", @@ -30,7 +30,7 @@ } }, "dependencies": { - "@embroider/macros": "^1.16.10", + "@embroider/macros": "^1.16.6", "@warp-drive/build-config": "workspace:*" }, "peerDependencies": { diff --git a/packages/unpublished-eslint-rules/package.json b/packages/unpublished-eslint-rules/package.json index 26050a0ca89..342e23a428c 100644 --- a/packages/unpublished-eslint-rules/package.json +++ b/packages/unpublished-eslint-rules/package.json @@ -1,7 +1,7 @@ { "name": "eslint-plugin-ember-data-internal", "main": "./src/index.js", - "version": "5.4.0-alpha.136", + "version": "4.13.0-alpha.3", "private": true, "repository": { "type": "git", diff --git a/packages/unpublished-test-infra/package.json b/packages/unpublished-test-infra/package.json index 903852248fa..2fcb888e517 100644 --- a/packages/unpublished-test-infra/package.json +++ b/packages/unpublished-test-infra/package.json @@ -1,6 +1,6 @@ { "name": "@ember-data/unpublished-test-infra", - "version": "5.4.0-alpha.136", + "version": "4.13.0-alpha.3", "private": true, "description": "The default blueprint for ember-data private packages.", "keywords": [ @@ -76,7 +76,7 @@ "@ember-data/tracking": "workspace:*", "@warp-drive/diagnostic": "workspace:*", "@warp-drive/core-types": "workspace:*", - "@ember/test-helpers": "3.3.0 || ^4.0.4 || ^5.1.0" + "@ember/test-helpers": "3.3.0 || ^4.0.4" }, "peerDependenciesMeta": { "qunit": { @@ -90,7 +90,7 @@ } }, "dependencies": { - "@embroider/macros": "^1.16.10", + "@embroider/macros": "^1.16.6", "chalk": "^4.1.2", "qunit": "^2.20.1", "semver": "^7.6.3", @@ -101,7 +101,7 @@ "@babel/plugin-transform-typescript": "^7.24.5", "@babel/preset-env": "^7.24.5", "@babel/preset-typescript": "^7.24.1", - "@ember/test-helpers": "5.1.0", + "@ember/test-helpers": "4.0.4", "@glimmer/component": "^1.1.2", "@types/semver": "^7.5.8", "@types/qunit": "2.19.10", diff --git a/release/core/publish/steps/generate-mirror-tarballs.ts b/release/core/publish/steps/generate-mirror-tarballs.ts index fc784493916..c5072e9366d 100644 --- a/release/core/publish/steps/generate-mirror-tarballs.ts +++ b/release/core/publish/steps/generate-mirror-tarballs.ts @@ -70,13 +70,6 @@ export async function generateMirrorTarballs( newContents = newContents.replace(new RegExp(`"${from}`, 'g'), `"${to}`); } - // macros.globalConfig['WarpDrive'] - // macros.setGlobalConfig(import.meta.filename, 'WarpDrive', finalizedConfig); - if (strat.name === '@warp-drive/build-config') { - newContents = newContents.replace(new RegExp(`'WarpDrive'`, 'g'), `'WarpDriveMirror'`); - } - - newContents = newContents.replace(/getGlobalConfig\(\)\.WarpDrive\./g, 'getGlobalConfig().WarpDriveMirror.'); newContents = newContents.replace(new RegExp(`'@ember-data/'`, 'g'), `'@ember-data-mirror/'`); newContents = newContents.replace(new RegExp(`"@ember-data/"`, 'g'), `"@ember-data-mirror/"`); diff --git a/release/core/publish/steps/generate-tarballs.ts b/release/core/publish/steps/generate-tarballs.ts index 4496f5489ed..dd5713f6b77 100644 --- a/release/core/publish/steps/generate-tarballs.ts +++ b/release/core/publish/steps/generate-tarballs.ts @@ -307,34 +307,8 @@ async function convertTypesToModules(pkg: Package, subdir: 'unstable-preview-typ } } -function exposeTypes(pkg: Package, subdir: 'unstable-preview-types' | 'preview-types' | 'types') { - if (pkg.pkgData.exports) { - /** - * Allows tsconfig.json#compilerOptions#types to use import paths, - * rather than file paths (there are no file path guarantees for any given package manager) - */ - pkg.pkgData.exports[`./${subdir}`] = { - /** - * No default, import, or require here, because there are no actual modules to import. - */ - types: `./${subdir}/index.d.ts`, - }; - - /** - * For older tsconfig.json settings - */ - pkg.pkgData.typesVersions = { - // very loose TS version - '*': { - [subdir]: [`./${subdir}`], - }, - }; - } -} - async function makeTypesAlpha(pkg: Package) { scrubTypesFromExports(pkg); - exposeTypes(pkg, 'unstable-preview-types'); // enforce that the correct types directory is present const present = new Set(pkg.pkgData.files); @@ -361,7 +335,6 @@ async function makeTypesAlpha(pkg: Package) { async function makeTypesBeta(pkg: Package) { scrubTypesFromExports(pkg); - exposeTypes(pkg, 'preview-types'); // enforce that the correct types directory is present const present = new Set(pkg.pkgData.files); diff --git a/release/strategy.json b/release/strategy.json index b18d962e679..20149cf59a2 100644 --- a/release/strategy.json +++ b/release/strategy.json @@ -78,10 +78,10 @@ "mirrorPublish": false }, "@warp-drive/ember": { - "stage": "stable", + "stage": "alpha", "types": "alpha", "typesPublish": false, - "mirrorPublish": true + "mirrorPublish": false }, "@warp-drive/schema": { "stage": "alpha", @@ -89,12 +89,6 @@ "typesPublish": false, "mirrorPublish": false }, - "@warp-drive/experiments": { - "stage": "beta", - "types": "private", - "typesPublish": false, - "mirrorPublish": true - }, "@warp-drive/schema-record": { "stage": "stable", "types": "alpha", diff --git a/release/utils/package.ts b/release/utils/package.ts index 150cb49a7dd..4f01e9e0b51 100644 --- a/release/utils/package.ts +++ b/release/utils/package.ts @@ -59,7 +59,6 @@ export type PACKAGEJSON = { scripts?: Record; files?: string[]; exports?: ExportConfig; - typesVersions?: { [tsVersion: string]: { [relativeImportPath: string]: string[] } }; 'ember-addon'?: { main?: 'addon-main.js'; type?: 'addon'; diff --git a/tests/blueprints/package.json b/tests/blueprints/package.json index e483d6d3927..5f8c7277cc2 100644 --- a/tests/blueprints/package.json +++ b/tests/blueprints/package.json @@ -1,6 +1,6 @@ { "name": "blueprint-tests", - "version": "5.4.0-alpha.136", + "version": "4.13.0-alpha.3", "private": true, "description": "Provides tests for blueprints", "repository": { @@ -77,7 +77,7 @@ "@ember-data/tracking": "workspace:*", "@ember-data/unpublished-test-infra": "workspace:*", "@ember/edition-utils": "^1.2.0", - "@ember/test-helpers": "5.1.0", + "@ember/test-helpers": "4.0.4", "@ember/test-waiters": "^3.1.0", "@glimmer/component": "^1.1.2", "@glimmer/tracking": "^1.1.2", @@ -86,7 +86,7 @@ "ember-cli": "~5.12.0", "ember-cli-blueprint-test-helpers": "^0.19.2", "ember-source": "~5.12.0", - "mocha": "^10.8.2", + "mocha": "^10.7.3", "pnpm-sync-dependencies-meta-injected": "0.0.14", "silent-error": "^1.1.1" }, diff --git a/tests/builders/package.json b/tests/builders/package.json index 05978262b09..24a6603870e 100644 --- a/tests/builders/package.json +++ b/tests/builders/package.json @@ -1,6 +1,6 @@ { "name": "builders-test-app", - "version": "5.4.0-alpha.136", + "version": "4.13.0-alpha.3", "private": true, "description": "Provides tests for URL and Request Building Capabilities", "keywords": [], @@ -88,16 +88,16 @@ "@ember/edition-utils": "^1.2.0", "@ember/optional-features": "^2.1.0", "@ember/string": "^3.1.1", - "@ember/test-helpers": "5.1.0", + "@ember/test-helpers": "4.0.4", "@ember/test-waiters": "^3.1.0", - "@embroider/addon-shim": "^1.9.0", + "@embroider/addon-shim": "^1.8.9", "@glimmer/component": "^1.1.2", "@glimmer/tracking": "^1.1.2", "@warp-drive/core-types": "workspace:*", "@warp-drive/diagnostic": "workspace:*", "@warp-drive/internal-config": "workspace:*", "@warp-drive/build-config": "workspace:*", - "ember-auto-import": "2.10.0", + "ember-auto-import": "^2.8.1", "ember-cli": "~5.12.0", "ember-cli-babel": "^8.2.0", "ember-cli-dependency-checker": "^3.3.2", diff --git a/tests/docs/fixtures/expected.js b/tests/docs/fixtures/expected.js index 61097015b4d..90248c6e9f3 100644 --- a/tests/docs/fixtures/expected.js +++ b/tests/docs/fixtures/expected.js @@ -91,6 +91,7 @@ module.exports = { '(private) @ember-data/store RecordArray#store', '(private) @ember-data/store Snapshot#constructor', '(private) @ember-data/store Store#_push', + '(private) @ember-data/store Store#find', '(private) @ember-data/store Store#init', '(public) @ember-data/active-record/request @ember-data/active-record/request#createRecord', '(public) @ember-data/active-record/request @ember-data/active-record/request#deleteRecord', @@ -125,6 +126,8 @@ module.exports = { '(public) @ember-data/adapter BuildURLMixin#urlForQuery', '(public) @ember-data/adapter BuildURLMixin#urlForQueryRecord', '(public) @ember-data/adapter BuildURLMixin#urlForUpdateRecord', + '(public) @ember-data/adapter/error @ember-data/adapter/error#errorsArrayToHash', + '(public) @ember-data/adapter/error @ember-data/adapter/error#errorsHashToArray', '(public) @ember-data/adapter/json-api JSONAPIAdapter#buildQuery', '(public) @ember-data/adapter/json-api JSONAPIAdapter#coalesceFindRequests', '(public) @ember-data/adapter/rest RESTAdapter#buildQuery', @@ -250,6 +253,7 @@ module.exports = { '(public) @ember-data/legacy-compat SnapshotRecordArray#length', '(public) @ember-data/legacy-compat SnapshotRecordArray#modelName', '(public) @ember-data/legacy-compat SnapshotRecordArray#snapshots', + '(public) @ember-data/legacy-compat SnapshotRecordArray#type', '(public) @ember-data/legacy-compat/builders @ember-data/legacy-compat/builders#findAll', '(public) @ember-data/legacy-compat/builders @ember-data/legacy-compat/builders#findRecord', '(public) @ember-data/legacy-compat/builders @ember-data/legacy-compat/builders#query', @@ -437,6 +441,7 @@ module.exports = { '(public) @ember-data/serializer/rest RESTSerializer#serialize', '(public) @ember-data/serializer/rest RESTSerializer#serializeIntoHash', '(public) @ember-data/serializer/rest RESTSerializer#serializePolymorphicType', + '(public) @ember-data/store @ember-data/store#normalizeModelName', '(public) @ember-data/store @ember-data/store#recordIdentifierFor', '(public) @ember-data/store @ember-data/store#setIdentifierForgetMethod', '(public) @ember-data/store @ember-data/store#setIdentifierGenerationMethod', @@ -528,6 +533,7 @@ module.exports = { '(public) @ember-data/store NotificationManager#unsubscribe', '(public) @ember-data/store RecordArray#isUpdating', '(public) @ember-data/store RecordArray#save', + '(public) @ember-data/store RecordArray#type', '(public) @ember-data/store RecordArray#update', '(public) @ember-data/store RecordReference#id', '(public) @ember-data/store RecordReference#identifier', @@ -553,6 +559,7 @@ module.exports = { '(public) @ember-data/store Snapshot#modelName', '(public) @ember-data/store Snapshot#record', '(public) @ember-data/store Snapshot#serialize', + '(public) @ember-data/store Snapshot#type', '(public) @ember-data/store StableRecordIdentifier#id', '(public) @ember-data/store StableRecordIdentifier#lid', '(public) @ember-data/store StableRecordIdentifier#type', @@ -566,6 +573,7 @@ module.exports = { '(public) @ember-data/store Store#getReference', '(public) @ember-data/store Store#getRequestStateService', '(public) @ember-data/store Store#getSchemaDefinitionService', + '(public) @ember-data/store Store#hasRecordForId', '(public) @ember-data/store Store#identifierCache', '(public) @ember-data/store Store#identifierCache', '(public) @ember-data/store Store#instantiateRecord (hook)', @@ -595,23 +603,39 @@ module.exports = { '(public) @warp-drive/build-config/debugging DebugLogging#LOG_GRAPH', '(public) @warp-drive/build-config/debugging DebugLogging#LOG_IDENTIFIERS', '(public) @warp-drive/build-config/debugging DebugLogging#LOG_INSTANCE_CACHE', - '(public) @warp-drive/build-config/debugging DebugLogging#LOG_METRIC_COUNTS', '(public) @warp-drive/build-config/debugging DebugLogging#LOG_MUTATIONS', '(public) @warp-drive/build-config/debugging DebugLogging#LOG_NOTIFICATIONS', '(public) @warp-drive/build-config/debugging DebugLogging#LOG_OPERATIONS', '(public) @warp-drive/build-config/debugging DebugLogging#LOG_PAYLOADS', '(public) @warp-drive/build-config/debugging DebugLogging#LOG_REQUEST_STATUS', '(public) @warp-drive/build-config/debugging DebugLogging#LOG_REQUESTS', + '(public) @warp-drive/build-config/deprecations CurrentDeprecations#DEPRECATE_A_USAGE', + '(public) @warp-drive/build-config/deprecations CurrentDeprecations#DEPRECATE_ARRAY_LIKE', '(public) @warp-drive/build-config/deprecations CurrentDeprecations#DEPRECATE_COMPUTED_CHAINS', + '(public) @warp-drive/build-config/deprecations CurrentDeprecations#DEPRECATE_EARLY_STATIC', '(public) @warp-drive/build-config/deprecations CurrentDeprecations#DEPRECATE_EMBER_INFLECTOR', - '(public) @warp-drive/build-config/deprecations CurrentDeprecations#DEPRECATE_LEGACY_IMPORTS', + '(public) @warp-drive/build-config/deprecations CurrentDeprecations#DEPRECATE_HAS_RECORD', + '(public) @warp-drive/build-config/deprecations CurrentDeprecations#DEPRECATE_HELPERS', + '(public) @warp-drive/build-config/deprecations CurrentDeprecations#DEPRECATE_JSON_API_FALLBACK', '(public) @warp-drive/build-config/deprecations CurrentDeprecations#DEPRECATE_MANY_ARRAY_DUPLICATES', + '(public) @warp-drive/build-config/deprecations CurrentDeprecations#DEPRECATE_MODEL_REOPEN', + '(public) @warp-drive/build-config/deprecations CurrentDeprecations#DEPRECATE_NON_EXPLICIT_POLYMORPHISM', '(public) @warp-drive/build-config/deprecations CurrentDeprecations#DEPRECATE_NON_STRICT_ID', '(public) @warp-drive/build-config/deprecations CurrentDeprecations#DEPRECATE_NON_STRICT_TYPES', '(public) @warp-drive/build-config/deprecations CurrentDeprecations#DEPRECATE_NON_UNIQUE_PAYLOADS', + '(public) @warp-drive/build-config/deprecations CurrentDeprecations#DEPRECATE_PROMISE_MANY_ARRAY_BEHAVIORS', + '(public) @warp-drive/build-config/deprecations CurrentDeprecations#DEPRECATE_PROMISE_PROXIES', '(public) @warp-drive/build-config/deprecations CurrentDeprecations#DEPRECATE_RELATIONSHIP_REMOTE_UPDATE_CLEARING_LOCAL_STATE', + '(public) @warp-drive/build-config/deprecations CurrentDeprecations#DEPRECATE_RELATIONSHIPS_WITHOUT_ASYNC', + '(public) @warp-drive/build-config/deprecations CurrentDeprecations#DEPRECATE_RELATIONSHIPS_WITHOUT_INVERSE', + '(public) @warp-drive/build-config/deprecations CurrentDeprecations#DEPRECATE_RELATIONSHIPS_WITHOUT_TYPE', + '(public) @warp-drive/build-config/deprecations CurrentDeprecations#DEPRECATE_RSVP_PROMISE', + '(public) @warp-drive/build-config/deprecations CurrentDeprecations#DEPRECATE_SAVE_PROMISE_ACCESS', + '(public) @warp-drive/build-config/deprecations CurrentDeprecations#DEPRECATE_SNAPSHOT_MODEL_CLASS_ACCESS', '(public) @warp-drive/build-config/deprecations CurrentDeprecations#DEPRECATE_STORE_EXTENDS_EMBER_OBJECT', - '(public) @warp-drive/build-config/deprecations CurrentDeprecations#DISABLE_7X_DEPRECATIONS', + '(public) @warp-drive/build-config/deprecations CurrentDeprecations#DEPRECATE_STORE_FIND', + '(public) @warp-drive/build-config/deprecations CurrentDeprecations#DEPRECATE_STRING_ARG_SCHEMAS', + '(public) @warp-drive/build-config/deprecations CurrentDeprecations#DISABLE_6X_DEPRECATIONS', '(public) @warp-drive/build-config/deprecations CurrentDeprecations#ENABLE_LEGACY_SCHEMA_SERVICE', ], }; diff --git a/tests/docs/package.json b/tests/docs/package.json index 913d05a9e6b..346ba9e65a3 100644 --- a/tests/docs/package.json +++ b/tests/docs/package.json @@ -1,6 +1,6 @@ { "name": "docs-tests", - "version": "5.4.0-alpha.136", + "version": "4.13.0-alpha.3", "private": true, "description": "Provides tests for blueprints", "repository": { diff --git a/tests/ember-data__adapter/app/services/store.ts b/tests/ember-data__adapter/app/services/store.ts index 987a8e3118d..2e360e8abb2 100644 --- a/tests/ember-data__adapter/app/services/store.ts +++ b/tests/ember-data__adapter/app/services/store.ts @@ -18,7 +18,12 @@ import type { StableRecordIdentifier } from '@warp-drive/core-types'; import type { TypeFromInstance } from '@warp-drive/core-types/record'; export default class Store extends BaseStore { - requestManager = new RequestManager().use([LegacyNetworkHandler, Fetch]).useCache(CacheHandler); + constructor(args: unknown) { + super(args); + this.requestManager = new RequestManager(); + this.requestManager.use([LegacyNetworkHandler, Fetch]); + this.requestManager.useCache(CacheHandler); + } createSchemaService(): ReturnType { return buildSchema(this); diff --git a/tests/ember-data__adapter/package.json b/tests/ember-data__adapter/package.json index 589bfb331c0..0fb10aa0f81 100644 --- a/tests/ember-data__adapter/package.json +++ b/tests/ember-data__adapter/package.json @@ -1,6 +1,6 @@ { "name": "ember-data__adapter", - "version": "5.4.0-alpha.136", + "version": "4.13.0-alpha.3", "private": true, "description": "Tests for @ember-data/adapter", "repository": { @@ -85,16 +85,16 @@ "@ember-data/tracking": "workspace:*", "@ember-data/unpublished-test-infra": "workspace:*", "@ember/optional-features": "^2.1.0", - "@ember/test-helpers": "5.1.0", + "@ember/test-helpers": "4.0.4", "@ember/test-waiters": "^3.1.0 || ^4.0.0", - "@embroider/addon-shim": "^1.9.0", + "@embroider/addon-shim": "^1.8.9", "@glimmer/component": "^1.1.2", "@glimmer/tracking": "^1.1.2", "@warp-drive/core-types": "workspace:*", "@warp-drive/build-config": "workspace:*", "@warp-drive/internal-config": "workspace:*", "@warp-drive/diagnostic": "workspace:*", - "ember-auto-import": "2.10.0", + "ember-auto-import": "^2.8.1", "ember-cli": "~5.12.0", "ember-cli-babel": "^8.2.0", "ember-cli-dependency-checker": "^3.3.2", diff --git a/tests/ember-data__graph/ember-cli-build.js b/tests/ember-data__graph/ember-cli-build.js index 436ede741f6..837d9d3e6d3 100644 --- a/tests/ember-data__graph/ember-cli-build.js +++ b/tests/ember-data__graph/ember-cli-build.js @@ -20,6 +20,9 @@ module.exports = async function (defaults) { setConfig(app, __dirname, { compatWith: process.env.EMBER_DATA_FULL_COMPAT ? '99.0' : null, + deprecations: { + DISABLE_6X_DEPRECATIONS: false, + }, }); app.import('node_modules/@warp-drive/diagnostic/dist/styles/dom-reporter.css'); diff --git a/tests/ember-data__graph/package.json b/tests/ember-data__graph/package.json index 329e74c0052..24335ca3c12 100644 --- a/tests/ember-data__graph/package.json +++ b/tests/ember-data__graph/package.json @@ -1,6 +1,6 @@ { "name": "ember-data__graph", - "version": "5.4.0-alpha.136", + "version": "4.13.0-alpha.3", "private": true, "description": "Provides tests for @ember-data/graph", "keywords": [], @@ -79,17 +79,17 @@ "@ember-data/unpublished-test-infra": "workspace:*", "@ember/edition-utils": "^1.2.0", "@ember/optional-features": "^2.1.0", - "@ember/test-helpers": "5.1.0", + "@ember/test-helpers": "4.0.4", "@ember/test-waiters": "^3.1.0", - "@embroider/addon-shim": "^1.9.0", - "@embroider/macros": "^1.16.10", + "@embroider/addon-shim": "^1.8.9", + "@embroider/macros": "^1.16.6", "@glimmer/component": "^1.1.2", "@glimmer/tracking": "^1.1.2", "@warp-drive/build-config": "workspace:*", "@warp-drive/core-types": "workspace:*", "@warp-drive/internal-config": "workspace:*", "@warp-drive/diagnostic": "workspace:*", - "ember-auto-import": "2.10.0", + "ember-auto-import": "^2.8.1", "ember-cli": "~5.12.0", "ember-cli-babel": "^8.2.0", "ember-cli-dependency-checker": "^3.3.2", diff --git a/tests/ember-data__graph/tests/integration/graph/diff-preservation-test.ts b/tests/ember-data__graph/tests/integration/graph/diff-preservation-test.ts index 8cbe448ea0b..c83a198176d 100644 --- a/tests/ember-data__graph/tests/integration/graph/diff-preservation-test.ts +++ b/tests/ember-data__graph/tests/integration/graph/diff-preservation-test.ts @@ -726,165 +726,6 @@ module('Integration | Graph | Diff Preservation', function (hooks) { 'namespace apps relationship is correct' ); }); - } else { - deprecatedTest( - 'updateRelationship operation from the collection side clear local state', - { id: 'ember-data:deprecate-relationship-remote-update-clearing-local-state', count: 2, until: '6.0.0' }, - function (assert) { - // tests that Many:Many, Many:One do not clear local state from - // either side when updating the relationship from the Many side - // we set the flag on the inverse to ensure that we detect this - // from either side - const { owner } = this; - - class App extends Model { - @attr declare name: string; - @hasMany('config', { async: false, inverse: 'app' }) declare configs: Config[]; - @hasMany('namespace', { async: false, inverse: 'apps' }) declare namespaces: Namespace | null; - } - - class Namespace extends Model { - @attr declare name: string; - @hasMany('app', { async: false, inverse: 'namespaces' }) declare apps: App[]; - } - - class Config extends Model { - @attr declare name: string; - @belongsTo('app', { async: false, inverse: 'configs' }) declare app: App | null; - } - - function identifier(type: string, id: string) { - return store.identifierCache.getOrCreateRecordIdentifier({ type, id }); - } - - owner.register('model:app', App); - owner.register('model:namespace', Namespace); - owner.register('model:config', Config); - const store = owner.lookup('service:store') as unknown as Store; - const graph = graphFor(store); - const appIdentifier = identifier('app', '1'); - - // set initial state - // one app, with 4 configs and 4 namespaces - // each config belongs to the app - // each namespace has the app and 2 more apps - store._join(() => { - // setup primary app relationships - // this also convers the belongsTo side on config - graph.push({ - op: 'updateRelationship', - field: 'configs', - record: appIdentifier, - value: { - data: [ - { type: 'config', id: '1' }, - { type: 'config', id: '2' }, - { type: 'config', id: '3' }, - { type: 'config', id: '4' }, - ], - }, - }); - graph.push({ - op: 'updateRelationship', - field: 'namespaces', - record: appIdentifier, - value: { - data: [ - { type: 'namespace', id: '1' }, - { type: 'namespace', id: '2' }, - { type: 'namespace', id: '3' }, - { type: 'namespace', id: '4' }, - ], - }, - }); - // setup namespace relationships - ['1', '2', '3', '4'].forEach((id) => { - graph.push({ - op: 'updateRelationship', - field: 'apps', - record: identifier('namespace', id), - value: { - data: [ - { type: 'app', id: '1' }, - { type: 'app', id: '2' }, - { type: 'app', id: '3' }, - ], - }, - }); - }); - }); - - // mutate app:1.configs, adding config:5 - // mutate app:1.namespaces, adding namespace:5 - store._join(() => { - graph.update({ - op: 'addToRelatedRecords', - field: 'configs', - record: appIdentifier, - value: identifier('config', '5'), - }); - graph.update({ - op: 'addToRelatedRecords', - field: 'namespaces', - record: appIdentifier, - value: identifier('namespace', '5'), - }); - }); - - // push the exact same remote state to the graph again - // for app:1 - store._join(() => { - // setup primary app relationships - // this also convers the belongsTo side on config - graph.push({ - op: 'updateRelationship', - field: 'configs', - record: appIdentifier, - value: { - data: [ - { type: 'config', id: '1' }, - { type: 'config', id: '2' }, - { type: 'config', id: '3' }, - { type: 'config', id: '4' }, - ], - }, - }); - graph.push({ - op: 'updateRelationship', - field: 'namespaces', - record: appIdentifier, - value: { - data: [ - { type: 'namespace', id: '1' }, - { type: 'namespace', id: '2' }, - { type: 'namespace', id: '3' }, - { type: 'namespace', id: '4' }, - ], - }, - }); - }); - - // we should have both not err'd and still have cleared the mutated state - const { data: configs } = graph.getData(appIdentifier, 'configs'); - assert.arrayStrictEquals( - configs, - [identifier('config', '1'), identifier('config', '2'), identifier('config', '3'), identifier('config', '4')], - 'configs are correct' - ); - - const { data: namespaces } = graph.getData(appIdentifier, 'namespaces'); - assert.arrayStrictEquals( - namespaces, - [ - identifier('namespace', '1'), - identifier('namespace', '2'), - identifier('namespace', '3'), - identifier('namespace', '4'), - ], - 'namespaces are correct' - ); - } - ); } test('updateRelationship operation from the collection side does not clear local state when `resetOnRemoteUpdate: false` is set', function (assert) { diff --git a/tests/ember-data__graph/tests/test-helper.ts b/tests/ember-data__graph/tests/test-helper.ts index be934515e51..c380c8a49c8 100644 --- a/tests/ember-data__graph/tests/test-helper.ts +++ b/tests/ember-data__graph/tests/test-helper.ts @@ -17,7 +17,6 @@ configure(); setApplication(Application.create(config.APP)); void start({ tryCatch: true, - // debug: true, groupLogs: false, instrument: true, hideReport: true, diff --git a/tests/ember-data__json-api/package.json b/tests/ember-data__json-api/package.json index de95d13f11a..715334da9c0 100644 --- a/tests/ember-data__json-api/package.json +++ b/tests/ember-data__json-api/package.json @@ -1,6 +1,6 @@ { "name": "ember-data__json-api", - "version": "5.4.0-alpha.136", + "version": "4.13.0-alpha.3", "private": true, "description": "Provides tests for @ember-data/json-api", "keywords": [], @@ -83,10 +83,10 @@ "@ember-data/unpublished-test-infra": "workspace:*", "@ember/edition-utils": "^1.2.0", "@ember/optional-features": "^2.1.0", - "@ember/test-helpers": "5.1.0", + "@ember/test-helpers": "4.0.4", "@ember/test-waiters": "^3.1.0", - "@embroider/addon-shim": "^1.9.0", - "@embroider/macros": "^1.16.10", + "@embroider/addon-shim": "^1.8.9", + "@embroider/macros": "^1.16.6", "@glimmer/component": "^1.1.2", "@glimmer/tracking": "^1.1.2", "@warp-drive/core-types": "workspace:*", @@ -94,7 +94,7 @@ "@warp-drive/holodeck": "workspace:*", "@warp-drive/internal-config": "workspace:*", "@warp-drive/build-config": "workspace:*", - "ember-auto-import": "2.10.0", + "ember-auto-import": "^2.8.1", "ember-cli": "~5.12.0", "ember-cli-babel": "^8.2.0", "ember-cli-dependency-checker": "^3.3.2", diff --git a/tests/ember-data__model/package.json b/tests/ember-data__model/package.json index aed53d139fa..fb39c597020 100644 --- a/tests/ember-data__model/package.json +++ b/tests/ember-data__model/package.json @@ -1,6 +1,6 @@ { "name": "ember-data__model", - "version": "5.4.0-alpha.136", + "version": "4.13.0-alpha.3", "private": true, "description": "Tests for @ember-data/model", "repository": { @@ -72,17 +72,17 @@ "@ember-data/store": "workspace:*", "@ember-data/tracking": "workspace:*", "@ember/optional-features": "^2.1.0", - "@ember/test-helpers": "5.1.0", + "@ember/test-helpers": "4.0.4", "@ember/test-waiters": "^3.1.0", - "@embroider/addon-shim": "^1.9.0", - "@embroider/macros": "^1.16.10", + "@embroider/addon-shim": "^1.8.9", + "@embroider/macros": "^1.16.6", "@glimmer/component": "^1.1.2", "@glimmer/tracking": "^1.1.2", "@warp-drive/core-types": "workspace:*", "@warp-drive/diagnostic": "workspace:*", "@warp-drive/internal-config": "workspace:*", "@warp-drive/build-config": "workspace:*", - "ember-auto-import": "2.10.0", + "ember-auto-import": "^2.8.1", "ember-cli": "~5.12.0", "ember-cli-babel": "^8.2.0", "ember-cli-dependency-checker": "^3.3.2", diff --git a/tests/ember-data__request/package.json b/tests/ember-data__request/package.json index b3e6e74fcfe..e8486d5e74b 100644 --- a/tests/ember-data__request/package.json +++ b/tests/ember-data__request/package.json @@ -1,6 +1,6 @@ { "name": "ember-data__request", - "version": "5.4.0-alpha.136", + "version": "4.13.0-alpha.3", "private": true, "description": "Provides tests for @ember-data/request", "keywords": [], @@ -51,9 +51,9 @@ "@ember-data/request-utils": "workspace:*", "@ember/edition-utils": "^1.2.0", "@ember/optional-features": "^2.1.0", - "@ember/test-helpers": "5.1.0", + "@ember/test-helpers": "4.0.4", "@ember/test-waiters": "^3.1.0", - "@embroider/addon-shim": "^1.9.0", + "@embroider/addon-shim": "^1.8.9", "@glimmer/component": "^1.1.2", "@glimmer/tracking": "^1.1.2", "@warp-drive/core-types": "workspace:*", @@ -61,8 +61,8 @@ "@warp-drive/diagnostic": "workspace:*", "@warp-drive/holodeck": "workspace:*", "@warp-drive/internal-config": "workspace:*", - "bun-types": "^1.2.2", - "ember-auto-import": "2.10.0", + "bun-types": "^1.1.30", + "ember-auto-import": "^2.8.1", "ember-cli": "~5.12.0", "ember-cli-babel": "^8.2.0", "ember-cli-dependency-checker": "^3.3.2", diff --git a/tests/ember-data__serializer/package.json b/tests/ember-data__serializer/package.json index c175c029329..4d98c8b5472 100644 --- a/tests/ember-data__serializer/package.json +++ b/tests/ember-data__serializer/package.json @@ -1,6 +1,6 @@ { "name": "ember-data__serializer", - "version": "5.4.0-alpha.136", + "version": "4.13.0-alpha.3", "private": true, "description": "Tests for the @ember-data/serializer package", "repository": { @@ -78,14 +78,14 @@ "@ember-data/tracking": "workspace:*", "@ember-data/unpublished-test-infra": "workspace:*", "@ember/optional-features": "^2.1.0", - "@ember/test-helpers": "5.1.0", + "@ember/test-helpers": "4.0.4", "@ember/test-waiters": "^3.1.0", "@glimmer/component": "^1.1.2", "@glimmer/tracking": "^1.1.2", "@warp-drive/core-types": "workspace:*", "@warp-drive/internal-config": "workspace:*", "@warp-drive/build-config": "workspace:*", - "ember-auto-import": "2.10.0", + "ember-auto-import": "^2.8.1", "ember-cli": "~5.12.0", "ember-cli-babel": "^8.2.0", "ember-cli-dependency-checker": "^3.3.2", diff --git a/tests/embroider-basic-compat/package.json b/tests/embroider-basic-compat/package.json index 506550e0d7f..13531188a48 100644 --- a/tests/embroider-basic-compat/package.json +++ b/tests/embroider-basic-compat/package.json @@ -1,6 +1,6 @@ { "name": "embroider-basic-compat", - "version": "5.4.0-alpha.136", + "version": "4.13.0-alpha.3", "private": true, "description": "Small description for embroider-basic-compat goes here", "repository": { @@ -76,12 +76,12 @@ "@ember-data/unpublished-test-infra": "workspace:*", "@ember/optional-features": "^2.1.0", "@ember/string": "^3.1.1", - "@ember/test-helpers": "5.1.0", + "@ember/test-helpers": "4.0.4", "@ember/test-waiters": "^3.1.0", - "@embroider/compat": "^3.8.0", - "@embroider/core": "^3.5.0", - "@embroider/webpack": "^4.0.9", - "ember-auto-import": "2.10.0", + "@embroider/compat": "^3.6.5", + "@embroider/core": "^3.4.19", + "@embroider/webpack": "^4.0.8", + "ember-auto-import": "^2.8.1", "ember-data": "workspace:*", "pnpm-sync-dependencies-meta-injected": "0.0.14", "webpack": "^5.92.0", diff --git a/tests/fastboot/package.json b/tests/fastboot/package.json index 31bfd619928..c35948db44a 100644 --- a/tests/fastboot/package.json +++ b/tests/fastboot/package.json @@ -1,6 +1,6 @@ { "name": "fastboot-test-app", - "version": "5.4.0-alpha.136", + "version": "4.13.0-alpha.3", "private": true, "description": "Small description for fastboot-test-app goes here", "repository": { @@ -24,7 +24,7 @@ }, "dependencies": { "@ember-data/unpublished-test-infra": "workspace:*", - "ember-auto-import": "2.10.0", + "ember-auto-import": "^2.8.1", "ember-data": "workspace:*", "pnpm-sync-dependencies-meta-injected": "0.0.14", "webpack": "^5.92.0" @@ -66,7 +66,7 @@ "@babel/runtime": "^7.24.5", "@ember/optional-features": "^2.1.0", "@ember/string": "^3.1.1", - "@ember/test-helpers": "5.1.0", + "@ember/test-helpers": "4.0.4", "@ember/test-waiters": "^3.1.0", "@glimmer/component": "^1.1.2", "@glimmer/tracking": "^1.1.2", diff --git a/tests/full-data-asset-size-app/package.json b/tests/full-data-asset-size-app/package.json index ea6e1f50b68..489d6fd6e28 100644 --- a/tests/full-data-asset-size-app/package.json +++ b/tests/full-data-asset-size-app/package.json @@ -1,6 +1,6 @@ { "name": "full-data-asset-size-app", - "version": "5.4.0-alpha.136", + "version": "4.13.0-alpha.3", "private": true, "description": "An app for determining asset-size of the meta package", "repository": { @@ -30,7 +30,7 @@ "@ember/optional-features": "^2.1.0", "@glimmer/component": "^1.1.2", "@glimmer/tracking": "^1.1.2", - "ember-auto-import": "2.10.0", + "ember-auto-import": "^2.8.1", "ember-cli": "~5.12.0", "ember-cli-babel": "^8.2.0", "ember-cli-dependency-checker": "^3.3.2", diff --git a/tests/main/ember-cli-build.js b/tests/main/ember-cli-build.js index 486676f5862..fa7c18ba6c6 100644 --- a/tests/main/ember-cli-build.js +++ b/tests/main/ember-cli-build.js @@ -57,16 +57,8 @@ module.exports = async function (defaults) { setConfig(app, __dirname, { compatWith: process.env.EMBER_DATA_FULL_COMPAT ? '99.0' : null, - debug: { - // LOG_GRAPH: true, - // LOG_IDENTIFIERS: true, - // LOG_NOTIFICATIONS: true, - // LOG_INSTANCE_CACHE: true, - // LOG_MUTATIONS: true, - // LOG_PAYLOADS: true, - // LOG_REQUESTS: true, - // LOG_REQUEST_STATUS: true, - // LOG_OPERATIONS: true, + deprecations: { + DISABLE_6X_DEPRECATIONS: false, }, }); diff --git a/tests/main/package.json b/tests/main/package.json index bde0d1867be..42c3dab4247 100644 --- a/tests/main/package.json +++ b/tests/main/package.json @@ -1,6 +1,6 @@ { "name": "main-test-app", - "version": "5.4.0-alpha.136", + "version": "4.13.0-alpha.3", "private": true, "description": "A data layer for your Ember applications.", "repository": { @@ -96,9 +96,9 @@ "@ember/edition-utils": "^1.2.0", "@ember/optional-features": "^2.1.0", "@ember/string": "^3.1.1", - "@ember/test-helpers": "5.1.0", + "@ember/test-helpers": "4.0.4", "@ember/test-waiters": "^3.1.0", - "@embroider/macros": "^1.16.10", + "@embroider/macros": "^1.16.6", "@glimmer/component": "^1.1.2", "@glimmer/tracking": "^1.1.2", "@glint/core": "1.5.0", @@ -117,7 +117,7 @@ "broccoli-string-replace": "^0.1.2", "broccoli-test-helper": "^2.0.0", "broccoli-uglify-sourcemap": "^4.0.0", - "ember-auto-import": "2.10.0", + "ember-auto-import": "^2.8.1", "ember-cached-decorator-polyfill": "^1.0.2", "ember-cli": "~5.12.0", "ember-cli-babel": "^8.2.0", @@ -133,7 +133,7 @@ "ember-inflector": "4.0.3", "ember-load-initializers": "^2.1.2", "ember-maybe-import-regenerator": "^1.0.0", - "ember-template-imports": "4.3.0", + "ember-template-imports": "4.1.3", "ember-qunit": "8.0.2", "ember-resolver": "^11.0.1", "ember-source": "~5.12.0", diff --git a/tests/main/tests/acceptance/relationships/has-many-test.js b/tests/main/tests/acceptance/relationships/has-many-test.js index 4548a68c6d3..7bd2f103bbb 100644 --- a/tests/main/tests/acceptance/relationships/has-many-test.js +++ b/tests/main/tests/acceptance/relationships/has-many-test.js @@ -17,6 +17,8 @@ import JSONAPIAdapter from '@ember-data/adapter/json-api'; import Model, { attr, belongsTo, hasMany } from '@ember-data/model'; import { LEGACY_SUPPORT } from '@ember-data/model/-private'; import JSONAPISerializer from '@ember-data/serializer/json-api'; +import { deprecatedTest } from '@ember-data/unpublished-test-infra/test-support/deprecated-test'; +import { DEPRECATE_ARRAY_LIKE } from '@warp-drive/build-config/deprecations'; class Person extends Model { @attr() @@ -819,7 +821,13 @@ module('autotracking has-many', function (hooks) { } get sortedChildren() { - return this.children.slice().sort((a, b) => (a.name > b.name ? 1 : -1)); + if (DEPRECATE_ARRAY_LIKE) { + const result = this.children.sortBy('name'); + assert.expectDeprecation({ id: 'ember-data:deprecate-array-like' }); + return result; + } else { + return this.children.slice().sort((a, b) => (a.name > b.name ? 1 : -1)); + } } @action @@ -864,6 +872,219 @@ module('autotracking has-many', function (hooks) { assert.deepEqual(names, ['RGB', 'RGB'], 'rendered 2 children'); }); + deprecatedTest( + 'We can re-render hasMany w/PromiseManyArray.sortBy', + { id: 'ember-data:deprecate-promise-many-array-behaviors', until: '5.0', count: 3 }, + async function (assert) { + class ChildrenList extends Component { + @service store; + + get sortedChildren() { + const result = this.args.person.children.sortBy('name'); + assert.expectDeprecation({ id: 'ember-data:deprecate-array-like' }); + return result; + } + + @action + createChild() { + const parent = this.args.person; + const name = 'RGB'; + this.store.createRecord('person', { name, parent }); + } + } + + const layout = hbs` + + +

{{this.sortedChildren.length}}

+
    + {{#each this.sortedChildren as |child|}} +
  • {{child.name}}
  • + {{/each}} +
+ `; + this.owner.register('component:children-list', setComponentTemplate(layout, ChildrenList)); + + store.createRecord('person', { id: '1', name: 'Doodad' }); + this.person = store.peekRecord('person', '1'); + + await render(hbs``); + + let names = findAll('li').map((e) => e.textContent); + + assert.deepEqual(names, [], 'rendered no children'); + + await click('#createChild'); + + names = findAll('li').map((e) => e.textContent); + assert.deepEqual(names, ['RGB'], 'rendered 1 child'); + + await click('#createChild'); + + names = findAll('li').map((e) => e.textContent); + assert.deepEqual(names, ['RGB', 'RGB'], 'rendered 2 children'); + } + ); + + deprecatedTest( + 'We can re-render hasMany with sort computed macro on PromiseManyArray', + { id: 'ember-data:deprecate-promise-many-array-behaviors', until: '5.0', count: 3 }, + async function (assert) { + class ChildrenList extends Component { + @service store; + + sortProperties = ['name']; + @sort('args.person.children', 'sortProperties') sortedChildren; + + @action + createChild() { + const parent = this.args.person; + const name = 'RGB'; + this.store.createRecord('person', { name, parent }); + } + } + + const layout = hbs` + + +

{{this.sortedChildren.length}}

+
    + {{#each this.sortedChildren as |child|}} +
  • {{child.name}}
  • + {{/each}} +
+ `; + this.owner.register('component:children-list', setComponentTemplate(layout, ChildrenList)); + + store.createRecord('person', { id: '1', name: 'Doodad' }); + this.person = store.peekRecord('person', '1'); + + await render(hbs``); + + let names = findAll('li').map((e) => e.textContent); + + assert.deepEqual(names, [], 'rendered no children'); + + await click('#createChild'); + + names = findAll('li').map((e) => e.textContent); + assert.deepEqual(names, ['RGB'], 'rendered 1 child'); + + await click('#createChild'); + + names = findAll('li').map((e) => e.textContent); + assert.deepEqual(names, ['RGB', 'RGB'], 'rendered 2 children'); + assert.expectDeprecation({ id: 'ember-data:no-a-with-array-like', count: 3 }); + } + ); + + deprecatedTest( + 'We can re-render hasMany with PromiseManyArray.objectAt', + { id: 'ember-data:deprecate-promise-many-array-behaviors', until: '5.0', count: 6 }, + async function (assert) { + let calls = 0; + class ChildrenList extends Component { + @service store; + + get firstChild() { + const result = this.args.person.children.objectAt(0); + assert.expectDeprecation({ id: 'ember-data:deprecate-array-like' }); + return result; + } + + get lastChild() { + const result = this.args.person.children.objectAt(-1); + assert.expectDeprecation({ id: 'ember-data:deprecate-array-like' }); + return result; + } + + @action + createChild() { + const parent = this.args.person; + const name = 'RGB ' + calls++; + this.store.createRecord('person', { name, parent }); + } + } + + const layout = hbs` + + +

{{this.firstChild.name}}

+

{{this.lastChild.name}}

+ `; + this.owner.register('component:children-list', setComponentTemplate(layout, ChildrenList)); + + store.createRecord('person', { id: '1', name: 'Doodad' }); + this.person = store.peekRecord('person', '1'); + + await render(hbs``); + + assert.dom('h2').hasText('', 'rendered no children'); + + await click('#createChild'); + + assert.dom('h2').hasText('RGB 0', 'renders first child'); + assert.dom('h3').hasText('RGB 0', 'renders last child'); + + await click('#createChild'); + + assert.dom('h2').hasText('RGB 0', 'renders first child'); + assert.dom('h3').hasText('RGB 1', 'renders last child'); + } + ); + + deprecatedTest( + 'We can re-render hasMany with PromiseManyArray.map', + { id: 'ember-data:deprecate-promise-many-array-behaviors', until: '5.0', count: 3 }, + async function (assert) { + class ChildrenList extends Component { + @service store; + + get children() { + return this.args.person.children.map((child) => child); + } + + @action + createChild() { + const parent = this.args.person; + const name = 'RGB'; + this.store.createRecord('person', { name, parent }); + } + } + + const layout = hbs` + + +

{{this.children.length}}

+
    + {{#each this.children as |child|}} +
  • {{child.name}}
  • + {{/each}} +
+ `; + this.owner.register('component:children-list', setComponentTemplate(layout, ChildrenList)); + + store.createRecord('person', { id: '1', name: 'Doodad' }); + this.person = store.peekRecord('person', '1'); + + await render(hbs``); + + let names = findAll('li').map((e) => e.textContent); + + assert.deepEqual(names, [], 'rendered no children'); + + await click('#createChild'); + + names = findAll('li').map((e) => e.textContent); + assert.deepEqual(names, ['RGB'], 'rendered 1 child'); + + await click('#createChild'); + + names = findAll('li').map((e) => e.textContent); + assert.deepEqual(names, ['RGB', 'RGB'], 'rendered 2 children'); + } + ); + test('We can re-render hasMany', async function (assert) { class ChildrenList extends Component { @service store; diff --git a/tests/main/tests/acceptance/tracking-promise-flags-test.js b/tests/main/tests/acceptance/tracking-promise-flags-test.js new file mode 100644 index 00000000000..f3ae8ec1117 --- /dev/null +++ b/tests/main/tests/acceptance/tracking-promise-flags-test.js @@ -0,0 +1,69 @@ +import { render, settled } from '@ember/test-helpers'; + +import { module } from 'qunit'; + +import { hbs } from 'ember-cli-htmlbars'; +import { setupRenderingTest } from 'ember-qunit'; + +import JSONAPIAdapter from '@ember-data/adapter/json-api'; +import Model, { attr } from '@ember-data/model'; +import { deprecatedTest } from '@ember-data/unpublished-test-infra/test-support/deprecated-test'; + +class Widget extends Model { + @attr name; +} + +module('acceptance/tracking-promise-flags', function (hooks) { + setupRenderingTest(hooks); + + hooks.beforeEach(function () { + const { owner } = this; + owner.register('model:widget', Widget); + owner.register( + 'serializer:application', + class { + normalizeResponse = (_, __, data) => data; + static create() { + return new this(); + } + } + ); + }); + + deprecatedTest( + 'can track isPending', + { id: 'ember-data:deprecate-promise-proxies', until: '5.0', count: 6 }, + async function (assert) { + const { owner } = this; + let resolve; + class TestAdapter extends JSONAPIAdapter { + findRecord() { + return new Promise((r) => { + resolve = r; + }); + } + } + owner.register('adapter:application', TestAdapter); + const store = owner.lookup('service:store'); + store.DISABLE_WAITER = true; + this.model = store.findRecord('widget', '1'); + + await render(hbs`{{#if this.model.isPending}}Pending{{else}}{{this.model.name}}{{/if}}`); + + assert.dom().containsText('Pending'); + + resolve({ + data: { + id: '1', + type: 'widget', + attributes: { + name: 'Contraption', + }, + }, + }); + await settled(); + + assert.dom().containsText('Contraption'); + } + ); +}); diff --git a/tests/main/tests/deprecations/deprecate-early-static-test.js b/tests/main/tests/deprecations/deprecate-early-static-test.js new file mode 100644 index 00000000000..661c9a57b1e --- /dev/null +++ b/tests/main/tests/deprecations/deprecate-early-static-test.js @@ -0,0 +1,63 @@ +import { module } from 'qunit'; + +import { setupTest } from 'ember-qunit'; + +import Model from '@ember-data/model'; +import { deprecatedTest } from '@ember-data/unpublished-test-infra/test-support/deprecated-test'; + +module('Deprecations', function (hooks) { + setupTest(hooks); + + const StaticModelMethods = [ + { name: 'typeForRelationship', count: 3 }, + { name: 'inverseFor', count: 5 }, + { name: '_findInverseFor', count: 3 }, + { name: 'eachRelationship', count: 3 }, + { name: 'eachRelatedType', count: 3 }, + { name: 'determineRelationshipType', count: 1 }, + { name: 'eachAttribute', count: 2 }, + { name: 'eachTransformedAttribute', count: 4 }, + { name: 'toString', count: 1 }, + ]; + const StaticModelGetters = [ + { name: 'inverseMap', count: 1 }, + { name: 'relationships', count: 3 }, + { name: 'relationshipNames', count: 1 }, + { name: 'relatedTypes', count: 2 }, + { name: 'relationshipsByName', count: 2 }, + { name: 'relationshipsObject', count: 1 }, + { name: 'fields', count: 1 }, + { name: 'attributes', count: 1 }, + { name: 'transformedAttributes', count: 3 }, + ]; + + function checkDeprecationForProp(prop) { + deprecatedTest( + `Accessing static prop ${prop.name} is deprecated`, + { id: 'ember-data:deprecate-early-static', until: '5.0', count: prop.count }, + function (assert) { + class Post extends Model {} + Post[prop.name]; + assert.ok(true); + } + ); + } + function checkDeprecationForMethod(method) { + deprecatedTest( + `Accessing static method ${method.name} is deprecated`, + { id: 'ember-data:deprecate-early-static', until: '5.0', count: method.count }, + function (assert) { + class Post extends Model {} + try { + Post[method.name](); + } catch { + // do nothing + } + assert.ok(true); + } + ); + } + + StaticModelGetters.forEach(checkDeprecationForProp); + StaticModelMethods.forEach(checkDeprecationForMethod); +}); diff --git a/tests/main/tests/deprecations/deprecate-helpers-test.js b/tests/main/tests/deprecations/deprecate-helpers-test.js new file mode 100644 index 00000000000..e3b09457598 --- /dev/null +++ b/tests/main/tests/deprecations/deprecate-helpers-test.js @@ -0,0 +1,48 @@ +import { module } from 'qunit'; + +import { setupTest } from 'ember-qunit'; + +import { errorsArrayToHash, errorsHashToArray } from '@ember-data/adapter/error'; +import { normalizeModelName } from '@ember-data/store'; +import { normalizeModelName as _privateNormalize } from '@ember-data/store/-private'; +import { deprecatedTest } from '@ember-data/unpublished-test-infra/test-support/deprecated-test'; + +module('Deprecations', function (hooks) { + setupTest(hooks); + + deprecatedTest( + `Calling normalizeModelName`, + { id: 'ember-data:deprecate-normalize-modelname-helper', until: '5.0', count: 1 }, + function (assert) { + normalizeModelName('user'); + assert.ok(true); + } + ); + + deprecatedTest( + `Calling normalizeModelName imported from private`, + { id: 'ember-data:deprecate-normalize-modelname-helper', until: '5.0', count: 1 }, + function (assert) { + _privateNormalize('user'); + assert.ok(true); + } + ); + + deprecatedTest( + `Calling errorsArrayToHash`, + { id: 'ember-data:deprecate-errors-array-to-hash-helper', until: '5.0', count: 1 }, + function (assert) { + errorsArrayToHash([]); + assert.ok(true); + } + ); + + deprecatedTest( + `Calling errorsHashToArray`, + { id: 'ember-data:deprecate-errors-hash-to-array-helper', until: '5.0', count: 1 }, + function (assert) { + errorsHashToArray({}); + assert.ok(true); + } + ); +}); diff --git a/tests/main/tests/deprecations/deprecate-reopen-class-test.js b/tests/main/tests/deprecations/deprecate-reopen-class-test.js new file mode 100644 index 00000000000..9e4e5dd1f2f --- /dev/null +++ b/tests/main/tests/deprecations/deprecate-reopen-class-test.js @@ -0,0 +1,30 @@ +import { module } from 'qunit'; + +import { setupTest } from 'ember-qunit'; + +import Model from '@ember-data/model'; +import { deprecatedTest } from '@ember-data/unpublished-test-infra/test-support/deprecated-test'; + +module('Deprecations', function (hooks) { + setupTest(hooks); + + deprecatedTest( + `Calling on natively extended class`, + { id: 'ember-data:deprecate-model-reopenclass', until: '5.0', count: 1 }, + function (assert) { + class Post extends Model {} + Post.reopenClass({}); + assert.ok(true); + } + ); + + deprecatedTest( + `Calling on classic extended class`, + { id: 'ember-data:deprecate-model-reopenclass', until: '5.0', count: 1 }, + function (assert) { + const Post = Model.extend(); + Post.reopenClass({}); + assert.ok(true); + } + ); +}); diff --git a/tests/main/tests/deprecations/deprecate-reopen-test.js b/tests/main/tests/deprecations/deprecate-reopen-test.js new file mode 100644 index 00000000000..4cf4ac2c847 --- /dev/null +++ b/tests/main/tests/deprecations/deprecate-reopen-test.js @@ -0,0 +1,30 @@ +import { module } from 'qunit'; + +import { setupTest } from 'ember-qunit'; + +import Model from '@ember-data/model'; +import { deprecatedTest } from '@ember-data/unpublished-test-infra/test-support/deprecated-test'; + +module('Deprecations', function (hooks) { + setupTest(hooks); + + deprecatedTest( + `Calling on natively extended class`, + { id: 'ember-data:deprecate-model-reopen', until: '5.0', count: 1 }, + function (assert) { + class Post extends Model {} + Post.reopen({}); + assert.ok(true); + } + ); + + deprecatedTest( + `Calling on classic extended class`, + { id: 'ember-data:deprecate-model-reopen', until: '5.0', count: 1 }, + function (assert) { + const Post = Model.extend(); + Post.reopen({}); + assert.ok(true); + } + ); +}); diff --git a/tests/main/tests/integration/cache/spec-cache-errors-test.ts b/tests/main/tests/integration/cache/spec-cache-errors-test.ts index 144446857a2..2bf29f0871c 100644 --- a/tests/main/tests/integration/cache/spec-cache-errors-test.ts +++ b/tests/main/tests/integration/cache/spec-cache-errors-test.ts @@ -114,11 +114,11 @@ class TestCache implements Cache { calculateChanges?: boolean ): void | string[] { if (!this._data.has(identifier)) { - this.wrapper.notifyChange(identifier, 'added', null); + this.wrapper.notifyChange(identifier, 'added'); } this._data.set(identifier, data); - this.wrapper.notifyChange(identifier, 'attributes', null); - this.wrapper.notifyChange(identifier, 'relationships', null); + this.wrapper.notifyChange(identifier, 'attributes'); + this.wrapper.notifyChange(identifier, 'relationships'); } clientDidCreate(identifier: StableRecordIdentifier, options?: Record): Record { this._isNew = true; @@ -354,7 +354,7 @@ module('integration/record-data Custom Cache (v2) Errors', function (hooks) { }, }, ]; - storeWrapper.notifyChange(identifier, 'errors', null); + storeWrapper.notifyChange(identifier, 'errors'); nameError = person.errors.errorsFor('firstName').objectAt(0); @@ -362,7 +362,7 @@ module('integration/record-data Custom Cache (v2) Errors', function (hooks) { assert.false(person.isValid, 'person is not valid'); errorsToReturn = []; - storeWrapper.notifyChange(identifier, 'errors', null); + storeWrapper.notifyChange(identifier, 'errors'); assert.strictEqual(person.errors.errorsFor('firstName').length, 0, 'no errors on name'); assert.true(person.isValid, 'person is valid'); @@ -376,7 +376,7 @@ module('integration/record-data Custom Cache (v2) Errors', function (hooks) { }, }, ]; - storeWrapper.notifyChange(identifier, 'errors', null); + storeWrapper.notifyChange(identifier, 'errors'); assert.false(person.isValid, 'person is not valid'); diff --git a/tests/main/tests/integration/cache/spec-cache-state-test.ts b/tests/main/tests/integration/cache/spec-cache-state-test.ts index d3605790ea3..b82362cb4b3 100644 --- a/tests/main/tests/integration/cache/spec-cache-state-test.ts +++ b/tests/main/tests/integration/cache/spec-cache-state-test.ts @@ -129,11 +129,11 @@ class TestCache implements Cache { calculateChanges?: boolean ): void | string[] { if (!this._data.has(identifier)) { - this._storeWrapper.notifyChange(identifier, 'added', null); + this._storeWrapper.notifyChange(identifier, 'added'); } this._data.set(identifier, data); - this._storeWrapper.notifyChange(identifier, 'attributes', null); - this._storeWrapper.notifyChange(identifier, 'relationships', null); + this._storeWrapper.notifyChange(identifier, 'attributes'); + this._storeWrapper.notifyChange(identifier, 'relationships'); } mutate(operation: LocalRelationshipOperation): void { throw new Error('Method not implemented.'); @@ -145,7 +145,7 @@ class TestCache implements Cache { clientDidCreate(identifier: StableRecordIdentifier, options?: Record): Record { this._isNew = true; - this._storeWrapper.notifyChange(identifier, 'added', null); + this._storeWrapper.notifyChange(identifier, 'added'); return {}; } willCommit(identifier: StableRecordIdentifier): void {} @@ -374,14 +374,14 @@ module('integration/record-data - Record Data State', function (hooks) { assert.strictEqual(people.length, 1, 'live array starting length is 1'); isNew = true; - storeWrapper.notifyChange(personIdentifier, 'state', null); + storeWrapper.notifyChange(personIdentifier, 'state'); await settled(); assert.true(person.isNew, 'person is new'); assert.strictEqual(people.length, 1, 'live array starting length is 1'); isNew = false; isDeleted = true; - storeWrapper.notifyChange(personIdentifier, 'state', null); + storeWrapper.notifyChange(personIdentifier, 'state'); await settled(); assert.false(person.isNew, 'person is not new'); assert.true(person.isDeleted, 'person is deleted'); @@ -389,7 +389,7 @@ module('integration/record-data - Record Data State', function (hooks) { isNew = false; isDeleted = false; - storeWrapper.notifyChange(personIdentifier, 'state', null); + storeWrapper.notifyChange(personIdentifier, 'state'); await settled(); assert.false(person.isNew, 'person is not new'); assert.false(person.isDeleted, 'person is not deleted'); @@ -401,7 +401,7 @@ module('integration/record-data - Record Data State', function (hooks) { assert.true(calledSetIsDeleted, 'called setIsDeleted'); isDeletionCommitted = true; - storeWrapper.notifyChange(personIdentifier, 'state', null); + storeWrapper.notifyChange(personIdentifier, 'state'); await settled(); assert.strictEqual(people.length, 0, 'committing a deletion updates the live array'); }); diff --git a/tests/main/tests/integration/cache/spec-cache-test.ts b/tests/main/tests/integration/cache/spec-cache-test.ts index b7a7c0af44d..8ee29368750 100644 --- a/tests/main/tests/integration/cache/spec-cache-test.ts +++ b/tests/main/tests/integration/cache/spec-cache-test.ts @@ -143,11 +143,11 @@ class TestCache implements Cache { calculateChanges?: boolean ): void | string[] { if (!this._data.has(identifier)) { - this._storeWrapper.notifyChange(identifier, 'added', null); + this._storeWrapper.notifyChange(identifier, 'added'); } this._data.set(identifier, data); - this._storeWrapper.notifyChange(identifier, 'attributes', null); - this._storeWrapper.notifyChange(identifier, 'relationships', null); + this._storeWrapper.notifyChange(identifier, 'attributes'); + this._storeWrapper.notifyChange(identifier, 'relationships'); } clientDidCreate(identifier: StableRecordIdentifier, options?: Record): Record { diff --git a/tests/main/tests/integration/inverse-test.js b/tests/main/tests/integration/inverse-test.js new file mode 100644 index 00000000000..217ff318a38 --- /dev/null +++ b/tests/main/tests/integration/inverse-test.js @@ -0,0 +1,312 @@ +import { module } from 'qunit'; + +import { setupTest } from 'ember-qunit'; + +import Model, { attr, belongsTo } from '@ember-data/model'; +import { deprecatedTest } from '@ember-data/unpublished-test-infra/test-support/deprecated-test'; +import { DEBUG } from '@warp-drive/build-config/env'; + +function stringify(string) { + return function () { + return string; + }; +} + +module('integration/inverse-test - inverseFor', function (hooks) { + setupTest(hooks); + let store; + + hooks.beforeEach(function () { + const { owner } = this; + store = owner.lookup('service:store'); + }); + + deprecatedTest( + 'Finds the inverse when there is only one possible available', + { id: 'ember-data:deprecate-non-strict-relationships', until: '5.0', count: 2 }, + function (assert) { + class User extends Model { + @attr() + name; + + @belongsTo('user', { async: true, inverse: null }) + bestFriend; + + @belongsTo('job', { async: false }) + job; + + toString() { + return stringify('user'); + } + } + + class Job extends Model { + @attr() + isGood; + + @belongsTo('user', { async: false }) + user; + + toString() { + return stringify('job'); + } + } + + const { owner } = this; + owner.register('model:user', User); + owner.register('model:job', Job); + + const job = store.modelFor('job'); + const inverseDefinition = job.inverseFor('user', store); + + assert.deepEqual( + inverseDefinition, + { + type: 'user', + name: 'job', + kind: 'belongsTo', + options: { + async: false, + }, + }, + 'Gets correct type, name and kind' + ); + } + ); + + deprecatedTest( + 'Finds the inverse when only one side has defined it manually', + { id: 'ember-data:deprecate-non-strict-relationships', until: '5.0', count: 3 }, + function (assert) { + class User extends Model { + @attr() + name; + + @belongsTo('user', { async: true, inverse: null }) + bestFriend; + + @belongsTo('job', { async: false }) + job; + + @belongsTo('job', { async: false }) + previousJob; + + toString() { + return stringify('user'); + } + } + + class Job extends Model { + @attr() + isGood; + + @belongsTo('user', { async: false }) + user; + + @belongsTo('user', { inverse: 'previousJob', async: false }) + owner; + + toString() { + return stringify('job'); + } + } + + const { owner } = this; + owner.register('model:user', User); + owner.register('model:job', Job); + + const job = store.modelFor('job'); + const user = store.modelFor('user'); + + assert.deepEqual( + job.inverseFor('owner', store), + { + type: 'user', //the model's type + name: 'previousJob', //the models relationship key + kind: 'belongsTo', + options: { + async: false, + }, + }, + 'Gets correct type, name and kind' + ); + + assert.deepEqual( + user.inverseFor('previousJob', store), + { + type: 'job', //the model's type + name: 'owner', //the models relationship key + kind: 'belongsTo', + options: { + inverse: 'previousJob', + async: false, + }, + }, + 'Gets correct type, name and kind' + ); + } + ); + + deprecatedTest( + 'Returns null if inverse relationship it is manually set with a different relationship key', + { id: 'ember-data:deprecate-non-strict-relationships', until: '5.0', count: 1 }, + function (assert) { + class User extends Model { + @attr() + name; + + @belongsTo('user', { async: true, inverse: null }) + bestFriend; + + @belongsTo('job', { async: false }) + job; + + toString() { + return stringify('user'); + } + } + + class Job extends Model { + @attr() + isGood; + + @belongsTo('user', { inverse: 'previousJob', async: false }) + user; + + toString() { + return stringify('job'); + } + } + + const { owner } = this; + owner.register('model:user', User); + owner.register('model:job', Job); + + const user = store.modelFor('user'); + assert.strictEqual(user.inverseFor('job', store), null, 'There is no inverse'); + } + ); + + if (DEBUG) { + deprecatedTest( + 'Errors out if you define 2 inverses to the same model', + { id: 'ember-data:deprecate-non-strict-relationships', until: '5.0', count: 1 }, + function (assert) { + class User extends Model { + @attr() + name; + + @belongsTo('user', { async: true, inverse: null }) + bestFriend; + + @belongsTo('job', { async: false }) + job; + + toString() { + return stringify('user'); + } + } + + class Job extends Model { + @attr() + isGood; + + @belongsTo('user', { inverse: 'job', async: false }) + user; + + @belongsTo('user', { inverse: 'job', async: false }) + owner; + + toString() { + return stringify('job'); + } + } + const { owner } = this; + owner.register('model:user', User); + owner.register('model:job', Job); + + const user = store.modelFor('user'); + assert.expectAssertion(() => { + user.inverseFor('job', store); + }, /You defined the 'job' relationship on model:user, but you defined the inverse relationships of type model:job multiple times/i); + } + ); + } + + deprecatedTest( + 'Caches findInverseFor return value', + { id: 'ember-data:deprecate-non-strict-relationships', until: '5.0', count: 2 }, + function (assert) { + assert.expect(1); + class User extends Model { + @attr() + name; + + @belongsTo('user', { async: true, inverse: null }) + bestFriend; + + @belongsTo('job', { async: false }) + job; + + toString() { + return stringify('user'); + } + } + + class Job extends Model { + @attr() + isGood; + + @belongsTo('user', { async: false }) + user; + + toString() { + return stringify('job'); + } + } + const { owner } = this; + owner.register('model:user', User); + owner.register('model:job', Job); + + const job = store.modelFor('job'); + + const inverseForUser = job.inverseFor('user', store); + job.findInverseFor = function () { + assert.ok(false, 'Find is not called anymore'); + }; + + assert.strictEqual(inverseForUser, job.inverseFor('user', store), 'Inverse cached succesfully'); + } + ); + + if (DEBUG) { + deprecatedTest( + 'Errors out if you do not define an inverse for a reflexive relationship', + { id: 'ember-data:deprecate-non-strict-relationships', until: '5.0', count: 1 }, + function (assert) { + class ReflexiveModel extends Model { + @belongsTo('reflexive-model', { async: false }) + reflexiveProp; + + toString() { + return stringify('reflexiveModel'); + } + } + + const { owner } = this; + owner.register('model:reflexive-model', ReflexiveModel); + + //Maybe store is evaluated lazily, so we need this :( + assert.expectWarning(() => { + const reflexiveModel = store.push({ + data: { + type: 'reflexive-model', + id: '1', + }, + }); + reflexiveModel.reflexiveProp; + }, /Detected a reflexive relationship named 'reflexiveProp' on the schema for 'reflexive-model' without an inverse option/); + } + ); + } +}); diff --git a/tests/main/tests/integration/record-arrays/adapter-populated-record-array-test.js b/tests/main/tests/integration/record-arrays/adapter-populated-record-array-test.js index fda58f2a4b6..fa30ecd99c4 100644 --- a/tests/main/tests/integration/record-arrays/adapter-populated-record-array-test.js +++ b/tests/main/tests/integration/record-arrays/adapter-populated-record-array-test.js @@ -167,6 +167,22 @@ module('integration/record-arrays/collection', function (hooks) { ); }); + test('recordArray.replace() throws error', async function (assert) { + const store = this.owner.lookup('service:store'); + const recordArray = store.recordArrayManager.createArray({ type: 'person', query: null }); + + await settled(); + + assert.expectAssertion( + () => { + recordArray.replace(); + }, + 'Mutating this array of records via splice is not allowed.', + 'throws error' + ); + assert.expectDeprecation({ id: 'ember-data:deprecate-array-like' }); + }); + test('recordArray mutation throws error', async function (assert) { const store = this.owner.lookup('service:store'); const recordArray = store.recordArrayManager.createArray({ type: 'person', query: null }); diff --git a/tests/main/tests/integration/records/relationship-changes-test.js b/tests/main/tests/integration/records/relationship-changes-test.js index ca899a4f49c..c6f9ebcd1fc 100644 --- a/tests/main/tests/integration/records/relationship-changes-test.js +++ b/tests/main/tests/integration/records/relationship-changes-test.js @@ -1,3 +1,5 @@ +import EmberObject from '@ember/object'; +import { alias } from '@ember/object/computed'; import { settled } from '@ember/test-helpers'; import { module, test } from 'qunit'; @@ -7,6 +9,7 @@ import { setupTest } from 'ember-qunit'; import Adapter from '@ember-data/adapter'; import Model, { attr, belongsTo, hasMany } from '@ember-data/model'; import JSONAPISerializer from '@ember-data/serializer/json-api'; +import { deprecatedTest } from '@ember-data/unpublished-test-infra/test-support/deprecated-test'; const Author = Model.extend({ name: attr('string'), @@ -62,6 +65,114 @@ module('integration/records/relationship-changes - Relationship changes', functi this.owner.register('serializer:application', class extends JSONAPISerializer {}); }); + deprecatedTest( + 'Calling push with relationship recalculates computed alias property if the relationship was empty and is added to', + { id: 'ember-data:deprecate-promise-many-array-behaviors', until: '5.0', count: 2 }, + function (assert) { + assert.expect(2); + + const store = this.owner.lookup('service:store'); + + const Obj = EmberObject.extend({ + person: null, + siblings: alias('person.siblings'), + }); + + const obj = Obj.create(); + + store.push({ + data: { + type: 'person', + id: 'wat', + attributes: { + firstName: 'Yehuda', + lastName: 'Katz', + }, + relationships: { + siblings: { + data: [], + }, + }, + }, + }); + obj.person = store.peekRecord('person', 'wat'); + assert.arrayStrictEquals(obj.siblings.slice(), [], 'siblings cp should have calculated empty initially'); + + store.push({ + data: { + type: 'person', + id: 'wat', + attributes: {}, + relationships: { + siblings: { + data: [sibling1Ref], + }, + }, + }, + included: [sibling1], + }); + + const cpResult = obj.siblings.slice(); + assert.strictEqual(cpResult.length, 1, 'siblings cp should have recalculated'); + obj.destroy(); + } + ); + + deprecatedTest( + 'Calling push with relationship recalculates computed alias property to firstObject if the relationship was empty and is added to', + { id: 'ember-data:deprecate-promise-many-array-behaviors', until: '5.0', count: 1 }, + function (assert) { + assert.expect(3); + + const store = this.owner.lookup('service:store'); + + const Obj = EmberObject.extend({ + person: null, + firstSibling: alias('person.siblings.firstObject'), + }); + + const obj = Obj.create(); + + store.push({ + data: { + type: 'person', + id: 'wat', + attributes: { + firstName: 'Yehuda', + lastName: 'Katz', + }, + relationships: { + siblings: { + data: [], + }, + }, + }, + }); + obj.person = store.peekRecord('person', 'wat'); + assert.strictEqual(obj.sibling, undefined, 'We have no first sibling initially'); + + store.push({ + data: { + type: 'person', + id: 'wat', + attributes: {}, + relationships: { + siblings: { + data: [sibling1Ref], + }, + }, + }, + included: [sibling1], + }); + + const cpResult = obj.firstSibling; + assert.strictEqual(cpResult?.id, '1', 'siblings cp should have recalculated'); + obj.destroy(); + + assert.expectDeprecation({ id: 'ember-data:deprecate-array-like' }); + } + ); + test('Calling push with relationship triggers observers once if the relationship was not empty and was added to', async function (assert) { assert.expect(2); diff --git a/tests/main/tests/integration/records/save-test.js b/tests/main/tests/integration/records/save-test.js index ddb5a31555d..762ce8e5527 100644 --- a/tests/main/tests/integration/records/save-test.js +++ b/tests/main/tests/integration/records/save-test.js @@ -10,6 +10,7 @@ import Model, { attr } from '@ember-data/model'; import { createDeferred } from '@ember-data/request'; import JSONAPISerializer from '@ember-data/serializer/json-api'; import testInDebug from '@ember-data/unpublished-test-infra/test-support/test-in-debug'; +import { DEPRECATE_SAVE_PROMISE_ACCESS } from '@warp-drive/build-config/deprecations'; module('integration/records/save - Save Record', function (hooks) { setupTest(hooks); @@ -36,12 +37,35 @@ module('integration/records/save - Save Record', function (hooks) { const saved = post.save(); + if (DEPRECATE_SAVE_PROMISE_ACCESS) { + // `save` returns a PromiseObject which allows to call get on it + assert.strictEqual(saved.get('id'), undefined, `.get('id') is undefined before save resolves`); + } + deferred.resolve({ data: { id: '123', type: 'post' } }); const model = await saved; assert.ok(true, 'save operation was resolved'); - assert.strictEqual(saved.id, undefined, `.id is undefined after save resolves`); - assert.strictEqual(model.id, '123', `record.id is '123' after save resolves`); + if (DEPRECATE_SAVE_PROMISE_ACCESS) { + assert.strictEqual(saved.get('id'), '123', `.get('id') is '123' after save resolves`); + assert.strictEqual(model.id, '123', `record.id is '123' after save resolves`); + } else { + assert.strictEqual(saved.id, undefined, `.id is undefined after save resolves`); + assert.strictEqual(model.id, '123', `record.id is '123' after save resolves`); + } assert.strictEqual(model, post, 'resolves with the model'); + if (DEPRECATE_SAVE_PROMISE_ACCESS) { + // We don't care about the exact value of the property, but accessing it + // should not throw an error and only show a deprecation. + saved.__ec_cancel__ = true; + assert.true(saved.__ec_cancel__, '__ec_cancel__ can be accessed on the proxy'); + assert.strictEqual( + model.__ec_cancel__, + undefined, + '__ec_cancel__ can be accessed on the record but is not present' + ); + + assert.expectDeprecation({ id: 'ember-data:model-save-promise', count: 10 }); + } }); test('Will reject save on error', async function (assert) { diff --git a/tests/main/tests/integration/references/autotracking-test.js b/tests/main/tests/integration/references/autotracking-test.js index ffd58d545d8..9e95ab31add 100644 --- a/tests/main/tests/integration/references/autotracking-test.js +++ b/tests/main/tests/integration/references/autotracking-test.js @@ -10,32 +10,6 @@ import Model, { attr, belongsTo, hasMany } from '@ember-data/model'; import { recordIdentifierFor } from '@ember-data/store'; import { DEBUG } from '@warp-drive/build-config/env'; -function pushDefaultUser(store) { - return store.push({ - data: { - type: 'user', - id: '1', - attributes: { - name: 'Chris', - }, - relationships: { - bestFriend: { - data: { type: 'user', id: '2' }, - }, - friends: { - data: [{ type: 'user', id: '2' }], - }, - }, - }, - included: [ - { type: 'user', id: '2', attributes: { name: 'Igor' } }, - { type: 'user', id: '3', attributes: { name: 'David' } }, - { type: 'user', id: '4', attributes: { name: 'Scott' } }, - { type: 'user', id: '5', attributes: { name: 'Rob' } }, - ], - }); -} - module('integration/references/autotracking', function (hooks) { setupRenderingTest(hooks); @@ -47,7 +21,7 @@ module('integration/references/autotracking', function (hooks) { friends; } - let store; + let store, user; hooks.beforeEach(function () { const { owner } = this; owner.register('model:user', User); @@ -76,10 +50,33 @@ module('integration/references/autotracking', function (hooks) { } } ); + + user = store.push({ + data: { + type: 'user', + id: '1', + attributes: { + name: 'Chris', + }, + relationships: { + bestFriend: { + data: { type: 'user', id: '2' }, + }, + friends: { + data: [{ type: 'user', id: '2' }], + }, + }, + }, + included: [ + { type: 'user', id: '2', attributes: { name: 'Igor' } }, + { type: 'user', id: '3', attributes: { name: 'David' } }, + { type: 'user', id: '4', attributes: { name: 'Scott' } }, + { type: 'user', id: '5', attributes: { name: 'Rob' } }, + ], + }); }); test('BelongsToReference.id() is autotracked', async function (assert) { - const user = pushDefaultUser(store); class TestContext { user = user; @@ -143,7 +140,6 @@ module('integration/references/autotracking', function (hooks) { }); test('BelongsToReference.id() autotracking works with null value changes', async function (assert) { - const user = pushDefaultUser(store); class TestContext { user = user; @@ -177,7 +173,6 @@ module('integration/references/autotracking', function (hooks) { }); test('HasManyReference.ids() is autotracked', async function (assert) { - const user = pushDefaultUser(store); class TestContext { user = user; @@ -206,47 +201,10 @@ module('integration/references/autotracking', function (hooks) { assert.deepEqual(testContext.friendIds, ['2', '6'], 'the ids are correct when the new record is saved'); }); - test('HasManyReference.value() is autotracked under unload/restore conditions', async function (assert) { - pushDefaultUser(store); + test('HasManyReference.value() is autotracked', async function (assert) { store.unloadAll(); await settled(); - const user = store.push({ - data: { - type: 'user', - id: '1', - attributes: { - name: 'Chris', - }, - relationships: {}, - }, - included: [], - }); - class TestContext { - user = user; - - get friends() { - return this.user.hasMany('friends').value(); - } - } - const testContext = new TestContext(); - this.set('context', testContext); - await render( - hbs`{{#each this.context.friends as |friend|}}id: {{if friend.id friend.id 'null'}}, {{else}}No Friends Loaded{{/each}}` - ); - - assert.strictEqual(getRootElement().textContent, 'No Friends Loaded', 'the ids are initially correct'); - assert.deepEqual(testContext.friends, null, 'the value is initially null'); - const igor = store.push({ - data: { type: 'user', id: '2', attributes: { name: 'Igor' } }, - included: [{ type: 'user', id: '1', relationships: { friends: { data: [{ type: 'user', id: '2' }] } } }], - }); - await settled(); - assert.strictEqual(getRootElement().textContent, 'id: 2, ', 'the ManyArray is rendered once loaded'); - assert.deepEqual(testContext.friends, [igor], 'the friends are correct once loaded'); - }); - - test('HasManyReference.value() is autotracked', async function (assert) { - const user = store.push({ + user = store.push({ data: { type: 'user', id: '1', diff --git a/tests/main/tests/integration/references/belongs-to-test.js b/tests/main/tests/integration/references/belongs-to-test.js index 6c8f5ccf264..bed38c7385c 100644 --- a/tests/main/tests/integration/references/belongs-to-test.js +++ b/tests/main/tests/integration/references/belongs-to-test.js @@ -6,8 +6,11 @@ import { setupTest } from 'ember-qunit'; import JSONAPIAdapter from '@ember-data/adapter/json-api'; import Model, { attr, belongsTo, hasMany } from '@ember-data/model'; +import { createDeferred } from '@ember-data/request'; import JSONAPISerializer from '@ember-data/serializer/json-api'; +import { deprecatedTest } from '@ember-data/unpublished-test-infra/test-support/deprecated-test'; import testInDebug from '@ember-data/unpublished-test-infra/test-support/test-in-debug'; +import { DEPRECATE_NON_EXPLICIT_POLYMORPHISM } from '@warp-drive/build-config/deprecations'; class Family extends Model { @hasMany('person', { async: true, inverse: 'family' }) persons; @@ -409,6 +412,48 @@ module('integration/references/belongs-to', function (hooks) { assert.deepEqual(familyReference.meta(), { updatedAt: timestamp2 }, 'meta is updated'); }); + deprecatedTest( + 'push(promise)', + { id: 'ember-data:deprecate-promise-proxies', until: '5.0', count: 1 }, + async function (assert) { + const store = this.owner.lookup('service:store'); + const Family = store.modelFor('family'); + + const deferred = createDeferred(); + + const person = store.push({ + data: { + type: 'person', + id: '1', + relationships: { + family: { + data: { type: 'family', id: '1' }, + }, + }, + }, + }); + const familyReference = person.belongsTo('family'); + const push = familyReference.push(deferred.promise); + + assert.ok(push.then, 'BelongsToReference.push returns a promise'); + + deferred.resolve({ + data: { + type: 'family', + id: '1', + attributes: { + name: 'Coreleone', + }, + }, + }); + + await push.then(function (record) { + assert.ok(record instanceof Family, 'push resolves with the record'); + assert.strictEqual(record.name, 'Coreleone', 'name is updated'); + }); + } + ); + testInDebug('push(object) asserts for invalid modelClass', async function (assert) { class Family extends Model { @hasMany('person', { async: true, inverse: 'family' }) persons; @@ -444,9 +489,14 @@ module('integration/references/belongs-to', function (hooks) { const familyReference = person.belongsTo('family'); - await assert.expectAssertion(async function () { - await familyReference.push(anotherPerson); - }, "The 'person' type does not implement 'family' and thus cannot be assigned to the 'family' relationship in 'person'. If this relationship should be polymorphic, mark person.family as `polymorphic: true` and person.persons as implementing it via `as: 'family'`."); + await assert.expectAssertion( + async function () { + await familyReference.push(anotherPerson); + }, + DEPRECATE_NON_EXPLICIT_POLYMORPHISM + ? "The 'person' type does not implement 'family' and thus cannot be assigned to the 'family' relationship in 'person'. Make it a descendant of 'family' or use a mixin of the same name." + : "The 'person' type does not implement 'family' and thus cannot be assigned to the 'family' relationship in 'person'. If this relationship should be polymorphic, mark person.family as `polymorphic: true` and person.persons as implementing it via `as: 'family'`." + ); }); testInDebug('push(object) works with polymorphic types', async function (assert) { diff --git a/tests/main/tests/integration/references/has-many-test.js b/tests/main/tests/integration/references/has-many-test.js index 260f15b4b0a..eb1b8561e9b 100755 --- a/tests/main/tests/integration/references/has-many-test.js +++ b/tests/main/tests/integration/references/has-many-test.js @@ -4,8 +4,11 @@ import { setupRenderingTest } from 'ember-qunit'; import Adapter from '@ember-data/adapter'; import Model, { attr, belongsTo, hasMany } from '@ember-data/model'; +import { createDeferred } from '@ember-data/request'; import JSONAPISerializer from '@ember-data/serializer/json-api'; +import { deprecatedTest } from '@ember-data/unpublished-test-infra/test-support/deprecated-test'; import testInDebug from '@ember-data/unpublished-test-infra/test-support/test-in-debug'; +import { DEPRECATE_NON_EXPLICIT_POLYMORPHISM } from '@warp-drive/build-config/deprecations'; import createTrackingContext from '../../helpers/create-tracking-context'; @@ -329,9 +332,14 @@ module('integration/references/has-many', function (hooks) { }); const petsReference = person.hasMany('pets'); - await assert.expectAssertion(async () => { - await petsReference.push([{ type: 'person', id: '1' }]); - }, "The 'person' type does not implement 'animal' and thus cannot be assigned to the 'pets' relationship in 'person'. If this relationship should be polymorphic, mark person.pets as `polymorphic: true` and person.owner as implementing it via `as: 'animal'`."); + await assert.expectAssertion( + async () => { + await petsReference.push([{ type: 'person', id: '1' }]); + }, + DEPRECATE_NON_EXPLICIT_POLYMORPHISM + ? "The 'person' type does not implement 'animal' and thus cannot be assigned to the 'pets' relationship in 'person'. Make it a descendant of 'animal' or use a mixin of the same name." + : "The 'person' type does not implement 'animal' and thus cannot be assigned to the 'pets' relationship in 'person'. If this relationship should be polymorphic, mark person.pets as `polymorphic: true` and person.owner as implementing it via `as: 'animal'`." + ); }); test('push valid json:api', async function (assert) { @@ -375,6 +383,48 @@ module('integration/references/has-many', function (hooks) { assert.strictEqual(personsReference.link(), '/families/1/persons', 'link is not updated'); }); + deprecatedTest( + 'push(promise)', + { id: 'ember-data:deprecate-promise-proxies', until: '5.0', count: 1 }, + async function (assert) { + const store = this.owner.lookup('service:store'); + const deferred = createDeferred(); + + const family = store.push({ + data: { + type: 'family', + id: '1', + relationships: { + persons: { + data: [ + { type: 'person', id: '1' }, + { type: 'person', id: '2' }, + ], + }, + }, + }, + }); + const personsReference = family.hasMany('persons'); + const pushResult = personsReference.push(deferred.promise); + + assert.ok(pushResult.then, 'HasManyReference.push returns a promise'); + + const payload = { + data: [ + { type: 'person', id: '1', attributes: { name: 'Vito' } }, + { type: 'person', id: '2', attributes: { name: 'Michael' } }, + ], + }; + + deferred.resolve(payload); + + const records = await pushResult; + assert.strictEqual(records.length, 2); + assert.strictEqual(records.at(0).name, 'Vito'); + assert.strictEqual(records.at(1).name, 'Michael'); + } + ); + test('push(document) can update links', async function (assert) { const store = this.owner.lookup('service:store'); diff --git a/tests/main/tests/integration/relationships/belongs-to-test.js b/tests/main/tests/integration/relationships/belongs-to-test.js index 8b945fbb8d6..df839881df4 100644 --- a/tests/main/tests/integration/relationships/belongs-to-test.js +++ b/tests/main/tests/integration/relationships/belongs-to-test.js @@ -7,7 +7,9 @@ import { setupTest } from 'ember-qunit'; import JSONAPIAdapter from '@ember-data/adapter/json-api'; import Model, { attr, belongsTo, hasMany } from '@ember-data/model'; import JSONAPISerializer from '@ember-data/serializer/json-api'; +import { deprecatedTest } from '@ember-data/unpublished-test-infra/test-support/deprecated-test'; import testInDebug from '@ember-data/unpublished-test-infra/test-support/test-in-debug'; +import { DEPRECATE_NON_EXPLICIT_POLYMORPHISM } from '@warp-drive/build-config/deprecations'; import { getRelationshipStateForRecord, hasRelationshipForRecord } from '../../helpers/accessors'; @@ -580,9 +582,14 @@ module('integration/relationship/belongs_to Belongs-To Relationships', function }, }); - assert.expectAssertion(() => { - post.user = comment; - }, "The 'comment' type does not implement 'user' and thus cannot be assigned to the 'user' relationship in 'post'. If this relationship should be polymorphic, mark message.user as `polymorphic: true` and comment.messages as implementing it via `as: 'user'`."); + assert.expectAssertion( + () => { + post.user = comment; + }, + DEPRECATE_NON_EXPLICIT_POLYMORPHISM + ? "The 'comment' type does not implement 'user' and thus cannot be assigned to the 'user' relationship in 'post'. Make it a descendant of 'user' or use a mixin of the same name." + : "The 'comment' type does not implement 'user' and thus cannot be assigned to the 'user' relationship in 'post'. If this relationship should be polymorphic, mark message.user as `polymorphic: true` and comment.messages as implementing it via `as: 'user'`." + ); } ); @@ -825,6 +832,52 @@ module('integration/relationship/belongs_to Belongs-To Relationships', function }); }); + deprecatedTest( + 'A record can be created with a resolved belongsTo promise', + { id: 'ember-data:deprecate-promise-proxies', until: '5.0' }, + async function (assert) { + assert.expect(1); + + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); + + adapter.shouldBackgroundReloadRecord = () => false; + + const Group = Model.extend({ + people: hasMany('person', { async: false, inverse: 'group' }), + }); + + const Person = Model.extend({ + group: belongsTo('group', { async: true, inverse: 'people' }), + }); + + this.owner.register('model:group', Group); + this.owner.register('model:person', Person); + + store.push({ + data: { + id: '1', + type: 'group', + }, + }); + const originalOwner = store.push({ + data: { + id: '1', + type: 'person', + group: { data: { type: 'group', id: '1' } }, + }, + }); + + const groupPromise = originalOwner.group; + const group = await groupPromise; + const person = store.createRecord('person', { + group: groupPromise, + }); + const personGroup = await person.group; + assert.strictEqual(personGroup, group, 'the group matches'); + } + ); + test('polymorphic belongsTo class-checks check the superclass', function (assert) { assert.expect(1); @@ -1097,6 +1150,19 @@ module('integration/relationship/belongs_to Belongs-To Relationships', function assert.strictEqual(book.author, author, 'Book has an author after rollback attributes'); }); + testInDebug('Passing a model as type to belongsTo should not work', function (assert) { + assert.expect(2); + + assert.expectAssertion(() => { + const User = Model.extend(); + + Model.extend({ + user: belongsTo(User, { async: false, inverse: null }), + }); + }, /The first argument to belongsTo must be a string/); + assert.expectDeprecation({ id: 'ember-data:deprecate-non-strict-relationships' }); + }); + test('belongsTo hasAnyRelationshipData async loaded', async function (assert) { assert.expect(1); class Book extends Model { diff --git a/tests/main/tests/integration/relationships/collection/mutating-has-many-test.ts b/tests/main/tests/integration/relationships/collection/mutating-has-many-test.ts new file mode 100644 index 00000000000..6037969b471 --- /dev/null +++ b/tests/main/tests/integration/relationships/collection/mutating-has-many-test.ts @@ -0,0 +1,425 @@ +import { settled } from '@ember/test-helpers'; + +import { module, test } from 'qunit'; + +import { setupRenderingTest } from 'ember-qunit'; + +import type { ManyArray } from '@ember-data/model'; +import Model, { attr, hasMany } from '@ember-data/model'; +import type Store from '@ember-data/store'; +import { recordIdentifierFor } from '@ember-data/store'; +import { DEPRECATE_MANY_ARRAY_DUPLICATES, DISABLE_6X_DEPRECATIONS } from '@warp-drive/build-config/deprecations'; +import type { ExistingResourceIdentifierObject } from '@warp-drive/core-types/spec/json-api-raw'; +import { Type } from '@warp-drive/core-types/symbols'; + +import type { ReactiveContext } from '../../../helpers/reactive-context'; +import { reactiveContext } from '../../../helpers/reactive-context'; + +class User extends Model { + @attr declare name: string; + @hasMany('user', { async: false, inverse: 'friends' }) declare friends: ManyArray; + + [Type] = 'user' as const; +} + +function krystanData() { + return { + id: '2', + type: 'user', + attributes: { + name: 'Krystan', + }, + }; +} + +function krystanRef(): ExistingResourceIdentifierObject { + return { type: 'user', id: '2' }; +} + +function samData() { + return { + id: '3', + type: 'user', + attributes: { + name: 'Sam', + }, + }; +} + +function samRef(): ExistingResourceIdentifierObject { + return { type: 'user', id: '3' }; +} + +function ericData() { + return { + id: '4', + type: 'user', + attributes: { + name: 'Eric', + }, + }; +} + +function ericRef(): ExistingResourceIdentifierObject { + return { type: 'user', id: '4' }; +} + +function chrisData(friends: ExistingResourceIdentifierObject[]) { + return { + id: '1', + type: 'user', + attributes: { + name: 'Chris', + }, + relationships: { + friends: { + data: friends, + }, + }, + }; +} + +function makeUser(store: Store, friends: ExistingResourceIdentifierObject[]): User { + return store.push({ + data: chrisData(friends), + included: [krystanData(), samData(), ericData()], + }) as User; +} + +type Mutation = { + name: string; + method: 'push' | 'unshift' | 'splice'; + values: ExistingResourceIdentifierObject[]; + start?: (record: User) => number; + deleteCount?: (record: User) => number; +}; + +function generateAppliedMutation(store: Store, record: User, mutation: Mutation) { + const friends = record.friends; + let outcomeValues: User[]; + let error: string; + + let seen = new Set(); + const duplicates = new Set(); + let outcome: User[]; + + switch (mutation.method) { + case 'push': + error = "Cannot push duplicates to a hasMany's state."; + outcomeValues = [...friends, ...mutation.values.map((ref) => store.peekRecord(ref) as User)]; + + outcomeValues.forEach((item) => { + if (seen.has(item)) { + duplicates.add(item); + } else { + seen.add(item); + } + }); + + outcome = Array.from(new Set(outcomeValues)); + + break; + case 'unshift': { + error = "Cannot unshift duplicates to a hasMany's state."; + const added = mutation.values.map((ref) => store.peekRecord(ref) as User); + seen = new Set(friends); + outcome = []; + added.forEach((item) => { + if (seen.has(item)) { + duplicates.add(item); + } else { + seen.add(item); + outcome.push(item); + } + }); + outcome.push(...friends); + break; + } + case 'splice': { + const start = mutation.start?.(record) ?? 0; + const deleteCount = mutation.deleteCount?.(record) ?? 0; + outcomeValues = friends.slice(); + const added = mutation.values.map((ref) => store.peekRecord(ref) as User); + outcomeValues.splice(start, deleteCount, ...added); + + if (start === 0 && deleteCount === friends.length) { + error = `Cannot replace a hasMany's state with a new state that contains duplicates.`; + + outcomeValues.forEach((item) => { + if (seen.has(item)) { + duplicates.add(item); + } else { + seen.add(item); + } + }); + + outcome = Array.from(new Set(outcomeValues)); + } else { + error = "Cannot splice a hasMany's state with a new state that contains duplicates."; + + const reducedFriends = friends.slice(); + reducedFriends.splice(start, deleteCount); + seen = new Set(reducedFriends); + const unique: User[] = []; + + added.forEach((item) => { + if (seen.has(item)) { + duplicates.add(item); + } else { + seen.add(item); + unique.push(item); + } + }); + reducedFriends.splice(start, 0, ...unique); + outcome = reducedFriends; + } + break; + } + } + + const hasDuplicates = duplicates.size > 0; + return { + hasDuplicates, + duplicates: Array.from(duplicates), + deduped: { + length: outcome.length, + membership: outcome, + ids: outcome.map((v) => v.id), + }, + unchanged: { + length: friends.length, + membership: friends.slice(), + ids: friends.map((v) => v.id), + }, + error, + }; +} + +async function applyMutation(assert: Assert, store: Store, record: User, mutation: Mutation, rc: ReactiveContext) { + assert.ok(true, `LOG: applying "${mutation.name}" with ids [${mutation.values.map((v) => v.id).join(',')}]`); + + const { counters, fieldOrder } = rc; + const friendsIndex = fieldOrder.indexOf('friends'); + const initialFriendsCount = counters.friends; + if (initialFriendsCount === undefined) { + throw new Error('could not find counters.friends'); + } + + const result = generateAppliedMutation(store, record, mutation); + const initialIds = record.friends.map((f) => f.id).join(','); + + const shouldError = result.hasDuplicates && /* inline-macro-config */ !DEPRECATE_MANY_ARRAY_DUPLICATES; + const shouldDeprecate = + result.hasDuplicates && + /* inline-macro-config */ DEPRECATE_MANY_ARRAY_DUPLICATES && + /* inline-macro-config */ !DISABLE_6X_DEPRECATIONS; + const expected = shouldError ? result.unchanged : result.deduped; + + try { + switch (mutation.method) { + case 'push': + record.friends.push(...mutation.values.map((ref) => store.peekRecord(ref) as User)); + break; + case 'unshift': + record.friends.unshift(...mutation.values.map((ref) => store.peekRecord(ref) as User)); + break; + case 'splice': + record.friends.splice( + mutation.start?.(record) ?? 0, + mutation.deleteCount?.(record) ?? 0, + ...mutation.values.map((ref) => store.peekRecord(ref) as User) + ); + break; + } + assert.ok(!shouldError, `expected error ${shouldError ? '' : 'NOT '}to be thrown`); + if (shouldDeprecate) { + const expectedMessage = `${ + result.error + } This behavior is deprecated. Found duplicates for the following records within the new state provided to \`.friends\`\n\t- ${Array.from(result.duplicates) + .map((r) => recordIdentifierFor(r).lid) + .sort((a, b) => a.localeCompare(b)) + .join('\n\t- ')}`; + assert.expectDeprecation({ + id: 'ember-data:deprecate-many-array-duplicates', + until: '6.0', + count: 1, + message: expectedMessage, + }); + } + } catch (e) { + assert.ok(shouldError, `expected error ${shouldError ? '' : 'NOT '}to be thrown`); + const expectedMessage = shouldError + ? `${result.error} Found duplicates for the following records within the new state provided to \`.friends\`\n\t- ${Array.from(result.duplicates) + .map((r) => recordIdentifierFor(r).lid) + .sort((a, b) => a.localeCompare(b)) + .join('\n\t- ')}` + : ''; + assert.strictEqual((e as Error).message, expectedMessage, `error thrown has correct message: ${expectedMessage}`); + } + + const expectedIds = expected.ids.join(','); + + assert.strictEqual( + record.friends.length, + expected.length, + `the new state has the correct length of ${expected.length} after ${mutation.method}` + ); + assert.deepEqual( + record.friends.slice(), + expected.membership, + `the new state has the correct records [${expectedIds}] after ${mutation.method} (had [${record.friends + .map((f) => f.id) + .join(',')}])` + ); + assert.deepEqual( + record.hasMany('friends').ids(), + expected.ids, + `the new state has the correct ids on the reference [${expectedIds}] after ${mutation.method}` + ); + assert.strictEqual( + record.hasMany('friends').ids().length, + expected.length, + `the new state has the correct length on the reference of ${expected.length} after ${mutation.method}` + ); + assert.strictEqual( + record.friends.length, + new Set(record.friends).size, + `the new state has no duplicates after ${mutation.method}` + ); + + await settled(); + + const start = mutation.start?.(record) ?? 0; + const deleteCount = mutation.deleteCount?.(record) ?? 0; + const isReplace = + mutation.method === 'splice' && (deleteCount > 0 || (start === 0 && deleteCount === record.friends.length)); + + if (shouldError || (!isReplace && initialIds === expectedIds)) { + assert.strictEqual(counters.friends, initialFriendsCount, 'reactivity: friendsCount does not increment'); + } else { + assert.strictEqual(counters.friends, initialFriendsCount + 1, 'reactivity: friendsCount increments'); + } + assert + .dom(`li:nth-child(${friendsIndex + 1})`) + .hasText(`friends: [${expectedIds}]`, 'reactivity: friends are rendered'); +} + +function getStartingState() { + return [ + { name: 'empty friends', cb: (store: Store) => makeUser(store, []) }, + { name: '1 friend', cb: (store: Store) => makeUser(store, [krystanRef()]) }, + { name: '2 friends', cb: (store: Store) => makeUser(store, [krystanRef(), samRef()]) }, + ]; +} + +function getValues() { + return [ + { + name: 'with empty array', + values: [], + }, + { + name: 'with NO duplicates (compared to initial remote state)', + values: [ericRef()], + }, + { + name: 'with duplicates NOT present in initial remote state', + values: [ericRef(), ericRef()], + }, + { + name: 'with duplicates present in initial remote state', + values: [krystanRef()], + }, + { + name: 'with all the duplicates', + values: [ericRef(), ericRef(), krystanRef()], + }, + ]; +} + +function generateMutations(baseMutation: Omit): Mutation[] { + return getValues().map((v) => ({ + ...baseMutation, + name: `${baseMutation.name} ${v.name}`, + values: v.values, + })); +} + +function getMutations(): Mutation[] { + return [ + ...generateMutations({ + name: 'push', + method: 'push', + }), + ...generateMutations({ + name: 'unshift', + method: 'unshift', + }), + ...generateMutations({ + name: 'replace', + method: 'splice', + start: () => 0, + deleteCount: (user) => user.friends.length, + }), + ...generateMutations({ + name: 'splice with delete (to beginning)', + method: 'splice', + start: () => 0, + deleteCount: (user) => (user.friends.length === 0 ? 0 : 1), + }), + ...generateMutations({ + name: 'splice (to beginning)', + method: 'splice', + start: () => 0, + deleteCount: () => 0, + }), + ...generateMutations({ + name: 'splice (to middle)', + method: 'splice', + start: (user) => Math.floor(user.friends.length / 2), + deleteCount: () => 0, + }), + ...generateMutations({ + name: 'splice (to end)', + method: 'splice', + start: (user) => user.friends.length, + deleteCount: () => 0, + }), + ]; +} + +module('Integration | Relationships | Collection | Mutation', function (hooks) { + setupRenderingTest(hooks); + + hooks.beforeEach(function () { + this.owner.register('model:user', User); + }); + + getStartingState().forEach((startingState) => { + module(`Starting state: ${startingState.name}`, function () { + getMutations().forEach((mutation) => { + module(`Mutation: ${mutation.name}`, function () { + getMutations().forEach((mutation2) => { + test(`followed by Mutation: ${mutation2.name}`, async function (assert) { + const store = this.owner.lookup('service:store') as Store; + const user = startingState.cb(store); + const rc = await reactiveContext.call(this, user, { + identity: null, + type: 'user', + fields: [{ name: 'friends', kind: 'hasMany', type: 'user', options: { async: false, inverse: null } }], + }); + rc.reset(); + + await applyMutation(assert, store, user, mutation, rc); + await applyMutation(assert, store, user, mutation2, rc); + }); + }); + }); + }); + }); + }); +}); diff --git a/tests/main/tests/integration/relationships/has-many-test.js b/tests/main/tests/integration/relationships/has-many-test.js index a6a9326c048..85e02673651 100644 --- a/tests/main/tests/integration/relationships/has-many-test.js +++ b/tests/main/tests/integration/relationships/has-many-test.js @@ -12,6 +12,7 @@ import JSONAPISerializer from '@ember-data/serializer/json-api'; import RESTSerializer from '@ember-data/serializer/rest'; import { deprecatedTest } from '@ember-data/unpublished-test-infra/test-support/deprecated-test'; import testInDebug from '@ember-data/unpublished-test-infra/test-support/test-in-debug'; +import { DEPRECATE_ARRAY_LIKE, DEPRECATE_NON_EXPLICIT_POLYMORPHISM } from '@warp-drive/build-config/deprecations'; import { getRelationshipStateForRecord, hasRelationshipForRecord } from '../../helpers/accessors'; @@ -1403,6 +1404,59 @@ module('integration/relationships/has_many - Has-Many Relationships', function ( }); }); + deprecatedTest( + 'PromiseArray proxies createRecord to its ManyArray once the hasMany is loaded', + { id: 'ember-data:deprecate-promise-many-array-behaviors', until: '5.0', count: 1 }, + async function (assert) { + assert.expect(4); + class Post extends Model { + @attr title; + @hasMany('comment', { async: true, inverse: 'message' }) comments; + } + + class Comment extends Model { + @attr body; + @belongsTo('post', { async: false, inverse: 'comments' }) message; + } + this.owner.register('model:post', Post); + this.owner.register('model:comment', Comment); + + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); + + adapter.findHasMany = function (store, snapshot, link, relationship) { + return Promise.resolve({ + data: [ + { id: '1', type: 'comment', attributes: { body: 'First' } }, + { id: '2', type: 'comment', attributes: { body: 'Second' } }, + ], + }); + }; + const post = store.push({ + data: { + type: 'post', + id: '1', + relationships: { + comments: { + links: { + related: 'someLink', + }, + }, + }, + }, + }); + + await post.comments.then(function (comments) { + assert.true(comments.isLoaded, 'comments are loaded'); + assert.strictEqual(comments.length, 2, 'comments have 2 length'); + + const newComment = post.comments.createRecord({ body: 'Third' }); + assert.strictEqual(newComment.body, 'Third', 'new comment is returned'); + assert.strictEqual(comments.length, 3, 'comments have 3 length, including new record'); + }); + } + ); + test('An updated `links` value should invalidate a relationship cache', async function (assert) { assert.expect(8); class Post extends Model { @@ -1585,6 +1639,165 @@ module('integration/relationships/has_many - Has-Many Relationships', function ( assert.strictEqual(igor.messages.at(0)?.body, 'Well I thought the title was fine'); }); + deprecatedTest( + 'Type can be inferred from the key of a hasMany relationship', + { id: 'ember-data:deprecate-non-strict-relationships', until: '5.0', count: 1 }, + async function (assert) { + assert.expect(1); + + const User = Model.extend({ + name: attr(), + contacts: hasMany({ inverse: null, async: false }), + }); + + const Contact = Model.extend({ + name: attr(), + user: belongsTo('user', { async: false, inverse: null }), + }); + + this.owner.register('model:user', User); + this.owner.register('model:contact', Contact); + + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); + + adapter.findRecord = function () { + return { + data: { + id: '1', + type: 'user', + relationships: { + contacts: { + data: [{ id: '1', type: 'contact' }], + }, + }, + }, + }; + }; + + const user = store.push({ + data: { + type: 'user', + id: '1', + relationships: { + contacts: { + data: [{ type: 'contact', id: '1' }], + }, + }, + }, + included: [ + { + type: 'contact', + id: '1', + }, + ], + }); + const contacts = await user.contacts; + assert.strictEqual(contacts.length, 1, 'The contacts relationship is correctly set up'); + } + ); + + deprecatedTest( + 'Type can be inferred from the key of an async hasMany relationship', + { id: 'ember-data:deprecate-non-strict-relationships', until: '5.0', count: 1 }, + async function (assert) { + class User extends Model { + @attr name; + @hasMany('message', { polymorphic: true, async: false, inverse: 'user' }) messages; + @hasMany({ async: true, inverse: null }) contacts; + } + this.owner.register('model:user', User); + + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); + + adapter.findRecord = function (store, type, ids, snapshots) { + return { + data: { + id: '1', + type: 'user', + relationships: { + contacts: { + data: [{ id: '1', type: 'contact' }], + }, + }, + }, + }; + }; + + store.push({ + data: { + type: 'user', + id: '1', + relationships: { + contacts: { + data: [{ type: 'contact', id: '1' }], + }, + }, + }, + included: [ + { + type: 'contact', + id: '1', + }, + ], + }); + + const user = await store.findRecord('user', '1'); + const contacts = await user.contacts; + assert.strictEqual(contacts.length, 1, 'The contacts relationship is correctly set up'); + } + ); + + deprecatedTest( + 'Polymorphic relationships work with a hasMany whose type is inferred', + { id: 'ember-data:deprecate-non-strict-relationships', until: '5.0', count: 1 }, + async function (assert) { + class User extends Model { + @attr name; + @hasMany('message', { polymorphic: true, async: false, inverse: 'user' }) messages; + @hasMany({ async: false, polymorphic: true, inverse: null }) contacts; + } + this.owner.register('model:user', User); + + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); + + adapter.findRecord = function (store, type, ids, snapshots) { + return { data: { id: '1', type: 'user' } }; + }; + + store.push({ + data: { + type: 'user', + id: '1', + relationships: { + contacts: { + data: [ + { type: 'email', id: '1' }, + { type: 'phone', id: '2' }, + ], + }, + }, + }, + included: [ + { + type: 'email', + id: '1', + }, + { + type: 'phone', + id: '2', + }, + ], + }); + const user = await store.findRecord('user', '1'); + const contacts = await user.contacts; + + assert.strictEqual(contacts.length, 2, 'The contacts relationship is correctly set up'); + } + ); + test('Polymorphic relationships work with a hasMany whose inverse is null', async function (assert) { assert.expect(1); class User extends Model { @@ -1700,7 +1913,9 @@ module('integration/relationships/has_many - Has-Many Relationships', function ( } catch (e) { assert.strictEqual( e.message, - "The 'post' type does not implement 'comment' and thus cannot be assigned to the 'comments' relationship in 'post'. If this relationship should be polymorphic, mark post.comments as `polymorphic: true` and post.message as implementing it via `as: 'comment'`.", + DEPRECATE_NON_EXPLICIT_POLYMORPHISM + ? "The 'post' type does not implement 'comment' and thus cannot be assigned to the 'comments' relationship in 'post'. Make it a descendant of 'comment' or use a mixin of the same name." + : "The 'post' type does not implement 'comment' and thus cannot be assigned to the 'comments' relationship in 'post'. If this relationship should be polymorphic, mark post.comments as `polymorphic: true` and post.message as implementing it via `as: 'comment'`.", 'should throw' ); } @@ -1775,7 +1990,9 @@ module('integration/relationships/has_many - Has-Many Relationships', function ( function () { user.messages.push(anotherUser); }, - `The schema for the relationship 'user' on 'user' type does not correctly implement 'message' and thus cannot be assigned to the 'messages' relationship in 'user'. If using this record in this polymorphic relationship is desired, correct the errors in the schema shown below: + DEPRECATE_NON_EXPLICIT_POLYMORPHISM + ? "The 'user' type does not implement 'message' and thus cannot be assigned to the 'messages' relationship in 'user'. Make it a descendant of 'message' or use a mixin of the same name." + : `The schema for the relationship 'user' on 'user' type does not correctly implement 'message' and thus cannot be assigned to the 'messages' relationship in 'user'. If using this record in this polymorphic relationship is desired, correct the errors in the schema shown below: \`\`\` { @@ -1907,7 +2124,9 @@ module('integration/relationships/has_many - Has-Many Relationships', function ( function () { user.messages.push(anotherUser); }, - `No 'user' field exists on 'user'. To use this type in the polymorphic relationship 'user.messages' the relationships schema definition for user should include: + DEPRECATE_NON_EXPLICIT_POLYMORPHISM + ? "The 'user' type does not implement 'message' and thus cannot be assigned to the 'messages' relationship in 'user'. Make it a descendant of 'message' or use a mixin of the same name." + : `No 'user' field exists on 'user'. To use this type in the polymorphic relationship 'user.messages' the relationships schema definition for user should include: \`\`\` { @@ -2753,6 +2972,21 @@ If using this relationship in a polymorphic manner is desired, the relationships assert.strictEqual(page.chapter, chapter, 'Page has a chapter after rollback attributes'); }); + testInDebug('Passing a model as type to hasMany should not work', function (assert) { + assert.expect(3); + + assert.expectAssertion(() => { + const User = Model.extend(); + + Model.extend({ + users: hasMany(User, { async: false, inverse: null }), + }); + }, /The first argument to hasMany must be a string/); + + assert.expectDeprecation({ id: 'ember-data:deprecate-early-static' }); + assert.expectDeprecation({ id: 'ember-data:deprecate-non-strict-relationships' }); + }); + test('Relationship.clear removes all records correctly', async function (assert) { class Post extends Model { @attr title; @@ -2822,7 +3056,13 @@ If using this relationship in a polymorphic manner is desired, the relationships ); const postComments = await post.comments; - postComments.length = 0; + + if (DEPRECATE_ARRAY_LIKE) { + postComments.clear(); + assert.expectDeprecation({ id: 'ember-data:deprecate-array-like' }); + } else { + postComments.length = 0; + } assert.deepEqual( comments.map((comment) => comment.post), @@ -3656,6 +3896,54 @@ If using this relationship in a polymorphic manner is desired, the relationships }); }); + deprecatedTest( + 'PromiseArray proxies createRecord to its ManyArray before the hasMany is loaded', + { id: 'ember-data:deprecate-promise-many-array-behaviors', until: '5.0', count: 1 }, + async function (assert) { + class Post extends Model { + @attr title; + @hasMany('comment', { async: true, inverse: 'message' }) comments; + } + class Comment extends Model { + @attr body; + @belongsTo('post', { async: false, inverse: 'comments' }) message; + } + this.owner.register('model:post', Post); + this.owner.register('model:comment', Comment); + + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); + + adapter.findHasMany = function (store, record, link, relationship) { + return Promise.resolve({ + data: [ + { id: '1', type: 'comment', attributes: { body: 'First' } }, + { id: '2', type: 'comment', attributes: { body: 'Second' } }, + ], + }); + }; + + const post = store.push({ + data: { + type: 'post', + id: '1', + relationships: { + comments: { + links: { + related: 'someLink', + }, + }, + }, + }, + }); + + const commentsPromise = post.comments; + commentsPromise.createRecord(); + const comments = await commentsPromise; + assert.strictEqual(comments.length, 3, 'comments have 3 length, including new record'); + } + ); + test('deleteRecord + unloadRecord', async function (assert) { class User extends Model { @attr name; @@ -4029,4 +4317,181 @@ If using this relationship in a polymorphic manner is desired, the relationships assert.strictEqual(person.phoneNumbers.length, 1); } ); + + deprecatedTest( + 'a synchronous hasMany record array should only remove object(s) if found in collection', + { + id: 'ember-data:deprecate-array-like', + count: 3, + until: '5.0', + }, + async function (assert) { + class Person extends Model { + @attr() + name; + @belongsTo('tag', { async: false, inverse: 'people' }) + tag; + } + + class Tag extends Model { + @hasMany('person', { async: false, inverse: 'tag' }) + people; + } + + this.owner.register('model:person', Person); + this.owner.register('model:tag', Tag); + + const store = this.owner.lookup('service:store'); + // eslint-disable-next-line no-unused-vars + const [tag, scumbagInRecordArray, _person2, scumbagNotInRecordArray] = store.push({ + data: [ + { + type: 'tag', + id: '1', + relationships: { + people: { + data: [ + { type: 'person', id: '1' }, + { type: 'person', id: '2' }, + ], + }, + }, + }, + { + type: 'person', + id: '1', + attributes: { + name: 'Scumbag Dale', + }, + }, + { + type: 'person', + id: '2', + attributes: { + name: 'Scumbag Tom', + }, + }, + { + type: 'person', + id: '3', + attributes: { + name: 'Scumbag Ross', + }, + }, + ], + }); + + const recordArray = tag.people; + + recordArray.removeObject(scumbagNotInRecordArray); + + assert.strictEqual( + recordArray.length, + 2, + 'Record array unchanged after attempting to remove object not found in collection' + ); + + recordArray.removeObject(scumbagInRecordArray); + + let didRemoveObject = recordArray.length === 1 && !recordArray.includes(scumbagInRecordArray); + assert.true(didRemoveObject, 'Record array successfully removed expected object from collection'); + + recordArray.push(scumbagInRecordArray); + + const scumbagsToRemove = [scumbagInRecordArray, scumbagNotInRecordArray]; + recordArray.removeObjects(scumbagsToRemove); + + didRemoveObject = recordArray.length === 1 && !recordArray.includes(scumbagInRecordArray); + assert.true(didRemoveObject, 'Record array only removes objects in list that are found in collection'); + } + ); + + deprecatedTest( + 'an asynchronous hasMany record array should only remove object(s) if found in collection', + { + id: 'ember-data:deprecate-promise-many-array-behaviors', + count: 6, + until: '5.0', + }, + async function (assert) { + class Person extends Model { + @attr() + name; + @belongsTo('tag', { async: false, inverse: 'people' }) + tag; + } + + class Tag extends Model { + @hasMany('person', { async: true, inverse: 'tag' }) + people; + } + + const store = this.owner.lookup('service:store'); + this.owner.register('model:person', Person); + this.owner.register('model:tag', Tag); + + // eslint-disable-next-line no-unused-vars + const [tag, scumbagInRecordArray, _person2, scumbagNotInRecordArray] = store.push({ + data: [ + { + type: 'tag', + id: '1', + relationships: { + people: { + data: [ + { type: 'person', id: '1' }, + { type: 'person', id: '2' }, + ], + }, + }, + }, + { + type: 'person', + id: '1', + attributes: { + name: 'Scumbag Dale', + }, + }, + { + type: 'person', + id: '2', + attributes: { + name: 'Scumbag Tom', + }, + }, + { + type: 'person', + id: '3', + attributes: { + name: 'Scumbag Ross', + }, + }, + ], + }); + + const recordArray = tag.people; + + recordArray.removeObject(scumbagNotInRecordArray); + + assert.strictEqual( + recordArray.length, + 2, + 'Record array unchanged after attempting to remove object not found in collection' + ); + + recordArray.removeObject(scumbagInRecordArray); + + let didRemoveObject = recordArray.length === 1 && !recordArray.includes(scumbagInRecordArray); + assert.true(didRemoveObject, 'Record array successfully removed expected object from collection'); + + recordArray.pushObject(scumbagInRecordArray); + + const scumbagsToRemove = [scumbagInRecordArray, scumbagNotInRecordArray]; + recordArray.removeObjects(scumbagsToRemove); + + didRemoveObject = recordArray.length === 1 && !recordArray.includes(scumbagInRecordArray); + assert.true(didRemoveObject, 'Record array only removes objects in list that are found in collection'); + assert.expectDeprecation({ id: 'ember-data:deprecate-array-like', count: 4 }); + } + ); }); diff --git a/tests/main/tests/integration/relationships/inverse-relationship-load-test.js b/tests/main/tests/integration/relationships/inverse-relationship-load-test.js index c9b518518dd..3641620be67 100644 --- a/tests/main/tests/integration/relationships/inverse-relationship-load-test.js +++ b/tests/main/tests/integration/relationships/inverse-relationship-load-test.js @@ -5,6 +5,7 @@ import { setupTest } from 'ember-qunit'; import JSONAPIAdapter from '@ember-data/adapter/json-api'; import Model, { attr, belongsTo, hasMany } from '@ember-data/model'; import JSONAPISerializer from '@ember-data/serializer/json-api'; +import { deprecatedTest } from '@ember-data/unpublished-test-infra/test-support/deprecated-test'; module('inverse relationship load test', function (hooks) { let store; @@ -23,6 +24,186 @@ module('inverse relationship load test', function (hooks) { ); }); + deprecatedTest( + 'one-to-many - findHasMany/implicit inverse - adds parent relationship information to the payload if it is not included/added by the serializer', + { id: 'ember-data:deprecate-non-strict-relationships', until: '5.0', count: 2 }, + async function (assert) { + const { owner } = this; + + owner.register( + 'adapter:application', + JSONAPIAdapter.extend({ + deleteRecord: () => Promise.resolve({ data: null }), + findHasMany: () => { + return Promise.resolve({ + data: [ + { + id: '1', + type: 'dog', + attributes: { + name: 'Scooby', + }, + }, + { + id: '2', + type: 'dog', + attributes: { + name: 'Scrappy', + }, + }, + ], + }); + }, + }) + ); + + class Person extends Model { + @hasMany('dog', { + async: true, + }) + dogs; + } + owner.register('model:person', Person); + + class Dog extends Model { + @belongsTo('person', { + async: true, + }) + person; + } + owner.register('model:dog', Dog); + + const person = store.push({ + data: { + type: 'person', + id: '1', + attributes: { + name: 'John Churchill', + }, + relationships: { + dogs: { + links: { + related: 'http://example.com/person/1/dogs', + }, + }, + }, + }, + }); + + const dogs = await person.dogs; + assert.false(person.hasMany('dogs').hasManyRelationship.state.isEmpty, 'relationship state was set up correctly'); + + assert.strictEqual(dogs.length, 2, 'hasMany relationship has correct number of records'); + const dog1 = dogs.at(0); + const dogPerson1 = await dog1.person; + assert.strictEqual( + dogPerson1.id, + '1', + 'dog.person inverse relationship is set up correctly when adapter does not include parent relationships in data.relationships' + ); + const dogPerson2 = await dogs.at(1).person; + assert.strictEqual( + dogPerson2.id, + '1', + 'dog.person inverse relationship is set up correctly when adapter does not include parent relationships in data.relationships' + ); + + await dog1.destroyRecord(); + assert.strictEqual(dogs.length, 1, 'record removed from hasMany relationship after deletion'); + assert.strictEqual(dogs.at(0).id, '2', 'hasMany relationship has correct records'); + } + ); + + deprecatedTest( + 'one-to-many (left hand async, right hand sync) - findHasMany/implicit inverse - adds parent relationship information to the payload if it is not included/added by the serializer', + { id: 'ember-data:deprecate-non-strict-relationships', until: '5.0', count: 2 }, + async function (assert) { + const { owner } = this; + + owner.register( + 'adapter:application', + JSONAPIAdapter.extend({ + deleteRecord: () => Promise.resolve({ data: null }), + findHasMany: () => { + return Promise.resolve({ + data: [ + { + id: '1', + type: 'dog', + attributes: { + name: 'Scooby', + }, + }, + { + id: '2', + type: 'dog', + attributes: { + name: 'Scrappy', + }, + }, + ], + }); + }, + }) + ); + + class Person extends Model { + @hasMany('dog', { + async: true, + }) + dogs; + } + owner.register('model:person', Person); + + class Dog extends Model { + @belongsTo('person', { + async: false, + }) + person; + } + owner.register('model:dog', Dog); + + const person = store.push({ + data: { + type: 'person', + id: '1', + attributes: { + name: 'John Churchill', + }, + relationships: { + dogs: { + links: { + related: 'http://example.com/person/1/dogs', + }, + }, + }, + }, + }); + + const dogs = await person.dogs; + assert.false(person.hasMany('dogs').hasManyRelationship.state.isEmpty, 'relationship state was set up correctly'); + + assert.strictEqual(dogs.length, 2, 'hasMany relationship has correct number of records'); + const dog1 = dogs.at(0); + const dogPerson1 = await dog1.person; + assert.strictEqual( + dogPerson1.id, + '1', + 'dog.person inverse relationship is set up correctly when adapter does not include parent relationships in data.relationships' + ); + const dogPerson2 = await dogs.at(1).person; + assert.strictEqual( + dogPerson2.id, + '1', + 'dog.person inverse relationship is set up correctly when adapter does not include parent relationships in data.relationships' + ); + + await dog1.destroyRecord(); + assert.strictEqual(dogs.length, 1, 'record removed from hasMany relationship after deletion'); + assert.strictEqual(dogs.at(0).id, '2', 'hasMany relationship has correct records'); + } + ); + test('one-to-many - findHasMany/explicit inverse - adds parent relationship information to the payload if it is not included/added by the serializer', async function (assert) { const { owner } = this; @@ -282,6 +463,158 @@ module('inverse relationship load test', function (hooks) { assert.strictEqual(dogs.at(0).id, '2'); }); + deprecatedTest( + 'one-to-one - findBelongsTo/implicit inverse - ensures inverse relationship is set up when payload does not return parent relationship info', + { id: 'ember-data:deprecate-non-strict-relationships', until: '5.0', count: 2 }, + async function (assert) { + const { owner } = this; + + owner.register( + 'adapter:application', + JSONAPIAdapter.extend({ + deleteRecord() { + return Promise.resolve({ + data: null, + }); + }, + findBelongsTo() { + return Promise.resolve({ + data: { + id: '1', + type: 'dog', + attributes: { + name: 'Scooby', + }, + }, + }); + }, + }) + ); + + class Person extends Model { + @attr() + name; + @belongsTo('dog', { async: true }) + favoriteDog; + } + owner.register('model:person', Person); + + class Dog extends Model { + @attr() + name; + @belongsTo('person', { async: true }) + person; + } + owner.register('model:dog', Dog); + + const person = store.push({ + data: { + type: 'person', + id: '1', + attributes: { + name: 'John Churchill', + }, + relationships: { + favoriteDog: { + links: { + related: 'http://example.com/person/1/favorite-dog', + }, + }, + }, + }, + }); + + let favoriteDog = await person.favoriteDog; + assert.false(person.belongsTo('favoriteDog').belongsToRelationship.state.isEmpty); + assert.strictEqual(favoriteDog.id, '1', 'favoriteDog id is set correctly'); + const favoriteDogPerson = await favoriteDog.person; + assert.strictEqual( + favoriteDogPerson.id, + '1', + 'favoriteDog.person inverse relationship is set up correctly when adapter does not include parent relationships in data.relationships' + ); + await favoriteDog.destroyRecord(); + favoriteDog = await person.favoriteDog; + assert.strictEqual(favoriteDog, null); + } + ); + + deprecatedTest( + 'one-to-one (left hand async, right hand sync) - findBelongsTo/implicit inverse - ensures inverse relationship is set up when payload does not return parent relationship info', + { id: 'ember-data:deprecate-non-strict-relationships', until: '5.0', count: 2 }, + async function (assert) { + const { owner } = this; + + owner.register( + 'adapter:application', + JSONAPIAdapter.extend({ + deleteRecord() { + return Promise.resolve({ + data: null, + }); + }, + findBelongsTo() { + return Promise.resolve({ + data: { + id: '1', + type: 'dog', + attributes: { + name: 'Scooby', + }, + }, + }); + }, + }) + ); + + class Person extends Model { + @attr() + name; + @belongsTo('dog', { async: true }) + favoriteDog; + } + owner.register('model:person', Person); + + class Dog extends Model { + @attr() + name; + @belongsTo('person', { async: true }) + person; + } + owner.register('model:dog', Dog); + + const person = store.push({ + data: { + type: 'person', + id: '1', + attributes: { + name: 'John Churchill', + }, + relationships: { + favoriteDog: { + links: { + related: 'http://example.com/person/1/favorite-dog', + }, + }, + }, + }, + }); + + let favoriteDog = await person.favoriteDog; + assert.false(person.belongsTo('favoriteDog').belongsToRelationship.state.isEmpty); + assert.strictEqual(favoriteDog.id, '1', 'favoriteDog id is set correctly'); + const favoriteDogPerson = await favoriteDog.person; + assert.strictEqual( + favoriteDogPerson.id, + '1', + 'favoriteDog.person inverse relationship is set up correctly when adapter does not include parent relationships in data.relationships' + ); + await favoriteDog.destroyRecord(); + favoriteDog = await person.favoriteDog; + assert.strictEqual(favoriteDog, null); + } + ); + test('one-to-one - findBelongsTo/explicit inverse - ensures inverse relationship is set up when payload does not return parent relationship info', async function (assert) { const { owner } = this; @@ -490,6 +823,176 @@ module('inverse relationship load test', function (hooks) { assert.strictEqual(favoriteDog, null); }); + deprecatedTest( + 'many-to-many - findHasMany/implicit inverse - adds parent relationship information to the payload if it is not included/added by the serializer', + { id: 'ember-data:deprecate-non-strict-relationships', until: '5.0', count: 2 }, + async function (assert) { + const { owner } = this; + + owner.register( + 'adapter:application', + JSONAPIAdapter.extend({ + deleteRecord: () => Promise.resolve({ data: null }), + findHasMany: () => { + return Promise.resolve({ + data: [ + { + id: '1', + type: 'dog', + attributes: { + name: 'Scooby', + }, + }, + { + id: '2', + type: 'dog', + attributes: { + name: 'Scrappy', + }, + }, + ], + }); + }, + }) + ); + + class Person extends Model { + @hasMany('dog', { + async: true, + }) + dogs; + } + owner.register('model:person', Person); + + class Dog extends Model { + @hasMany('person', { + async: true, + }) + walkers; + } + owner.register('model:dog', Dog); + + const person = store.push({ + data: { + type: 'person', + id: '1', + attributes: { + name: 'John Churchill', + }, + relationships: { + dogs: { + links: { + related: 'http://example.com/person/1/dogs', + }, + }, + }, + }, + }); + + const dogs = await person.dogs; + assert.false(person.hasMany('dogs').hasManyRelationship.state.isEmpty); + + assert.strictEqual(dogs.length, 2, 'left hand side relationship is set up with correct number of records'); + const [dog1, dog2] = dogs.slice(); + const dog1Walkers = await dog1.walkers; + assert.strictEqual(dog1Walkers.length, 1, 'dog1.walkers inverse relationship includes correct number of records'); + assert.strictEqual(dog1Walkers.at(0).id, '1', 'dog1.walkers inverse relationship is set up correctly'); + + const dog2Walkers = await dog2.walkers; + assert.strictEqual(dog2Walkers.length, 1, 'dog2.walkers inverse relationship includes correct number of records'); + assert.strictEqual(dog2Walkers.at(0).id, '1', 'dog2.walkers inverse relationship is set up correctly'); + + await dog1.destroyRecord(); + assert.strictEqual(dogs.length, 1, 'person.dogs relationship was updated when record removed'); + assert.strictEqual(dogs.at(0).id, '2', 'person.dogs relationship has the correct records'); + } + ); + + deprecatedTest( + 'many-to-many (left hand async, right hand sync) - findHasMany/implicit inverse - adds parent relationship information to the payload if it is not included/added by the serializer', + { id: 'ember-data:deprecate-non-strict-relationships', until: '5.0', count: 2 }, + async function (assert) { + const { owner } = this; + + owner.register( + 'adapter:application', + JSONAPIAdapter.extend({ + deleteRecord: () => Promise.resolve({ data: null }), + findHasMany: () => { + return Promise.resolve({ + data: [ + { + id: '1', + type: 'dog', + attributes: { + name: 'Scooby', + }, + }, + { + id: '2', + type: 'dog', + attributes: { + name: 'Scrappy', + }, + }, + ], + }); + }, + }) + ); + + class Person extends Model { + @hasMany('dog', { + async: true, + }) + dogs; + } + owner.register('model:person', Person); + + class Dog extends Model { + @hasMany('person', { + async: false, + }) + walkers; + } + owner.register('model:dog', Dog); + + const person = store.push({ + data: { + type: 'person', + id: '1', + attributes: { + name: 'John Churchill', + }, + relationships: { + dogs: { + links: { + related: 'http://example.com/person/1/dogs', + }, + }, + }, + }, + }); + + const dogs = await person.dogs; + assert.false(person.hasMany('dogs').hasManyRelationship.state.isEmpty); + + assert.strictEqual(dogs.length, 2, 'left hand side relationship is set up with correct number of records'); + const [dog1, dog2] = dogs.slice(); + const dog1Walkers = await dog1.walkers; + assert.strictEqual(dog1Walkers.length, 1, 'dog1.walkers inverse relationship includes correct number of records'); + assert.strictEqual(dog1Walkers.at(0).id, '1', 'dog1.walkers inverse relationship is set up correctly'); + + const dog2Walkers = await dog2.walkers; + assert.strictEqual(dog2Walkers.length, 1, 'dog2.walkers inverse relationship includes correct number of records'); + assert.strictEqual(dog2Walkers.at(0).id, '1', 'dog2.walkers inverse relationship is set up correctly'); + + await dog1.destroyRecord(); + assert.strictEqual(dogs.length, 1, 'person.dogs relationship was updated when record removed'); + assert.strictEqual(dogs.at(0).id, '2', 'person.dogs relationship has the correct records'); + } + ); + test('many-to-many - findHasMany/explicit inverse - adds parent relationship information to the payload if it is not included/added by the serializer', async function (assert) { const { owner } = this; @@ -656,6 +1159,158 @@ module('inverse relationship load test', function (hooks) { assert.strictEqual(dogs.at(0).id, '2', 'person.dogs relationship has the correct records'); }); + deprecatedTest( + 'many-to-one - findBelongsTo/implicit inverse - adds parent relationship information to the payload if it is not included/added by the serializer', + { id: 'ember-data:deprecate-non-strict-relationships', until: '5.0', count: 2 }, + async function (assert) { + const { owner } = this; + + owner.register( + 'adapter:application', + JSONAPIAdapter.extend({ + deleteRecord: () => Promise.resolve({ data: null }), + findBelongsTo: () => { + return Promise.resolve({ + data: { + type: 'person', + id: '1', + attributes: { + name: 'John Churchill', + }, + }, + }); + }, + }) + ); + + class Person extends Model { + @hasMany('dog', { + async: true, + }) + dogs; + } + owner.register('model:person', Person); + + class Dog extends Model { + @belongsTo('person', { + async: true, + }) + person; + } + owner.register('model:dog', Dog); + + let dog = store.push({ + data: { + type: 'dog', + id: '1', + attributes: { + name: 'A Really Good Dog', + }, + relationships: { + person: { + links: { + related: 'http://example.com/person/1', + }, + }, + }, + }, + }); + + const person = await dog.person; + assert.false( + dog.belongsTo('person').belongsToRelationship.state.isEmpty, + 'belongsTo relationship state was populated' + ); + assert.strictEqual(person.id, '1', 'dog.person relationship is correctly set up'); + + const dogs = await person.dogs; + + assert.strictEqual(dogs.length, 1, 'person.dogs inverse relationship includes correct number of records'); + const [dog1] = dogs.slice(); + assert.strictEqual(dog1.id, '1', 'dog1.person inverse relationship is set up correctly'); + + await person.destroyRecord(); + dog = await dog.person; + assert.strictEqual(dog, null, 'record deleted removed from belongsTo relationship'); + } + ); + + deprecatedTest( + 'many-to-one (left hand async, right hand sync) - findBelongsTo/implicit inverse - adds parent relationship information to the payload if it is not included/added by the serializer', + { id: 'ember-data:deprecate-non-strict-relationships', until: '5.0', count: 2 }, + async function (assert) { + const { owner } = this; + + owner.register( + 'adapter:application', + JSONAPIAdapter.extend({ + deleteRecord: () => Promise.resolve({ data: null }), + findBelongsTo: () => { + return Promise.resolve({ + data: { + type: 'person', + id: '1', + attributes: { + name: 'John Churchill', + }, + }, + }); + }, + }) + ); + + class Person extends Model { + @hasMany('dog', { + async: false, + }) + dogs; + } + owner.register('model:person', Person); + + class Dog extends Model { + @belongsTo('person', { + async: true, + }) + person; + } + owner.register('model:dog', Dog); + + let dog = store.push({ + data: { + type: 'dog', + id: '1', + attributes: { + name: 'A Really Good Dog', + }, + relationships: { + person: { + links: { + related: 'http://example.com/person/1', + }, + }, + }, + }, + }); + + const person = await dog.person; + assert.false( + dog.belongsTo('person').belongsToRelationship.state.isEmpty, + 'belongsTo relationship state was populated' + ); + assert.strictEqual(person.id, '1', 'dog.person relationship is correctly set up'); + + const dogs = await person.dogs; + + assert.strictEqual(dogs.length, 1, 'person.dogs inverse relationship includes correct number of records'); + const [dog1] = dogs.slice(); + assert.strictEqual(dog1.id, '1', 'dog1.person inverse relationship is set up correctly'); + + await person.destroyRecord(); + dog = await dog.person; + assert.strictEqual(dog, null, 'record deleted removed from belongsTo relationship'); + } + ); + test('many-to-one - findBelongsTo/explicit inverse - adds parent relationship information to the payload if it is not included/added by the serializer', async function (assert) { const { owner } = this; @@ -804,91 +1459,1701 @@ module('inverse relationship load test', function (hooks) { assert.strictEqual(dog, null, 'record deleted removed from belongsTo relationship'); }); - test('one-to-many - ids/non-link/explicit inverse - ids - records loaded through ids/findRecord are linked to the parent if the response from the server does not include relationship information', async function (assert) { - const { owner } = this; - - const scooby = { - id: '1', - type: 'dog', - attributes: { - name: 'Scooby', - }, - }; - - const scrappy = { - id: '2', - type: 'dog', - attributes: { - name: 'Scrappy', - }, - }; - - owner.register( - 'adapter:application', - JSONAPIAdapter.extend({ - deleteRecord: () => Promise.resolve({ data: null }), - findRecord: (_store, _type, id) => { - const dog = id === '1' ? scooby : scrappy; - return Promise.resolve({ - data: dog, - }); + deprecatedTest( + 'one-to-many - findHasMany/implicit inverse - fixes mismatched parent relationship information from the payload and deprecates', + { id: 'ember-data:deprecate-non-strict-relationships', debugOnly: true, until: '5.0', count: 2 }, + async function (assert) { + const { owner } = this; + + owner.register( + 'adapter:application', + JSONAPIAdapter.extend({ + deleteRecord: () => Promise.resolve({ data: null }), + findHasMany: () => { + return Promise.resolve({ + data: [ + { + id: '1', + type: 'dog', + attributes: { + name: 'Scooby', + }, + relationships: { + person: { + data: { + id: '2', + type: 'person', + }, + }, + }, + }, + { + id: '2', + type: 'dog', + attributes: { + name: 'Scrappy', + }, + relationships: { + person: { + data: { + id: '2', + type: 'person', + }, + }, + }, + }, + ], + }); + }, + }) + ); + + class Person extends Model { + @hasMany('dog', { + async: true, + }) + dogs; + } + owner.register('model:person', Person); + + class Dog extends Model { + @belongsTo('person', { + async: true, + }) + person; + } + owner.register('model:dog', Dog); + + const person = store.push({ + data: { + type: 'person', + id: '1', + attributes: { + name: 'John Churchill', + }, + relationships: { + dogs: { + links: { + related: 'http://example.com/person/1/dogs', + }, + }, + }, }, - }) - ); + }); - class Person extends Model { - @hasMany('dog', { - async: true, - inverse: 'pal', - }) - dogs; + await assert.expectAssertion(async () => { + await person.dogs; + }, /The record loaded at/); } - owner.register('model:person', Person); + ); + + deprecatedTest( + 'one-to-many (left hand async, right hand sync) - findHasMany/implicit inverse - fixes mismatched parent relationship information from the payload and deprecates', + { id: 'ember-data:deprecate-non-strict-relationships', debugOnly: true, until: '5.0', count: 2 }, + async function (assert) { + const { owner } = this; + + owner.register( + 'adapter:application', + JSONAPIAdapter.extend({ + deleteRecord: () => Promise.resolve({ data: null }), + findHasMany: () => { + return Promise.resolve({ + data: [ + { + id: '1', + type: 'dog', + attributes: { + name: 'Scooby', + }, + relationships: { + person: { + data: { + id: '2', + type: 'person', + }, + }, + }, + }, + { + id: '2', + type: 'dog', + attributes: { + name: 'Scrappy', + }, + relationships: { + person: { + data: { + id: '2', + type: 'person', + }, + }, + }, + }, + ], + }); + }, + }) + ); + + class Person extends Model { + @hasMany('dog', { + async: true, + }) + dogs; + } + owner.register('model:person', Person); + + class Dog extends Model { + @belongsTo('person', { + async: false, + }) + person; + } + owner.register('model:dog', Dog); + + const person = store.push({ + data: { + type: 'person', + id: '1', + attributes: { + name: 'John Churchill', + }, + relationships: { + dogs: { + links: { + related: 'http://example.com/person/1/dogs', + }, + }, + }, + }, + }); - class Dog extends Model { - @belongsTo('person', { - async: true, - inverse: 'dogs', - }) - pal; + await assert.expectAssertion(async () => { + await person.dogs; + }, /The record loaded at/); } - owner.register('model:dog', Dog); + ); + + deprecatedTest( + 'one-to-many - findHasMany/implicit inverse - fixes null relationship information from the payload and deprecates', + { id: 'ember-data:deprecate-non-strict-relationships', debugOnly: true, until: '5.0', count: 2 }, + async function (assert) { + const { owner } = this; + + owner.register( + 'adapter:application', + JSONAPIAdapter.extend({ + deleteRecord: () => Promise.resolve({ data: null }), + findHasMany: () => { + return Promise.resolve({ + data: [ + { + id: '1', + type: 'dog', + attributes: { + name: 'Scooby', + }, + relationships: { + person: { + data: null, + }, + }, + }, + { + id: '2', + type: 'dog', + attributes: { + name: 'Scrappy', + }, + relationships: { + person: { + data: null, + }, + }, + }, + ], + }); + }, + }) + ); + + class Person extends Model { + @hasMany('dog', { + async: true, + }) + dogs; + } + owner.register('model:person', Person); + + class Dog extends Model { + @belongsTo('person', { + async: true, + }) + person; + } + owner.register('model:dog', Dog); + + const person = store.push({ + data: { + type: 'person', + id: '1', + attributes: { + name: 'John Churchill', + }, + relationships: { + dogs: { + links: { + related: 'http://example.com/person/1/dogs', + }, + }, + }, + }, + }); - const person = store.push({ - data: { - type: 'person', - id: '1', - attributes: { - name: 'John Churchill', + await assert.expectAssertion(async () => { + await person.dogs; + }, /The record loaded at/); + } + ); + + deprecatedTest( + 'one-to-many (left hand async, right hand sync) - findHasMany/implicit inverse - fixes null relationship information from the payload and deprecates', + { id: 'ember-data:deprecate-non-strict-relationships', debugOnly: true, until: '5.0', count: 2 }, + async function (assert) { + const { owner } = this; + + owner.register( + 'adapter:application', + JSONAPIAdapter.extend({ + deleteRecord: () => Promise.resolve({ data: null }), + findHasMany: () => { + return Promise.resolve({ + data: [ + { + id: '1', + type: 'dog', + attributes: { + name: 'Scooby', + }, + relationships: { + person: { + data: null, + }, + }, + }, + { + id: '2', + type: 'dog', + attributes: { + name: 'Scrappy', + }, + relationships: { + person: { + data: null, + }, + }, + }, + ], + }); + }, + }) + ); + + class Person extends Model { + @hasMany('dog', { + async: true, + }) + dogs; + } + owner.register('model:person', Person); + + class Dog extends Model { + @belongsTo('person', { + async: false, + }) + person; + } + owner.register('model:dog', Dog); + + const person = store.push({ + data: { + type: 'person', + id: '1', + attributes: { + name: 'John Churchill', + }, + relationships: { + dogs: { + links: { + related: 'http://example.com/person/1/dogs', + }, + }, + }, }, - relationships: { - dogs: { - data: [ - { + }); + + await assert.expectAssertion(async () => { + await person.dogs; + }, /The record loaded at/); + } + ); + + deprecatedTest( + 'one-to-one - findBelongsTo/implicit inverse - fixes mismatched parent relationship information from the payload and deprecates', + { id: 'ember-data:deprecate-non-strict-relationships', debugOnly: true, until: '5.0', count: 2 }, + async function (assert) { + const { owner } = this; + + owner.register( + 'adapter:application', + JSONAPIAdapter.extend({ + deleteRecord: () => Promise.resolve({ data: null }), + findBelongsTo: () => { + return Promise.resolve({ + data: { id: '1', type: 'dog', + attributes: { + name: 'Scooby', + }, + relationships: { + person: { + data: { + id: '2', + type: 'person', + }, + }, + }, }, - { - id: '2', - type: 'dog', + }); + }, + }) + ); + + class Person extends Model { + @belongsTo('dog', { + async: true, + }) + dog; + } + owner.register('model:person', Person); + + class Dog extends Model { + @belongsTo('person', { + async: true, + }) + person; + } + owner.register('model:dog', Dog); + + const person = store.push({ + data: { + type: 'person', + id: '1', + attributes: { + name: 'John Churchill', + }, + relationships: { + dog: { + links: { + related: 'http://example.com/person/1/dog', }, - ], + }, }, }, - }, - }); + }); - const dogs = await person.dogs; - assert.false(person.hasMany('dogs').hasManyRelationship.state.isEmpty, 'relationship state was set up correctly'); + await assert.expectAssertion(async () => { + await person.dog; + }, /The record loaded at/); + } + ); + + deprecatedTest( + 'one-to-one (left hand async, right hand sync) - findBelongsTo/implicit inverse - fixes mismatched parent relationship information from the payload and deprecates', + { id: 'ember-data:deprecate-non-strict-relationships', debugOnly: true, until: '5.0', count: 2 }, + async function (assert) { + const { owner } = this; + + owner.register( + 'adapter:application', + JSONAPIAdapter.extend({ + deleteRecord: () => Promise.resolve({ data: null }), + findBelongsTo: () => { + return Promise.resolve({ + data: { + id: '1', + type: 'dog', + attributes: { + name: 'Scooby', + }, + relationships: { + person: { + data: { + id: '2', + type: 'person', + }, + }, + }, + }, + }); + }, + }) + ); + + class Person extends Model { + @belongsTo('dog', { + async: true, + }) + dog; + } + owner.register('model:person', Person); + + class Dog extends Model { + @belongsTo('person', { + async: false, + }) + person; + } + owner.register('model:dog', Dog); + + const person = store.push({ + data: { + type: 'person', + id: '1', + attributes: { + name: 'John Churchill', + }, + relationships: { + dog: { + links: { + related: 'http://example.com/person/1/dog', + }, + }, + }, + }, + }); - assert.strictEqual(dogs.length, 2, 'hasMany relationship has correct number of records'); - const dog1 = dogs.at(0); - const dogPerson1 = await dog1.pal; - assert.strictEqual( - dogPerson1.id, - '1', - 'dog.person inverse relationship is set up correctly when adapter does not include parent relationships in data.relationships' - ); + await assert.expectAssertion(async () => { + await person.dog; + }, /The record loaded at/); + } + ); + + deprecatedTest( + 'one-to-one - findBelongsTo/implicit inverse - fixes null relationship information from the payload and deprecates', + { id: 'ember-data:deprecate-non-strict-relationships', debugOnly: true, until: '5.0', count: 2 }, + async function (assert) { + const { owner } = this; + + owner.register( + 'adapter:application', + JSONAPIAdapter.extend({ + deleteRecord: () => Promise.resolve({ data: null }), + findBelongsTo: () => { + return Promise.resolve({ + data: { + id: '1', + type: 'dog', + attributes: { + name: 'Scooby', + }, + relationships: { + person: { + data: null, + }, + }, + }, + }); + }, + }) + ); + + class Person extends Model { + @belongsTo('dog', { + async: true, + }) + dog; + } + owner.register('model:person', Person); + + class Dog extends Model { + @belongsTo('person', { + async: true, + }) + person; + } + owner.register('model:dog', Dog); + + const person = store.push({ + data: { + type: 'person', + id: '1', + attributes: { + name: 'John Churchill', + }, + relationships: { + dog: { + links: { + related: 'http://example.com/person/1/dog', + }, + }, + }, + }, + }); + + await assert.expectAssertion(async () => { + await person.dog; + }, /The record loaded at/); + } + ); + + deprecatedTest( + 'one-to-one (left hand async, right hand sync) - findBelongsTo/implicit inverse - fixes null relationship information from the payload and deprecates', + { id: 'ember-data:deprecate-non-strict-relationships', debugOnly: true, until: '5.0', count: 2 }, + async function (assert) { + const { owner } = this; + + owner.register( + 'adapter:application', + JSONAPIAdapter.extend({ + deleteRecord: () => Promise.resolve({ data: null }), + findBelongsTo: () => { + return Promise.resolve({ + data: { + id: '1', + type: 'dog', + attributes: { + name: 'Scooby', + }, + relationships: { + person: { + data: null, + }, + }, + }, + }); + }, + }) + ); + + class Person extends Model { + @belongsTo('dog', { + async: true, + }) + dog; + } + owner.register('model:person', Person); + + class Dog extends Model { + @belongsTo('person', { + async: false, + }) + person; + } + owner.register('model:dog', Dog); + + const person = store.push({ + data: { + type: 'person', + id: '1', + attributes: { + name: 'John Churchill', + }, + relationships: { + dog: { + links: { + related: 'http://example.com/person/1/dog', + }, + }, + }, + }, + }); + + await assert.expectAssertion(async () => { + await person.dog; + }, /The record loaded at/); + } + ); + + deprecatedTest( + 'many-to-one - findBelongsTo/implicitInverse - fixes mismatched parent relationship information from the payload and deprecates', + { id: 'ember-data:deprecate-non-strict-relationships', debugOnly: true, until: '5.0', count: 2 }, + async function (assert) { + const { owner } = this; + + owner.register( + 'adapter:application', + JSONAPIAdapter.extend({ + deleteRecord: () => Promise.resolve({ data: null }), + findBelongsTo: () => { + return Promise.resolve({ + data: { + type: 'person', + id: '1', + attributes: { + name: 'John Churchill', + }, + relationships: { + dogs: { + data: [ + { + id: '2', + type: 'dog', + }, + ], + }, + }, + }, + }); + }, + }) + ); + + class Person extends Model { + @hasMany('dog', { + async: true, + }) + dogs; + } + owner.register('model:person', Person); + + class Dog extends Model { + @belongsTo('person', { + async: true, + }) + person; + } + owner.register('model:dog', Dog); + + const dog = store.push({ + data: { + type: 'dog', + id: '1', + attributes: { + name: 'A Really Good Dog', + }, + relationships: { + person: { + links: { + related: 'http://example.com/person/1', + }, + }, + }, + }, + }); + + await assert.expectAssertion(async () => { + await dog.person; + }, /The record loaded at/); + } + ); + + deprecatedTest( + 'many-to-one (left hand async, right hand sync) - findBelongsTo/implicitInverse - fixes mismatched parent relationship information from the payload and deprecates', + { id: 'ember-data:deprecate-non-strict-relationships', debugOnly: true, until: '5.0', count: 2 }, + async function (assert) { + const { owner } = this; + + owner.register( + 'adapter:application', + JSONAPIAdapter.extend({ + deleteRecord: () => Promise.resolve({ data: null }), + findBelongsTo: () => { + return Promise.resolve({ + data: { + type: 'person', + id: '1', + attributes: { + name: 'John Churchill', + }, + relationships: { + dogs: { + data: [ + { + id: '2', + type: 'dog', + }, + ], + }, + }, + }, + }); + }, + }) + ); + + class Person extends Model { + @hasMany('dog', { + async: false, + }) + dogs; + } + owner.register('model:person', Person); + + class Dog extends Model { + @belongsTo('person', { + async: true, + }) + person; + } + owner.register('model:dog', Dog); + + const dog = store.push({ + data: { + type: 'dog', + id: '1', + attributes: { + name: 'A Really Good Dog', + }, + relationships: { + person: { + links: { + related: 'http://example.com/person/1', + }, + }, + }, + }, + }); + + await assert.expectAssertion(async () => { + await dog.person; + }, /The record loaded at/); + } + ); + + deprecatedTest( + 'many-to-one - findBelongsTo/implicitInverse - fixes null relationship information from the payload and deprecates', + { id: 'ember-data:deprecate-non-strict-relationships', debugOnly: true, until: '5.0', count: 2 }, + async function (assert) { + const { owner } = this; + + owner.register( + 'adapter:application', + JSONAPIAdapter.extend({ + deleteRecord: () => Promise.resolve({ data: null }), + findBelongsTo: () => { + return Promise.resolve({ + data: { + type: 'person', + id: '1', + attributes: { + name: 'John Churchill', + }, + relationships: { + dogs: { + data: [], + }, + }, + }, + }); + }, + }) + ); + + class Person extends Model { + @hasMany('dog', { + async: true, + }) + dogs; + } + owner.register('model:person', Person); + + class Dog extends Model { + @belongsTo('person', { + async: true, + }) + person; + } + owner.register('model:dog', Dog); + + const dog = store.push({ + data: { + type: 'dog', + id: '1', + attributes: { + name: 'A Really Good Dog', + }, + relationships: { + person: { + links: { + related: 'http://example.com/person/1', + }, + }, + }, + }, + }); + + await assert.expectAssertion(async () => { + await dog.person; + }, /The record loaded at/); + } + ); + + deprecatedTest( + 'many-to-one (left hand async, right hand sync) - findBelongsTo/implicitInverse - fixes null relationship information from the payload and deprecates', + { id: 'ember-data:deprecate-non-strict-relationships', debugOnly: true, until: '5.0', count: 2 }, + async function (assert) { + const { owner } = this; + + owner.register( + 'adapter:application', + JSONAPIAdapter.extend({ + deleteRecord: () => Promise.resolve({ data: null }), + findBelongsTo: () => { + return Promise.resolve({ + data: { + type: 'person', + id: '1', + attributes: { + name: 'John Churchill', + }, + relationships: { + dogs: { + data: [], + }, + }, + }, + }); + }, + }) + ); + + class Person extends Model { + @hasMany('dog', { + async: false, + }) + dogs; + } + owner.register('model:person', Person); + + class Dog extends Model { + @belongsTo('person', { + async: true, + }) + person; + } + owner.register('model:dog', Dog); + + const dog = store.push({ + data: { + type: 'dog', + id: '1', + attributes: { + name: 'A Really Good Dog', + }, + relationships: { + person: { + links: { + related: 'http://example.com/person/1', + }, + }, + }, + }, + }); + + await assert.expectAssertion(async () => { + await dog.person; + }, /The record loaded at/); + } + ); + + deprecatedTest( + 'many-to-many - findHasMany/implicitInverse - fixes mismatched parent relationship information from the payload and deprecates', + { id: 'ember-data:deprecate-non-strict-relationships', debugOnly: true, until: '5.0', count: 2 }, + async function (assert) { + const { owner } = this; + + owner.register( + 'adapter:application', + JSONAPIAdapter.extend({ + deleteRecord: () => Promise.resolve({ data: null }), + findHasMany: () => { + return Promise.resolve({ + data: [ + { + id: '1', + type: 'dog', + attributes: { + name: 'Scooby', + }, + relationships: { + walkers: { + data: [ + { + id: '2', + type: 'person', + }, + ], + }, + }, + }, + { + id: '2', + type: 'dog', + attributes: { + name: 'Scrappy', + }, + relationships: { + walkers: { + data: [ + { + id: '2', + type: 'person', + }, + ], + }, + }, + }, + ], + }); + }, + }) + ); + + class Person extends Model { + @hasMany('dog', { + async: true, + }) + dogs; + } + owner.register('model:person', Person); + + class Dog extends Model { + @hasMany('person', { + async: true, + }) + walkers; + } + owner.register('model:dog', Dog); + + const person1 = store.push({ + data: { + type: 'person', + id: '1', + attributes: { + name: 'John Churchill', + }, + relationships: { + dogs: { + links: { + related: 'http://example.com/person/1/dogs', + }, + }, + }, + }, + }); + + await assert.expectAssertion(async () => { + await person1.dogs; + }, /The record loaded at/); + } + ); + + deprecatedTest( + 'many-to-many (left hand async, right hand sync) - findHasMany/implicitInverse - fixes mismatched parent relationship information from the payload and deprecates', + { id: 'ember-data:deprecate-non-strict-relationships', debugOnly: true, until: '5.0', count: 2 }, + async function (assert) { + const { owner } = this; + + owner.register( + 'adapter:application', + JSONAPIAdapter.extend({ + deleteRecord: () => Promise.resolve({ data: null }), + findHasMany: () => { + return Promise.resolve({ + data: [ + { + id: '1', + type: 'dog', + attributes: { + name: 'Scooby', + }, + relationships: { + walkers: { + data: [ + { + id: '2', + type: 'person', + }, + ], + }, + }, + }, + { + id: '2', + type: 'dog', + attributes: { + name: 'Scrappy', + }, + relationships: { + walkers: { + data: [ + { + id: '2', + type: 'person', + }, + ], + }, + }, + }, + ], + }); + }, + }) + ); + + class Person extends Model { + @hasMany('dog', { + async: true, + }) + dogs; + } + owner.register('model:person', Person); + + class Dog extends Model { + @hasMany('person', { + async: false, + }) + walkers; + } + owner.register('model:dog', Dog); + + const person1 = store.push({ + data: { + type: 'person', + id: '1', + attributes: { + name: 'John Churchill', + }, + relationships: { + dogs: { + links: { + related: 'http://example.com/person/1/dogs', + }, + }, + }, + }, + }); + + await assert.expectAssertion(async () => { + await person1.dogs; + }, /The record loaded at/); + } + ); + + deprecatedTest( + 'many-to-many - findHasMany/implicitInverse - fixes empty relationship information from the payload and deprecates', + { id: 'ember-data:deprecate-non-strict-relationships', debugOnly: true, until: '5.0', count: 2 }, + async function (assert) { + const { owner } = this; + + owner.register( + 'adapter:application', + JSONAPIAdapter.extend({ + deleteRecord: () => Promise.resolve({ data: null }), + findHasMany: () => { + return Promise.resolve({ + data: [ + { + id: '1', + type: 'dog', + attributes: { + name: 'Scooby', + }, + relationships: { + walkers: { + data: [], + }, + }, + }, + { + id: '2', + type: 'dog', + attributes: { + name: 'Scrappy', + }, + relationships: { + walkers: { + data: [], + }, + }, + }, + ], + }); + }, + }) + ); + + class Person extends Model { + @hasMany('dog', { + async: true, + }) + dogs; + } + owner.register('model:person', Person); + + class Dog extends Model { + @hasMany('person', { + async: true, + }) + walkers; + } + owner.register('model:dog', Dog); + + const person = store.push({ + data: { + type: 'person', + id: '1', + attributes: { + name: 'John Churchill', + }, + relationships: { + dogs: { + links: { + related: 'http://example.com/person/1/dogs', + }, + }, + }, + }, + }); + + await assert.expectAssertion(async () => { + await person.dogs; + }, /The record loaded at/); + } + ); + + deprecatedTest( + 'many-to-many (left hand async, right hand sync) - findHasMany/implicitInverse - fixes empty relationship information from the payload and deprecates', + { id: 'ember-data:deprecate-non-strict-relationships', debugOnly: true, until: '5.0', count: 2 }, + async function (assert) { + const { owner } = this; + + owner.register( + 'adapter:application', + JSONAPIAdapter.extend({ + deleteRecord: () => Promise.resolve({ data: null }), + findHasMany: () => { + return Promise.resolve({ + data: [ + { + id: '1', + type: 'dog', + attributes: { + name: 'Scooby', + }, + relationships: { + walkers: { + data: [], + }, + }, + }, + { + id: '2', + type: 'dog', + attributes: { + name: 'Scrappy', + }, + relationships: { + walkers: { + data: [], + }, + }, + }, + ], + }); + }, + }) + ); + + class Person extends Model { + @hasMany('dog', { + async: true, + }) + dogs; + } + owner.register('model:person', Person); + + class Dog extends Model { + @hasMany('person', { + async: false, + }) + walkers; + } + owner.register('model:dog', Dog); + + const person = store.push({ + data: { + type: 'person', + id: '1', + attributes: { + name: 'John Churchill', + }, + relationships: { + dogs: { + links: { + related: 'http://example.com/person/1/dogs', + }, + }, + }, + }, + }); + + await assert.expectAssertion(async () => { + await person.dogs; + }, /The record loaded at/); + } + ); + + deprecatedTest( + 'many-to-many - findHasMany/implicitInverse - fixes null relationship information from the payload and deprecates', + { id: 'ember-data:deprecate-non-strict-relationships', debugOnly: true, until: '5.0', count: 2 }, + async function (assert) { + const { owner } = this; + + owner.register( + 'adapter:application', + JSONAPIAdapter.extend({ + deleteRecord: () => Promise.resolve({ data: null }), + findHasMany: () => { + return Promise.resolve({ + data: [ + { + id: '1', + type: 'dog', + attributes: { + name: 'Scooby', + }, + relationships: { + walkers: { + data: null, + }, + }, + }, + { + id: '2', + type: 'dog', + attributes: { + name: 'Scrappy', + }, + relationships: { + walkers: { + data: null, + }, + }, + }, + ], + }); + }, + }) + ); + + class Person extends Model { + @hasMany('dog', { + async: true, + }) + dogs; + } + owner.register('model:person', Person); + + class Dog extends Model { + @hasMany('person', { + async: true, + }) + walkers; + } + owner.register('model:dog', Dog); + + const person = store.push({ + data: { + type: 'person', + id: '1', + attributes: { + name: 'John Churchill', + }, + relationships: { + dogs: { + links: { + related: 'http://example.com/person/1/dogs', + }, + }, + }, + }, + }); + + await assert.expectAssertion(async () => { + await person.dogs; + }, /The record loaded at/); + } + ); + + deprecatedTest( + 'many-to-many (left hand async, right hand sync) - findHasMany/implicitInverse - asserts incorrect null relationship information from the payload', + { id: 'ember-data:deprecate-non-strict-relationships', debugOnly: true, until: '5.0', count: 2 }, + async function (assert) { + const { owner } = this; + + owner.register( + 'adapter:application', + JSONAPIAdapter.extend({ + deleteRecord: () => Promise.resolve({ data: null }), + findHasMany: () => { + return Promise.resolve({ + data: [ + { + id: '1', + type: 'dog', + attributes: { + name: 'Scooby', + }, + relationships: { + walkers: { + data: null, + }, + }, + }, + { + id: '2', + type: 'dog', + attributes: { + name: 'Scrappy', + }, + relationships: { + walkers: { + data: null, + }, + }, + }, + ], + }); + }, + }) + ); + + class Person extends Model { + @hasMany('dog', { + async: true, + }) + dogs; + } + owner.register('model:person', Person); + + class Dog extends Model { + @hasMany('person', { + async: false, + }) + walkers; + } + owner.register('model:dog', Dog); + + const person = store.push({ + data: { + type: 'person', + id: '1', + attributes: { + name: 'John Churchill', + }, + relationships: { + dogs: { + links: { + related: 'http://example.com/person/1/dogs', + }, + }, + }, + }, + }); + + await assert.expectAssertion(async () => { + await person.dogs; + }, /The record loaded at data\[0\] in the payload specified null as its/); + } + ); + + deprecatedTest( + 'one-to-many - ids/non-link/implicit inverse - ids - records loaded through ids/findRecord are linked to the parent if the response from the server does not include relationship information', + { id: 'ember-data:deprecate-non-strict-relationships', debugOnly: true, until: '5.0', count: 2 }, + async function (assert) { + const { owner } = this; + + const scooby = { + id: '1', + type: 'dog', + attributes: { + name: 'Scooby', + }, + }; + + const scrappy = { + id: '2', + type: 'dog', + attributes: { + name: 'Scrappy', + }, + }; + + owner.register( + 'adapter:application', + JSONAPIAdapter.extend({ + deleteRecord: () => Promise.resolve({ data: null }), + findRecord: (_store, _type, id) => { + const dog = id === '1' ? scooby : scrappy; + return Promise.resolve({ + data: dog, + }); + }, + }) + ); + + class Person extends Model { + @hasMany('dog', { + async: true, + }) + dogs; + } + owner.register('model:person', Person); + + class Dog extends Model { + @belongsTo('person', { + async: true, + }) + person; + } + owner.register('model:dog', Dog); + + const person = store.push({ + data: { + type: 'person', + id: '1', + attributes: { + name: 'John Churchill', + }, + relationships: { + dogs: { + data: [ + { + id: '1', + type: 'dog', + }, + { + id: '2', + type: 'dog', + }, + ], + }, + }, + }, + }); + + const dogs = await person.dogs; + assert.false(person.hasMany('dogs').hasManyRelationship.state.isEmpty, 'relationship state was set up correctly'); + + assert.strictEqual(dogs.length, 2, 'hasMany relationship has correct number of records'); + const dog1 = dogs.at(0); + const dogPerson1 = await dog1.person; + assert.strictEqual( + dogPerson1.id, + '1', + 'dog.person inverse relationship is set up correctly when adapter does not include parent relationships in data.relationships' + ); + const dogPerson2 = await dogs.at(1).person; + assert.strictEqual( + dogPerson2.id, + '1', + 'dog.person inverse relationship is set up correctly when adapter does not include parent relationships in data.relationships' + ); + + await dog1.destroyRecord(); + assert.strictEqual(dogs.length, 1, 'record removed from hasMany relationship after deletion'); + assert.strictEqual(dogs.at(0).id, '2', 'hasMany relationship has correct records'); + } + ); + + deprecatedTest( + 'one-to-many (left hand async, right hand sync) - ids/non-link/implicit inverse - ids - records loaded through ids/findRecord are linked to the parent if the response from the server does not include relationship information', + { id: 'ember-data:deprecate-non-strict-relationships', debugOnly: true, until: '5.0', count: 2 }, + async function (assert) { + const { owner } = this; + + const scooby = { + id: '1', + type: 'dog', + attributes: { + name: 'Scooby', + }, + }; + + const scrappy = { + id: '2', + type: 'dog', + attributes: { + name: 'Scrappy', + }, + }; + + owner.register( + 'adapter:application', + JSONAPIAdapter.extend({ + deleteRecord: () => Promise.resolve({ data: null }), + findRecord: (_store, _type, id) => { + const dog = id === '1' ? scooby : scrappy; + return Promise.resolve({ + data: dog, + }); + }, + }) + ); + + class Person extends Model { + @hasMany('dog', { + async: true, + }) + dogs; + } + owner.register('model:person', Person); + + class Dog extends Model { + @belongsTo('person', { + async: false, + }) + person; + } + owner.register('model:dog', Dog); + + const person = store.push({ + data: { + type: 'person', + id: '1', + attributes: { + name: 'John Churchill', + }, + relationships: { + dogs: { + data: [ + { + id: '1', + type: 'dog', + }, + { + id: '2', + type: 'dog', + }, + ], + }, + }, + }, + }); + + const dogs = await person.dogs; + assert.false(person.hasMany('dogs').hasManyRelationship.state.isEmpty, 'relationship state was set up correctly'); + + assert.strictEqual(dogs.length, 2, 'hasMany relationship has correct number of records'); + const dog1 = dogs.at(0); + const dogPerson1 = await dog1.person; + assert.strictEqual( + dogPerson1.id, + '1', + 'dog.person inverse relationship is set up correctly when adapter does not include parent relationships in data.relationships' + ); + const dogPerson2 = await dogs.at(1).person; + assert.strictEqual( + dogPerson2.id, + '1', + 'dog.person inverse relationship is set up correctly when adapter does not include parent relationships in data.relationships' + ); + + await dog1.destroyRecord(); + assert.strictEqual(dogs.length, 1, 'record removed from hasMany relationship after deletion'); + assert.strictEqual(dogs.at(0).id, '2', 'hasMany relationship has correct records'); + } + ); + + test('one-to-many - ids/non-link/explicit inverse - ids - records loaded through ids/findRecord are linked to the parent if the response from the server does not include relationship information', async function (assert) { + const { owner } = this; + + const scooby = { + id: '1', + type: 'dog', + attributes: { + name: 'Scooby', + }, + }; + + const scrappy = { + id: '2', + type: 'dog', + attributes: { + name: 'Scrappy', + }, + }; + + owner.register( + 'adapter:application', + JSONAPIAdapter.extend({ + deleteRecord: () => Promise.resolve({ data: null }), + findRecord: (_store, _type, id) => { + const dog = id === '1' ? scooby : scrappy; + return Promise.resolve({ + data: dog, + }); + }, + }) + ); + + class Person extends Model { + @hasMany('dog', { + async: true, + inverse: 'pal', + }) + dogs; + } + owner.register('model:person', Person); + + class Dog extends Model { + @belongsTo('person', { + async: true, + inverse: 'dogs', + }) + pal; + } + owner.register('model:dog', Dog); + + const person = store.push({ + data: { + type: 'person', + id: '1', + attributes: { + name: 'John Churchill', + }, + relationships: { + dogs: { + data: [ + { + id: '1', + type: 'dog', + }, + { + id: '2', + type: 'dog', + }, + ], + }, + }, + }, + }); + + const dogs = await person.dogs; + assert.false(person.hasMany('dogs').hasManyRelationship.state.isEmpty, 'relationship state was set up correctly'); + + assert.strictEqual(dogs.length, 2, 'hasMany relationship has correct number of records'); + const dog1 = dogs.at(0); + const dogPerson1 = await dog1.pal; + assert.strictEqual( + dogPerson1.id, + '1', + 'dog.person inverse relationship is set up correctly when adapter does not include parent relationships in data.relationships' + ); const dogPerson2 = await dogs.at(1).pal; assert.strictEqual( dogPerson2.id, @@ -1077,6 +3342,468 @@ module('inverse relationship load test', function (hooks) { assert.strictEqual(dogs.at(0).id, '2', 'hasMany relationship has correct records'); }); + deprecatedTest( + 'one-to-many - ids/non-link/implicit inverse - records loaded through ids/findRecord do not get associated with the parent if the server specifies another resource as the relationship value in the response', + { id: 'ember-data:deprecate-non-strict-relationships', until: '5.0', count: 2 }, + async function (assert) { + const { owner } = this; + + const scooby = { + id: '1', + type: 'dog', + attributes: { + name: 'Scooby', + }, + relationships: { + person: { + data: { + id: '2', + type: 'person', + }, + }, + }, + }; + + const scrappy = { + id: '2', + type: 'dog', + attributes: { + name: 'Scrappy', + }, + relationships: { + person: { + data: { + id: '2', + type: 'person', + }, + }, + }, + }; + + owner.register( + 'adapter:application', + JSONAPIAdapter.extend({ + deleteRecord: () => Promise.resolve({ data: null }), + findRecord: (_store, _type, id) => { + const dog = id === '1' ? scooby : scrappy; + return Promise.resolve({ + data: dog, + }); + }, + }) + ); + + class Person extends Model { + @hasMany('dog', { + async: true, + }) + dogs; + } + owner.register('model:person', Person); + + class Dog extends Model { + @belongsTo('person', { + async: true, + }) + person; + } + owner.register('model:dog', Dog); + + const person = store.push({ + data: { + type: 'person', + id: '1', + attributes: { + name: 'John Churchill', + }, + relationships: { + dogs: { + data: [ + { + id: '1', + type: 'dog', + }, + { + id: '2', + type: 'dog', + }, + ], + }, + }, + }, + }); + + const person2 = store.push({ + data: { + type: 'person', + id: '2', + attributes: { + name: 'ok', + }, + }, + }); + + const dogs = await person.dogs; + assert.false(person.hasMany('dogs').hasManyRelationship.state.isEmpty, 'relationship state was set up correctly'); + + assert.strictEqual(dogs.length, 0, 'hasMany relationship for parent is empty'); + + const person2Dogs = await person2.dogs; + assert.strictEqual( + person2Dogs.length, + 2, + 'hasMany relationship on specified record has correct number of associated records' + ); + + const allDogs = store.peekAll('dogs').slice(); + for (let i = 0; i < allDogs.length; i++) { + const dog = allDogs[i]; + const dogPerson = await dog.person; + assert.strictEqual(dogPerson.id, person2.id, 'right hand side has correct belongsTo value'); + } + + const dog1 = store.peekRecord('dog', '1'); + await dog1.destroyRecord(); + assert.strictEqual(person2Dogs.length, 1, 'record removed from hasMany relationship after deletion'); + assert.strictEqual(person2Dogs.at(0).id, '2', 'hasMany relationship has correct records'); + } + ); + + deprecatedTest( + 'one-to-many (left hand async, right hand sync) - ids/non-link/implicit inverse - records loaded through ids/findRecord do not get associated with the parent if the server specifies another resource as the relationship value in the response', + { id: 'ember-data:deprecate-non-strict-relationships', until: '5.0', count: 2 }, + async function (assert) { + const { owner } = this; + + const scooby = { + id: '1', + type: 'dog', + attributes: { + name: 'Scooby', + }, + relationships: { + person: { + data: { + id: '2', + type: 'person', + }, + }, + }, + }; + + const scrappy = { + id: '2', + type: 'dog', + attributes: { + name: 'Scrappy', + }, + relationships: { + person: { + data: { + id: '2', + type: 'person', + }, + }, + }, + }; + + owner.register( + 'adapter:application', + JSONAPIAdapter.extend({ + deleteRecord: () => Promise.resolve({ data: null }), + findRecord: (_store, _type, id) => { + const dog = id === '1' ? scooby : scrappy; + return Promise.resolve({ + data: dog, + }); + }, + }) + ); + + class Person extends Model { + @hasMany('dog', { + async: true, + }) + dogs; + } + owner.register('model:person', Person); + + class Dog extends Model { + @belongsTo('person', { + async: false, + }) + person; + } + owner.register('model:dog', Dog); + + const person = store.push({ + data: { + type: 'person', + id: '1', + attributes: { + name: 'John Churchill', + }, + relationships: { + dogs: { + data: [ + { + id: '1', + type: 'dog', + }, + { + id: '2', + type: 'dog', + }, + ], + }, + }, + }, + }); + + const person2 = store.push({ + data: { + type: 'person', + id: '2', + attributes: { + name: 'ok', + }, + }, + }); + + const dogs = await person.dogs; + assert.false(person.hasMany('dogs').hasManyRelationship.state.isEmpty, 'relationship state was set up correctly'); + + assert.strictEqual(dogs.length, 0, 'hasMany relationship for parent is empty'); + + const person2Dogs = await person2.dogs; + assert.strictEqual( + person2Dogs.length, + 2, + 'hasMany relationship on specified record has correct number of associated records' + ); + + const allDogs = store.peekAll('dogs').slice(); + for (let i = 0; i < allDogs.length; i++) { + const dog = allDogs[i]; + const dogPerson = await dog.person; + assert.strictEqual(dogPerson.id, person2.id, 'right hand side has correct belongsTo value'); + } + + const dog1 = store.peekRecord('dog', '1'); + await dog1.destroyRecord(); + assert.strictEqual(person2Dogs.length, 1, 'record removed from hasMany relationship after deletion'); + assert.strictEqual(person2Dogs.at(0).id, '2', 'hasMany relationship has correct records'); + } + ); + + deprecatedTest( + 'one-to-many - ids/non-link/implicit inverse - records loaded through ids/findRecord do not get associated with the parent if the server specifies null as the relationship value in the response', + { id: 'ember-data:deprecate-non-strict-relationships', until: '5.0', count: 2 }, + async function (assert) { + const { owner } = this; + + const scooby = { + id: '1', + type: 'dog', + attributes: { + name: 'Scooby', + }, + relationships: { + person: { + data: null, + }, + }, + }; + + const scrappy = { + id: '2', + type: 'dog', + attributes: { + name: 'Scrappy', + }, + relationships: { + person: { + data: null, + }, + }, + }; + + owner.register( + 'adapter:application', + JSONAPIAdapter.extend({ + deleteRecord: () => Promise.resolve({ data: null }), + findRecord: (_store, _type, id) => { + const dog = id === '1' ? scooby : scrappy; + return Promise.resolve({ + data: dog, + }); + }, + }) + ); + + class Person extends Model { + @hasMany('dog', { + async: true, + }) + dogs; + } + owner.register('model:person', Person); + + class Dog extends Model { + @belongsTo('person', { + async: true, + }) + person; + } + owner.register('model:dog', Dog); + + const person = store.push({ + data: { + type: 'person', + id: '1', + attributes: { + name: 'John Churchill', + }, + relationships: { + dogs: { + data: [ + { + id: '1', + type: 'dog', + }, + { + id: '2', + type: 'dog', + }, + ], + }, + }, + }, + }); + + const personDogs = await person.dogs; + assert.false(person.hasMany('dogs').hasManyRelationship.state.isEmpty, 'relationship state was set up correctly'); + + assert.strictEqual(personDogs.length, 0, 'hasMany relationship for parent is empty'); + + const allDogs = store.peekAll('dogs').slice(); + for (let i = 0; i < allDogs.length; i++) { + const dog = allDogs[i]; + const dogPerson = await dog.person; + assert.strictEqual(dogPerson, null, 'right hand side has correct belongsTo value'); + } + + const dog1 = store.peekRecord('dog', '1'); + await dog1.destroyRecord(); + + assert.strictEqual(personDogs.length, 0); + } + ); + + deprecatedTest( + 'one-to-many (left hand async, right hand sync) - ids/non-link/implicit inverse - records loaded through ids/findRecord do not get associated with the parent if the server specifies null as the relationship value in the response', + { id: 'ember-data:deprecate-non-strict-relationships', until: '5.0', count: 2 }, + async function (assert) { + const { owner } = this; + + const scooby = { + id: '1', + type: 'dog', + attributes: { + name: 'Scooby', + }, + relationships: { + person: { + data: null, + }, + }, + }; + + const scrappy = { + id: '2', + type: 'dog', + attributes: { + name: 'Scrappy', + }, + relationships: { + person: { + data: null, + }, + }, + }; + + owner.register( + 'adapter:application', + JSONAPIAdapter.extend({ + deleteRecord: () => Promise.resolve({ data: null }), + findRecord: (_store, _type, id) => { + const dog = id === '1' ? scooby : scrappy; + return Promise.resolve({ + data: dog, + }); + }, + }) + ); + + class Person extends Model { + @hasMany('dog', { + async: true, + }) + dogs; + } + owner.register('model:person', Person); + + class Dog extends Model { + @belongsTo('person', { + async: false, + }) + person; + } + owner.register('model:dog', Dog); + + const person = store.push({ + data: { + type: 'person', + id: '1', + attributes: { + name: 'John Churchill', + }, + relationships: { + dogs: { + data: [ + { + id: '1', + type: 'dog', + }, + { + id: '2', + type: 'dog', + }, + ], + }, + }, + }, + }); + + const personDogs = await person.dogs; + assert.false(person.hasMany('dogs').hasManyRelationship.state.isEmpty, 'relationship state was set up correctly'); + + assert.strictEqual(personDogs.length, 0, 'hasMany relationship for parent is empty'); + + const allDogs = store.peekAll('dogs').slice(); + for (let i = 0; i < allDogs.length; i++) { + const dog = allDogs[i]; + const dogPerson = await dog.person; + assert.strictEqual(dogPerson, null, 'right hand side has correct belongsTo value'); + } + + const dog1 = store.peekRecord('dog', '1'); + await dog1.destroyRecord(); + + assert.strictEqual(personDogs.length, 0); + } + ); + test('one-to-many - ids/non-link/explicit inverse - records loaded through ids/findRecord do not get associated with the parent if the server specifies another resource as the relationship value in the response', async function (assert) { const { owner } = this; diff --git a/tests/main/tests/integration/relationships/inverse-relationships-test.js b/tests/main/tests/integration/relationships/inverse-relationships-test.js index 6ad758ca038..b2e143bc78d 100644 --- a/tests/main/tests/integration/relationships/inverse-relationships-test.js +++ b/tests/main/tests/integration/relationships/inverse-relationships-test.js @@ -2,8 +2,16 @@ import { module } from 'qunit'; import { setupTest } from 'ember-qunit'; -import Model, { belongsTo, hasMany } from '@ember-data/model'; +import { graphFor } from '@ember-data/graph/-private'; +import Model, { attr, belongsTo, hasMany } from '@ember-data/model'; +import { recordIdentifierFor } from '@ember-data/store'; +import { deprecatedTest } from '@ember-data/unpublished-test-infra/test-support/deprecated-test'; import testInDebug from '@ember-data/unpublished-test-infra/test-support/test-in-debug'; +import { DEPRECATE_RELATIONSHIPS_WITHOUT_INVERSE } from '@warp-drive/build-config/deprecations'; + +function test(label, callback) { + deprecatedTest(label, { id: 'ember-data:deprecate-non-strict-relationships', until: '5.0', count: 'ALL' }, callback); +} module('integration/relationships/inverse_relationships - Inverse Relationships', function (hooks) { setupTest(hooks); @@ -18,6 +26,415 @@ module('integration/relationships/inverse_relationships - Inverse Relationships' register = owner.register.bind(owner); }); + test('When a record is added to a has-many relationship, the inverse belongsTo is determined automatically', async function (assert) { + class Post extends Model { + @hasMany('comment', { async: false }) + comments; + } + + class Comment extends Model { + @belongsTo('post', { async: false }) + post; + } + + register('model:Post', Post); + register('model:Comment', Comment); + + const comment = store.createRecord('comment'); + const post = store.createRecord('post'); + + assert.strictEqual(comment.post, null, 'no post has been set on the comment'); + + post.comments.push(comment); + assert.strictEqual(comment.post, post, 'post was set on the comment'); + }); + + test('Inverse relationships can be explicitly nullable', function (assert) { + class User extends Model { + @hasMany('post', { inverse: 'participants', async: false }) + posts; + } + + class Post extends Model { + @belongsTo('user', { inverse: null, async: false }) + lastParticipant; + + @hasMany('user', { inverse: 'posts', async: false }) + participants; + } + + register('model:User', User); + register('model:Post', Post); + + const user = store.createRecord('user'); + const post = store.createRecord('post'); + + assert.strictEqual(user.inverseFor('posts').name, 'participants', 'User.posts inverse is Post.participants'); + assert.strictEqual(post.inverseFor('lastParticipant'), null, 'Post.lastParticipant has no inverse'); + assert.strictEqual(post.inverseFor('participants').name, 'posts', 'Post.participants inverse is User.posts'); + }); + + test('Null inverses are excluded from potential relationship resolutions', function (assert) { + class User extends Model { + @hasMany('post', { async: false }) + posts; + } + + class Post extends Model { + @belongsTo('user', { inverse: null, async: false }) + lastParticipant; + + @hasMany('user', { async: false }) + participants; + } + + register('model:User', User); + register('model:Post', Post); + + const user = store.createRecord('user'); + const post = store.createRecord('post'); + + assert.strictEqual(user.inverseFor('posts').name, 'participants', 'User.posts inverse is Post.participants'); + assert.strictEqual(post.inverseFor('lastParticipant'), null, 'Post.lastParticipant has no inverse'); + assert.strictEqual(post.inverseFor('participants').name, 'posts', 'Post.participants inverse is User.posts'); + }); + + test('When a record is added to a has-many relationship, the inverse belongsTo can be set explicitly', async function (assert) { + class Post extends Model { + @hasMany('comment', { inverse: 'redPost', async: false }) + comments; + } + + class Comment extends Model { + @belongsTo('post', { async: false }) + onePost; + + @belongsTo('post', { async: false }) + twoPost; + + @belongsTo('post', { async: false }) + redPost; + + @belongsTo('post', { async: false }) + bluePost; + } + + register('model:Post', Post); + register('model:Comment', Comment); + + const comment = store.createRecord('comment'); + const post = store.createRecord('post'); + + assert.strictEqual(comment.onePost, null, 'onePost has not been set on the comment'); + assert.strictEqual(comment.twoPost, null, 'twoPost has not been set on the comment'); + assert.strictEqual(comment.redPost, null, 'redPost has not been set on the comment'); + assert.strictEqual(comment.bluePost, null, 'bluePost has not been set on the comment'); + + post.comments.push(comment); + + assert.strictEqual(comment.onePost, null, 'onePost has not been set on the comment'); + assert.strictEqual(comment.twoPost, null, 'twoPost has not been set on the comment'); + assert.strictEqual(comment.redPost, post, 'redPost has been set on the comment'); + assert.strictEqual(comment.bluePost, null, 'bluePost has not been set on the comment'); + }); + + test("When a record's belongsTo relationship is set, it can specify the inverse hasMany to which the new child should be added", async function (assert) { + class Post extends Model { + @hasMany('comment', { async: false }) + meComments; + + @hasMany('comment', { async: false }) + youComments; + + @hasMany('comment', { async: false }) + everyoneWeKnowComments; + } + + class Comment extends Model { + @belongsTo('post', { inverse: 'youComments', async: false }) + post; + } + + register('model:Post', Post); + register('model:Comment', Comment); + + const comment = store.createRecord('comment'); + const post = store.createRecord('post'); + + assert.strictEqual(post.meComments.length, 0, 'meComments has no posts'); + assert.strictEqual(post.youComments.length, 0, 'youComments has no posts'); + assert.strictEqual(post.everyoneWeKnowComments.length, 0, 'everyoneWeKnowComments has no posts'); + + comment.set('post', post); + + assert.strictEqual(comment.post, post, 'The post that was set can be retrieved'); + + assert.strictEqual(post.meComments.length, 0, 'meComments has no posts'); + assert.strictEqual(post.youComments.length, 1, 'youComments had the post added'); + assert.strictEqual(post.everyoneWeKnowComments.length, 0, 'everyoneWeKnowComments has no posts'); + }); + + test('When setting a belongsTo, the OneToOne invariant is respected even when other records have been previously used', async function (assert) { + class Post extends Model { + @belongsTo('comment', { async: false }) + bestComment; + } + + class Comment extends Model { + @belongsTo('post', { async: false }) + post; + } + + register('model:Post', Post); + register('model:Comment', Comment); + + const comment = store.createRecord('comment'); + const post = store.createRecord('post'); + const post2 = store.createRecord('post'); + + comment.set('post', post); + post2.set('bestComment', null); + + assert.strictEqual(comment.post, post); + assert.strictEqual(post.bestComment, comment); + assert.strictEqual(post2.bestComment, null); + + comment.set('post', post2); + + assert.strictEqual(comment.post, post2); + assert.strictEqual(post.bestComment, null); + assert.strictEqual(post2.bestComment, comment); + }); + + test('When setting a belongsTo, the OneToOne invariant is transitive', async function (assert) { + class Post extends Model { + @belongsTo('comment', { async: false }) + bestComment; + } + + class Comment extends Model { + @belongsTo('post', { async: false }) + post; + } + + register('model:Post', Post); + register('model:Comment', Comment); + + const comment = store.createRecord('comment'); + const post = store.createRecord('post'); + const post2 = store.createRecord('post'); + + comment.set('post', post); + + assert.strictEqual(comment.post, post, 'comment post is set correctly'); + assert.strictEqual(post.bestComment, comment, 'post1 comment is set correctly'); + assert.strictEqual(post2.bestComment, null, 'post2 comment is not set'); + + post2.set('bestComment', comment); + + assert.strictEqual(comment.post, post2, 'comment post is set correctly'); + assert.strictEqual(post.bestComment, null, 'post1 comment is no longer set'); + assert.strictEqual(post2.bestComment, comment, 'post2 comment is set correctly'); + }); + + test('When setting a belongsTo, the OneToOne invariant is commutative', async function (assert) { + class Post extends Model { + @belongsTo('comment', { async: false }) + bestComment; + } + + class Comment extends Model { + @belongsTo('post', { async: false }) + post; + } + + register('model:Post', Post); + register('model:Comment', Comment); + + const post = store.createRecord('post'); + const comment = store.createRecord('comment'); + const comment2 = store.createRecord('comment'); + + comment.set('post', post); + + assert.strictEqual(comment.post, post); + assert.strictEqual(post.bestComment, comment); + assert.strictEqual(comment2.post, null); + + post.set('bestComment', comment2); + + assert.strictEqual(comment.post, null); + assert.strictEqual(post.bestComment, comment2); + assert.strictEqual(comment2.post, post); + }); + + test('OneToNone relationship works', async function (assert) { + assert.expect(3); + + class Post extends Model { + @attr('string') + name; + } + + class Comment extends Model { + @belongsTo('post', { async: false }) + post; + } + + register('model:Post', Post); + register('model:Comment', Comment); + + const comment = store.createRecord('comment'); + const post1 = store.createRecord('post'); + const post2 = store.createRecord('post'); + + comment.set('post', post1); + assert.strictEqual(comment.post, post1, 'the post is set to the first one'); + + comment.set('post', post2); + assert.strictEqual(comment.post, post2, 'the post is set to the second one'); + + comment.set('post', post1); + assert.strictEqual(comment.post, post1, 'the post is re-set to the first one'); + }); + + test('When a record is added to or removed from a polymorphic has-many relationship, the inverse belongsTo can be set explicitly', async function (assert) { + class User extends Model { + @hasMany('message', { async: false, inverse: 'redUser', polymorphic: true }) + messages; + } + + class Message extends Model { + @belongsTo('user', { async: false }) + oneUser; + + @belongsTo('user', { async: false }) + twoUser; + + @belongsTo('user', { async: false }) + redUser; + + @belongsTo('user', { async: false }) + blueUser; + } + + class Post extends Message {} + + register('model:User', User); + register('model:Message', Message); + register('model:Post', Post); + + const post = store.createRecord('post'); + const user = store.createRecord('user'); + + assert.strictEqual(post.oneUser, null, 'oneUser has not been set on the user'); + assert.strictEqual(post.twoUser, null, 'twoUser has not been set on the user'); + assert.strictEqual(post.redUser, null, 'redUser has not been set on the user'); + assert.strictEqual(post.blueUser, null, 'blueUser has not been set on the user'); + + user.messages.push(post); + + assert.strictEqual(post.oneUser, null, 'oneUser has not been set on the user'); + assert.strictEqual(post.twoUser, null, 'twoUser has not been set on the user'); + assert.strictEqual(post.redUser, user, 'redUser has been set on the user'); + assert.strictEqual(post.blueUser, null, 'blueUser has not been set on the user'); + + user.messages.pop(); + + assert.strictEqual(post.oneUser, null, 'oneUser has not been set on the user'); + assert.strictEqual(post.twoUser, null, 'twoUser has not been set on the user'); + assert.strictEqual(post.redUser, null, 'redUser has bot been set on the user'); + assert.strictEqual(post.blueUser, null, 'blueUser has not been set on the user'); + + assert.expectDeprecation({ id: 'ember-data:non-explicit-relationships', count: 1 }); + }); + + test("When a record's belongsTo relationship is set, it can specify the inverse polymorphic hasMany to which the new child should be added or removed", async function (assert) { + class User extends Model { + @hasMany('message', { polymorphic: true, async: false }) + meMessages; + + @hasMany('message', { polymorphic: true, async: false }) + youMessages; + + @hasMany('message', { polymorphic: true, async: false }) + everyoneWeKnowMessages; + } + + class Message extends Model { + @belongsTo('user', { inverse: 'youMessages', async: false, as: 'message' }) + user; + } + + class Post extends Message {} + + register('model:User', User); + register('model:Message', Message); + register('model:Post', Post); + + const user = store.createRecord('user'); + const post = store.createRecord('post'); + + assert.strictEqual(user.meMessages.length, 0, 'meMessages has no posts'); + assert.strictEqual(user.youMessages.length, 0, 'youMessages has no posts'); + assert.strictEqual(user.everyoneWeKnowMessages.length, 0, 'everyoneWeKnowMessages has no posts'); + + post.set('user', user); + + assert.strictEqual(user.meMessages.length, 0, 'meMessages has no posts'); + assert.strictEqual(user.youMessages.length, 1, 'youMessages had the post added'); + assert.strictEqual(user.everyoneWeKnowMessages.length, 0, 'everyoneWeKnowMessages has no posts'); + + post.set('user', null); + + assert.strictEqual(user.meMessages.length, 0, 'meMessages has no posts'); + assert.strictEqual(user.youMessages.length, 0, 'youMessages has no posts'); + assert.strictEqual(user.everyoneWeKnowMessages.length, 0, 'everyoneWeKnowMessages has no posts'); + }); + + test("When a record's polymorphic belongsTo relationship is set, it can specify the inverse hasMany to which the new child should be added", async function (assert) { + class Message extends Model { + @hasMany('comment', { inverse: null, async: false }) + meMessages; + + @hasMany('comment', { inverse: 'message', async: false, as: 'message' }) + youMessages; + + @hasMany('comment', { inverse: null, async: false }) + everyoneWeKnowMessages; + } + + class Post extends Message {} + + class Comment extends Message { + @belongsTo('message', { async: false, polymorphic: true, inverse: 'youMessages' }) + message; + } + + register('model:Message', Message); + register('model:Post', Post); + register('model:Comment', Comment); + + const comment = store.createRecord('comment'); + const post = store.createRecord('post'); + + assert.strictEqual(post.meMessages.length, 0, 'meMessages has no posts'); + assert.strictEqual(post.youMessages.length, 0, 'youMessages has no posts'); + assert.strictEqual(post.everyoneWeKnowMessages.length, 0, 'everyoneWeKnowMessages has no posts'); + + comment.set('message', post); + + assert.strictEqual(post.meMessages.length, 0, 'meMessages has no posts'); + assert.strictEqual(post.youMessages.length, 1, 'youMessages had the post added'); + assert.strictEqual(post.everyoneWeKnowMessages.length, 0, 'everyoneWeKnowMessages has no posts'); + + comment.set('message', null); + + assert.strictEqual(post.meMessages.length, 0, 'meMessages has no posts'); + assert.strictEqual(post.youMessages.length, 0, 'youMessages has no posts'); + assert.strictEqual(post.everyoneWeKnowMessages.length, 0, 'everyoneWeKnowMessages has no posts'); + }); + testInDebug("Inverse relationships that don't exist throw a nice error for a hasMany", async function (assert) { class User extends Model {} @@ -36,10 +453,15 @@ module('integration/relationships/inverse_relationships - Inverse Relationships' store.createRecord('comment'); - assert.expectAssertion(function () { - post = store.createRecord('post'); - post.comments; - }, /Expected a relationship schema for 'comment.testPost' to match the inverse of 'post.comments', but no relationship schema was found./); + assert.expectAssertion( + function () { + post = store.createRecord('post'); + post.comments; + }, + DEPRECATE_RELATIONSHIPS_WITHOUT_INVERSE + ? /We found no field named 'testPost' on the schema for 'comment' to be the inverse of the 'comments' relationship on 'post'. This is most likely due to a missing field on your model definition./ + : /Expected a relationship schema for 'comment.testPost' to match the inverse of 'post.comments', but no relationship schema was found./ + ); }); testInDebug("Inverse relationships that don't exist throw a nice error for a belongsTo", async function (assert) { @@ -59,10 +481,157 @@ module('integration/relationships/inverse_relationships - Inverse Relationships' let post; store.createRecord('user'); - assert.expectAssertion(function () { - post = store.createRecord('post'); - post.user; - }, /Expected a relationship schema for 'user.testPost' to match the inverse of 'post.user', but no relationship schema was found./); + assert.expectAssertion( + function () { + post = store.createRecord('post'); + post.user; + }, + DEPRECATE_RELATIONSHIPS_WITHOUT_INVERSE + ? /We found no field named 'testPost' on the schema for 'user' to be the inverse of the 'user' relationship on 'post'. This is most likely due to a missing field on your model definition./ + : /Expected a relationship schema for 'user.testPost' to match the inverse of 'post.user', but no relationship schema was found./ + ); + }); + + test('inverseFor is only called when inverse is not null', async function (assert) { + assert.expect(2); + + class Post extends Model { + @hasMany('comment', { async: false, inverse: null }) + comments; + } + + class Comment extends Model { + @belongsTo('post', { async: false, inverse: null }) + post; + } + + class User extends Model { + @hasMany('message', { async: false, inverse: 'user' }) + messages; + } + + class Message extends Model { + @belongsTo('user', { async: false, inverse: 'messages' }) + user; + } + + register('model:Post', Post); + register('model:Comment', Comment); + register('model:User', User); + register('model:Message', Message); + + Post.inverseFor = function () { + assert.notOk(true, 'Post model inverseFor is not called'); + }; + + Comment.inverseFor = function () { + assert.notOk(true, 'Comment model inverseFor is not called'); + }; + + Message.inverseFor = function () { + assert.ok(true, 'Message model inverseFor is called'); + }; + + User.inverseFor = function () { + assert.ok(true, 'User model inverseFor is called'); + }; + + store.push({ + data: { + id: '1', + type: 'post', + relationships: { + comments: { + data: [ + { + id: '1', + type: 'comment', + }, + { + id: '2', + type: 'comment', + }, + ], + }, + }, + }, + }); + store.push({ + data: [ + { + id: '1', + type: 'comment', + relationships: { + post: { + data: { + id: '1', + type: 'post', + }, + }, + }, + }, + { + id: '2', + type: 'comment', + relationships: { + post: { + data: { + id: '1', + type: 'post', + }, + }, + }, + }, + ], + }); + store.push({ + data: { + id: '1', + type: 'user', + relationships: { + messages: { + data: [ + { + id: '1', + type: 'message', + }, + { + id: '2', + type: 'message', + }, + ], + }, + }, + }, + }); + store.push({ + data: [ + { + id: '1', + type: 'message', + relationships: { + user: { + data: { + id: '1', + type: 'user', + }, + }, + }, + }, + { + id: '2', + type: 'message', + relationships: { + post: { + data: { + id: '1', + type: 'user', + }, + }, + }, + }, + ], + }); }); testInDebug( @@ -74,25 +643,65 @@ module('integration/relationships/inverse_relationships - Inverse Relationships' } register('model:user', User); - assert.expectAssertion(() => { - store.push({ - data: { - id: '1', - type: 'user', - relationships: { - post: { - data: { - id: '1', - type: 'post', - }, - }, - }, - }, - }); - }, /Missing Schema: Encountered a relationship identifier { type: 'post', id: '1' } for the 'user.post' belongsTo relationship on , but no schema exists for that type./); + assert.expectAssertion( + () => { + store.createRecord('user', { post: null }); + }, + DEPRECATE_RELATIONSHIPS_WITHOUT_INVERSE + ? /No model was found for 'post' and no schema handles the type/ + : /Missing Schema: Encountered a relationship identifier { type: 'post', id: '1' } for the 'user.post' belongsTo relationship on , but no schema exists for that type./ + ); // but don't error if the relationship is not used store.createRecord('user', {}); } ); + + test('No inverse configuration - should default to a null inverse', async function (assert) { + class User extends Model {} + + class Comment extends Model { + @belongsTo('user', { async: true }) + user; + } + + register('model:User', User); + register('model:Comment', Comment); + + const comment = store.createRecord('comment'); + + assert.strictEqual(comment.inverseFor('user'), null, 'Defaults to a null inverse'); + }); + + test('Unload a destroyed record should clean the relations', async function (assert) { + assert.expect(2); + + class Post extends Model { + @hasMany('comment', { async: true, inverse: 'post' }) + comments; + } + + class Comment extends Model { + @belongsTo('post', { async: true, inverse: 'comments' }) + post; + } + + register('model:post', Post); + register('model:comment', Comment); + + const comment = store.createRecord('comment'); + const post = store.createRecord('post'); + const comments = await post.comments; + comments.push(comment); + const identifier = recordIdentifierFor(comment); + + await comment.destroyRecord(); + + assert.false(graphFor(store).identifiers.has(identifier), 'relationships are cleared'); + assert.strictEqual( + store._instanceCache.peek({ identifier, bucket: 'resourceCache' }), + undefined, + 'The cache is destroyed' + ); + }); }); diff --git a/tests/main/tests/integration/relationships/one-to-many-test.js b/tests/main/tests/integration/relationships/one-to-many-test.js index 0f1e830af61..ce59fd8fa13 100644 --- a/tests/main/tests/integration/relationships/one-to-many-test.js +++ b/tests/main/tests/integration/relationships/one-to-many-test.js @@ -5,6 +5,7 @@ import { setupTest } from 'ember-qunit'; import Adapter from '@ember-data/adapter'; import Model, { attr, belongsTo, hasMany } from '@ember-data/model'; import JSONAPISerializer from '@ember-data/serializer/json-api'; +import { deprecatedTest } from '@ember-data/unpublished-test-infra/test-support/deprecated-test'; module('integration/relationships/one_to_many_test - OneToMany relationships', function (hooks) { setupTest(hooks); @@ -1483,4 +1484,50 @@ module('integration/relationships/one_to_many_test - OneToMany relationships', f assert.strictEqual(user.accounts.length, 0, 'User does not have the account anymore'); assert.strictEqual(account.user, null, 'Account does not have the user anymore'); }); + + deprecatedTest( + 'createRecord updates inverse record array which has observers', + { id: 'ember-data:deprecate-promise-many-array-behaviors', until: '5.0', count: 3 }, + async function (assert) { + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); + + adapter.findAll = () => { + return { + data: [ + { + id: '2', + type: 'user', + attributes: { + name: 'Stanley', + }, + }, + ], + }; + }; + + const users = await store.findAll('user'); + assert.strictEqual(users.length, 1, 'Exactly 1 user'); + + const user = users.at(0); + assert.strictEqual(user.messages.length, 0, 'Record array is initially empty'); + + // set up an observer + user.addObserver('messages.@each.title', () => {}); + user.messages.objectAt(0); + + const messages = await user.messages; + + assert.strictEqual(messages.length, 0, 'we have no messages'); + assert.strictEqual(user.messages.length, 0, 'we have no messages'); + + const message = store.createRecord('message', { user, title: 'EmberFest was great' }); + assert.strictEqual(messages.length, 1, 'The message is added to the record array'); + assert.strictEqual(user.messages.length, 1, 'The message is added to the record array'); + + const messageFromArray = user.messages.objectAt(0); + assert.strictEqual(message, messageFromArray, 'Only one message record instance should be created'); + assert.expectDeprecation({ id: 'ember-data:deprecate-array-like', count: 3 }); + } + ); }); diff --git a/tests/main/tests/integration/relationships/one-to-one-test.js b/tests/main/tests/integration/relationships/one-to-one-test.js index a2ceff7fd8d..2487e22daa0 100644 --- a/tests/main/tests/integration/relationships/one-to-one-test.js +++ b/tests/main/tests/integration/relationships/one-to-one-test.js @@ -7,7 +7,9 @@ import { setupTest } from 'ember-qunit'; import Adapter from '@ember-data/adapter'; import Model, { attr, belongsTo } from '@ember-data/model'; import JSONAPISerializer from '@ember-data/serializer/json-api'; +import { deprecatedTest } from '@ember-data/unpublished-test-infra/test-support/deprecated-test'; import testInDebug from '@ember-data/unpublished-test-infra/test-support/test-in-debug'; +import { DEPRECATE_PROMISE_PROXIES } from '@warp-drive/build-config/deprecations'; module('integration/relationships/one_to_one_test - OneToOne relationships', function (hooks) { setupTest(hooks); @@ -427,6 +429,109 @@ module('integration/relationships/one_to_one_test - OneToOne relationships', fun assert.strictEqual(job.user, user, 'User relationship was set up correctly'); }); + deprecatedTest( + 'Setting a BelongsTo to a promise unwraps the promise before setting- async', + { id: 'ember-data:deprecate-promise-proxies', until: '5.0', count: 1 }, + async function (assert) { + const store = this.owner.lookup('service:store'); + const stanley = store.push({ + data: { + id: '1', + type: 'user', + attributes: { + name: 'Stanley', + }, + relationships: { + bestFriend: { + data: { + id: '2', + type: 'user', + }, + }, + }, + }, + }); + const stanleysFriend = store.push({ + data: { + id: '2', + type: 'user', + attributes: { + name: "Stanley's friend", + }, + }, + }); + const newFriend = store.push({ + data: { + id: '3', + type: 'user', + attributes: { + name: 'New friend', + }, + }, + }); + + newFriend.bestFriend = stanleysFriend.bestFriend; + const fetchedUser = await stanley.bestFriend; + assert.strictEqual( + fetchedUser, + newFriend, + `Stanley's bestFriend relationship was updated correctly to newFriend` + ); + const fetchedUser2 = await newFriend.bestFriend; + assert.strictEqual( + fetchedUser2, + stanley, + `newFriend's bestFriend relationship was updated correctly to be Stanley` + ); + } + ); + + deprecatedTest( + 'Setting a BelongsTo to a promise works when the promise returns null- async', + { id: 'ember-data:deprecate-promise-proxies', until: '5.0', count: 1 }, + async function (assert) { + const store = this.owner.lookup('service:store'); + store.push({ + data: { + id: '1', + type: 'user', + attributes: { + name: 'Stanley', + }, + }, + }); + const igor = store.push({ + data: { + id: '2', + type: 'user', + attributes: { + name: 'Igor', + }, + }, + }); + const newFriend = store.push({ + data: { + id: '3', + type: 'user', + attributes: { + name: 'New friend', + }, + relationships: { + bestFriend: { + data: { + id: '1', + type: 'user', + }, + }, + }, + }, + }); + newFriend.bestFriend = igor.bestFriend; + const fetchedUser = await newFriend.bestFriend; + assert.strictEqual(fetchedUser, null, 'User relationship was updated correctly'); + } + ); + testInDebug("Setting a BelongsTo to a promise that didn't come from a relationship errors out", function (assert) { const store = this.owner.lookup('service:store'); @@ -457,11 +562,87 @@ module('integration/relationships/one_to_one_test - OneToOne relationships', fun }, }); - assert.expectAssertion(function () { - stanley.bestFriend = Promise.resolve(igor); - }, '[object Promise] is not a record instantiated by @ember-data/store'); + assert.expectAssertion( + function () { + stanley.bestFriend = Promise.resolve(igor); + }, + DEPRECATE_PROMISE_PROXIES + ? /You passed in a promise that did not originate from an EmberData relationship. You can only pass promises that come from a belongsTo or hasMany relationship to the get call./ + : '[object Promise] is not a record instantiated by @ember-data/store' + ); }); + deprecatedTest( + 'Setting a BelongsTo to a promise multiple times is resistant to race conditions when the first set resolves quicker', + { id: 'ember-data:deprecate-promise-proxies', until: '5.0', count: 2 }, + async function (assert) { + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); + + const stanley = store.push({ + data: { + id: '1', + type: 'user', + attributes: { + name: 'Stanley', + }, + relationships: { + bestFriend: { + data: { + id: '2', + type: 'user', + }, + }, + }, + }, + }); + const igor = store.push({ + data: { + id: '3', + type: 'user', + attributes: { + name: 'Igor', + }, + relationships: { + bestFriend: { + data: { + id: '5', + type: 'user', + }, + }, + }, + }, + }); + const newFriend = store.push({ + data: { + id: '7', + type: 'user', + attributes: { + name: 'New friend', + }, + }, + }); + + adapter.findRecord = function (store, type, id, snapshot) { + if (id === '5') { + return Promise.resolve({ data: { id: '5', type: 'user', attributes: { name: "Igor's friend" } } }); + } else if (id === '2') { + return Promise.resolve({ data: { id: '2', type: 'user', attributes: { name: "Stanley's friend" } } }); + } + }; + + const stanleyPromise = stanley.bestFriend; + const igorPromise = igor.bestFriend; + + await Promise.all([stanleyPromise, igorPromise]); + newFriend.bestFriend = stanleyPromise; + newFriend.bestFriend = igorPromise; + + const fetchedUser = await newFriend.bestFriend; + assert.strictEqual(fetchedUser?.name, "Igor's friend", 'User relationship was updated correctly'); + } + ); + test('Setting a OneToOne relationship to null reflects correctly on the other side - async', async function (assert) { const store = this.owner.lookup('service:store'); diff --git a/tests/main/tests/integration/relationships/polymorphic-mixins-belongs-to-test.js b/tests/main/tests/integration/relationships/polymorphic-mixins-belongs-to-test.js index 0d6675ba25b..8c4c41bb34b 100644 --- a/tests/main/tests/integration/relationships/polymorphic-mixins-belongs-to-test.js +++ b/tests/main/tests/integration/relationships/polymorphic-mixins-belongs-to-test.js @@ -8,6 +8,7 @@ import Adapter from '@ember-data/adapter'; import Model, { attr, belongsTo } from '@ember-data/model'; import JSONAPISerializer from '@ember-data/serializer/json-api'; import testInDebug from '@ember-data/unpublished-test-infra/test-support/test-in-debug'; +import { DEPRECATE_NON_EXPLICIT_POLYMORPHISM } from '@warp-drive/build-config/deprecations'; module( 'integration/relationships/polymorphic_mixins_belongs_to_test - Polymorphic belongsTo relationships with mixins', @@ -140,7 +141,9 @@ module( function () { user.bestMessage = video; }, - `No 'user' field exists on 'not-message'. To use this type in the polymorphic relationship 'user.bestMessage' the relationships schema definition for not-message should include: + DEPRECATE_NON_EXPLICIT_POLYMORPHISM + ? "The 'not-message' type does not implement 'message' and thus cannot be assigned to the 'bestMessage' relationship in 'user'. Make it a descendant of 'message' or use a mixin of the same name." + : `No 'user' field exists on 'not-message'. To use this type in the polymorphic relationship 'user.bestMessage' the relationships schema definition for not-message should include: \`\`\` { diff --git a/tests/main/tests/integration/relationships/polymorphic-mixins-has-many-test.js b/tests/main/tests/integration/relationships/polymorphic-mixins-has-many-test.js index 335a6cdca2e..7b9ebfad940 100644 --- a/tests/main/tests/integration/relationships/polymorphic-mixins-has-many-test.js +++ b/tests/main/tests/integration/relationships/polymorphic-mixins-has-many-test.js @@ -8,6 +8,7 @@ import Adapter from '@ember-data/adapter'; import Model, { attr, belongsTo, hasMany } from '@ember-data/model'; import JSONAPISerializer from '@ember-data/serializer/json-api'; import testInDebug from '@ember-data/unpublished-test-infra/test-support/test-in-debug'; +import { DEPRECATE_NON_EXPLICIT_POLYMORPHISM } from '@warp-drive/build-config/deprecations'; module( 'integration/relationships/polymorphic_mixins_has_many_test - Polymorphic hasMany relationships with mixins', @@ -183,12 +184,9 @@ module( ], }); - const fetchedMessages = await user.messages; - assert.expectAssertion( - function () { - fetchedMessages.push(notMessage); - }, - `No 'user' field exists on 'not-message'. To use this type in the polymorphic relationship 'user.messages' the relationships schema definition for not-message should include: + const expectedError = DEPRECATE_NON_EXPLICIT_POLYMORPHISM + ? /The 'not-message' type does not implement 'message' and thus cannot be assigned to the 'messages' relationship in 'user'. Make it a descendant of 'message/ + : `No 'user' field exists on 'not-message'. To use this type in the polymorphic relationship 'user.messages' the relationships schema definition for not-message should include: \`\`\` { @@ -206,7 +204,15 @@ module( } \`\`\` -` +`; + + const fetchedMessages = await user.messages; + assert.expectAssertion( + function () { + fetchedMessages.push(notMessage); + }, + expectedError, + `expected an error to match ${expectedError}` ); } ); diff --git a/tests/main/tests/integration/relationships/promise-many-array-test.js b/tests/main/tests/integration/relationships/promise-many-array-test.js new file mode 100644 index 00000000000..1472d937c2b --- /dev/null +++ b/tests/main/tests/integration/relationships/promise-many-array-test.js @@ -0,0 +1,154 @@ +import { A } from '@ember/array'; +import EmberObject, { computed } from '@ember/object'; +import { filterBy } from '@ember/object/computed'; +import { settled } from '@ember/test-helpers'; + +import { module } from 'qunit'; + +import { setupRenderingTest } from 'ember-qunit'; + +import Model, { attr, hasMany } from '@ember-data/model'; +import { deprecatedTest } from '@ember-data/unpublished-test-infra/test-support/deprecated-test'; +import { DEPRECATE_PROMISE_MANY_ARRAY_BEHAVIORS } from '@warp-drive/build-config/deprecations'; + +module('PromiseManyArray', (hooks) => { + setupRenderingTest(hooks); + + deprecatedTest( + 'PromiseManyArray is not side-affected by EmberArray', + { id: 'ember-data:no-a-with-array-like', until: '5.0', count: 1 }, + async function (assert) { + const { owner } = this; + class Person extends Model { + @attr('string') name; + } + class Group extends Model { + @hasMany('person', { async: true, inverse: null }) members; + } + owner.register('model:person', Person); + owner.register('model:group', Group); + const store = owner.lookup('service:store'); + const members = ['Bob', 'John', 'Michael', 'Larry', 'Lucy'].map((name) => store.createRecord('person', { name })); + const group = store.createRecord('group', { members }); + + const forEachFn = group.members.forEach; + assert.strictEqual(group.members.length, 5, 'initial length is correct'); + + if (DEPRECATE_PROMISE_MANY_ARRAY_BEHAVIORS) { + group.members.replace(0, 1); + assert.strictEqual(group.members.length, 4, 'updated length is correct'); + assert.expectDeprecation({ id: 'ember-data:deprecate-array-like' }); + } + + A(group.members); + + assert.strictEqual(forEachFn, group.members.forEach, 'we have the same function for forEach'); + + if (DEPRECATE_PROMISE_MANY_ARRAY_BEHAVIORS) { + group.members.replace(0, 1); + assert.strictEqual(group.members.length, 3, 'updated length is correct'); + // we'll want to use a different test for this but will want to still ensure we are not side-affected + assert.expectDeprecation({ id: 'ember-data:deprecate-promise-many-array-behaviors', until: '5.0', count: 2 }); + assert.expectDeprecation({ id: 'ember-data:deprecate-array-like' }); + } + } + ); + + deprecatedTest( + 'PromiseManyArray can be subscribed to by computed chains', + { id: 'ember-data:deprecate-promise-many-array-behaviors', until: '5.0', count: 16 }, + async function (assert) { + const { owner } = this; + class Person extends Model { + @attr('string') name; + } + class Group extends Model { + @hasMany('person', { async: true, inverse: null }) members; + + @computed('members.@each.id') + get memberIds() { + return this.members.map((m) => m.id); + } + + @filterBy('members', 'name', 'John') + johns; + } + owner.register('model:person', Person); + owner.register('model:group', Group); + owner.register( + 'serializer:application', + class extends EmberObject { + normalizeResponse(_, __, data) { + return data; + } + } + ); + + let _id = 0; + const names = ['Bob', 'John', 'Michael', 'John', 'Larry', 'Lucy']; + owner.register( + 'adapter:application', + class extends EmberObject { + findRecord(_store, _schema, id) { + assert.step(`findRecord ${id}`); + assert.strictEqual(id, String(_id + 1), 'findRecord id is correct'); + const name = names[_id++]; + const data = { + type: 'person', + id: `${_id}`, + attributes: { + name, + }, + }; + return { data }; + } + } + ); + const store = owner.lookup('service:store'); + + const group = store.push({ + data: { + type: 'group', + id: '1', + relationships: { + members: { + data: [ + { type: 'person', id: '1' }, + { type: 'person', id: '2' }, + { type: 'person', id: '3' }, + { type: 'person', id: '4' }, + { type: 'person', id: '5' }, + { type: 'person', id: '6' }, + ], + }, + }, + }, + }); + + // access the group data + let memberIds = group.memberIds; + let johnRecords = group.johns; + assert.strictEqual(memberIds.length, 0, 'member ids is 0 initially'); + assert.strictEqual(johnRecords.length, 0, 'john ids is 0 initially'); + + await settled(); + + assert.verifySteps([ + 'findRecord 1', + 'findRecord 2', + 'findRecord 3', + 'findRecord 4', + 'findRecord 5', + 'findRecord 6', + ]); + + memberIds = group.memberIds; + johnRecords = group.johns; + assert.strictEqual(memberIds.length, 6, 'memberIds length is correct'); + assert.strictEqual(johnRecords.length, 2, 'johnRecords length is correct'); + assert.strictEqual(group.members.length, 6, 'members length is correct'); + assert.expectDeprecation({ id: 'ember-data:no-a-with-array-like', count: 2 }); + assert.expectDeprecation({ id: 'ember-data:deprecate-array-like', count: 12 }); + } + ); +}); diff --git a/tests/main/tests/integration/relationships/rollback-test.ts b/tests/main/tests/integration/relationships/rollback-test.ts index 934b64983fe..e078d83af18 100644 --- a/tests/main/tests/integration/relationships/rollback-test.ts +++ b/tests/main/tests/integration/relationships/rollback-test.ts @@ -284,47 +284,22 @@ module('Integration | Relationships | Rollback', function (hooks) { assert.false(store.cache.hasChangedRelationships(appIdentifier), 'hasMany is clean'); }); - test('it returns the correct keys when a hasMany has state re-ordered (no-initial access)', function (assert) { - const store = this.owner.lookup('service:store') as Store; - const app = store.peekRecord('app', '1') as App; - const config1 = store.peekRecord('config', '1') as Config; - const config2 = store.peekRecord('config', '2') as Config; - const config3 = store.peekRecord('config', '3') as Config; - const appIdentifier = store.identifierCache.getOrCreateRecordIdentifier({ type: 'app', id: '1' }); - - app.configs.splice(app.configs.indexOf(config1), 1); - app.configs.push(config1); - assert.true(store.cache.hasChangedRelationships(appIdentifier), 'a hasMany has state removed'); - assert.arrayStrictEquals(app.configs, [config2, config3, config1], 'hasMany reordering has occurred'); - assert.strictEqual(config1.app, app, 'config1 app is correct'); - - const changed = store.cache.rollbackRelationships(appIdentifier); - assert.arrayStrictEquals(changed, ['configs'], 'hasMany has rolled back'); - assert.arrayStrictEquals(app.configs, [config1, config2, config3], 'hasMany has rolled back'); - assert.strictEqual(config2.app, app, 'config2 has rolled back'); - assert.false(store.cache.hasChangedRelationships(appIdentifier), 'hasMany is clean'); - }); - test('it returns the correct keys when a hasMany has state re-ordered', function (assert) { const store = this.owner.lookup('service:store') as Store; const app = store.peekRecord('app', '1') as App; const config1 = store.peekRecord('config', '1') as Config; const config2 = store.peekRecord('config', '2') as Config; const config3 = store.peekRecord('config', '3') as Config; - const appIdentifier = store.identifierCache.getOrCreateRecordIdentifier({ type: 'app', id: '1' }); - - assert.false(store.cache.hasChangedRelationships(appIdentifier), 'the hasMany is in a clean state'); - assert.arrayStrictEquals(app.configs, [config1, config2, config3], 'hasMany is in the correct starting order'); - app.configs.splice(app.configs.indexOf(config1), 1); app.configs.push(config1); + const appIdentifier = store.identifierCache.getOrCreateRecordIdentifier({ type: 'app', id: '1' }); assert.true(store.cache.hasChangedRelationships(appIdentifier), 'a hasMany has state removed'); assert.arrayStrictEquals(app.configs, [config2, config3, config1], 'hasMany reordering has occurred'); assert.strictEqual(config1.app, app, 'config1 app is correct'); const changed = store.cache.rollbackRelationships(appIdentifier); assert.arrayStrictEquals(changed, ['configs'], 'hasMany has rolled back'); - assert.arrayStrictEquals(app.configs, [config1, config2, config3], 'hasMany has restored the initial order'); + assert.arrayStrictEquals(app.configs, [config1, config2, config3], 'hasMany has rolled back'); assert.strictEqual(config2.app, app, 'config2 has rolled back'); assert.false(store.cache.hasChangedRelationships(appIdentifier), 'hasMany is clean'); }); @@ -394,16 +369,9 @@ module('Integration | Relationships | Rollback', function (hooks) { const config1 = store.peekRecord('config', '1') as Config; const config2 = store.peekRecord('config', '2') as Config; const config3 = store.peekRecord('config', '3') as Config; - - // app.configs is [1, 2, 3] initially const app = config1.app!; - - // we update the config1.app to point at a different app, - // which will remove config1 from app:1's list of configs config1.app = store.peekRecord('app', '2') as App; const configIdentifier = store.identifierCache.getOrCreateRecordIdentifier({ type: 'config', id: '1' }); - - // confirm the mutation assert.true(store.cache.hasChangedRelationships(configIdentifier), 'a belongsTo has state replaced'); assert.arrayStrictEquals(app.configs, [config2, config3], 'inverse has updated'); @@ -411,7 +379,6 @@ module('Integration | Relationships | Rollback', function (hooks) { assert.arrayStrictEquals(changed, ['app'], 'belongsTo has rolled back'); assert.strictEqual(config1.app, app, 'belongsTo has rolled back'); - // this is in a different order because we don't rollback the inverse except for the smaller specific change // this is a bit of a weird case, but it's the way it works // if we were to rollback the inverse, we'd have to rollback the inverse of the inverse, and so on diff --git a/tests/main/tests/integration/snapshot-test.js b/tests/main/tests/integration/snapshot-test.js index 191e9d66891..8accd4572ef 100644 --- a/tests/main/tests/integration/snapshot-test.js +++ b/tests/main/tests/integration/snapshot-test.js @@ -6,6 +6,7 @@ import JSONAPIAdapter from '@ember-data/adapter/json-api'; import { FetchManager, Snapshot } from '@ember-data/legacy-compat/-private'; import Model, { attr, belongsTo, hasMany } from '@ember-data/model'; import JSONAPISerializer from '@ember-data/serializer/json-api'; +import { deprecatedTest } from '@ember-data/unpublished-test-infra/test-support/deprecated-test'; let owner, store; @@ -108,6 +109,45 @@ module('integration/snapshot - Snapshot', function (hooks) { assert.strictEqual(snapshot.modelName, 'post', 'modelName is correct'); }); + deprecatedTest( + 'snapshot.type loads the class lazily', + { + id: 'ember-data:deprecate-snapshot-model-class-access', + count: 1, + until: '5.0', + }, + async function (assert) { + assert.expect(3); + + let postClassLoaded = false; + const modelFor = store.modelFor; + store.modelFor = (name) => { + if (name === 'post') { + postClassLoaded = true; + } + return modelFor.call(store, name); + }; + + await store._push({ + data: { + type: 'post', + id: '1', + attributes: { + title: 'Hello World', + }, + }, + }); + const identifier = store.identifierCache.getOrCreateRecordIdentifier({ type: 'post', id: '1' }); + const snapshot = await store._fetchManager.createSnapshot(identifier); + + assert.false(postClassLoaded, 'model class is not eagerly loaded'); + const type = snapshot.type; + assert.true(postClassLoaded, 'model class is loaded'); + const Post = store.modelFor('post'); + assert.strictEqual(type, Post, 'type is correct'); + } + ); + test('an initial findRecord call has no record for internal-model when a snapshot is generated', async function (assert) { assert.expect(2); store.adapterFor('application').findRecord = (store, type, id, snapshot) => { diff --git a/tests/main/tests/integration/store/adapter-for-test.js b/tests/main/tests/integration/store/adapter-for-test.js index d5573ba97d0..785785670ff 100644 --- a/tests/main/tests/integration/store/adapter-for-test.js +++ b/tests/main/tests/integration/store/adapter-for-test.js @@ -5,6 +5,8 @@ import { module, test } from 'qunit'; import Store from 'ember-data/store'; import { setupTest } from 'ember-qunit'; +import { deprecatedTest } from '@ember-data/unpublished-test-infra/test-support/deprecated-test'; + class TestAdapter { constructor(args) { Object.assign(this, args); @@ -158,6 +160,59 @@ module('integration/store - adapterFor', function (hooks) { assert.strictEqual(appAdapter, adapter, 'We fell back to the application adapter instance'); }); + deprecatedTest( + 'When the per-type, application and specified fallback adapters do not exist, we fallback to the -json-api adapter', + { + id: 'ember-data:deprecate-secret-adapter-fallback', + until: '5.0', + count: 2, + }, + async function (assert) { + const { owner } = this; + + let didInstantiateAdapter = false; + + class JsonApiAdapter extends TestAdapter { + didInit() { + didInstantiateAdapter = true; + } + } + + const lookup = owner.lookup; + owner.lookup = (registrationName) => { + if (registrationName === 'adapter:application') { + return undefined; + } + return lookup.call(owner, registrationName); + }; + + owner.unregister('adapter:-json-api'); + owner.register('adapter:-json-api', JsonApiAdapter); + + const adapter = store.adapterFor('person'); + + assert.ok(adapter instanceof JsonApiAdapter, 'We found the adapter'); + assert.ok(didInstantiateAdapter, 'We instantiated the adapter'); + didInstantiateAdapter = false; + + const appAdapter = store.adapterFor('application'); + + assert.ok(appAdapter instanceof JsonApiAdapter, 'We found the fallback -json-api adapter for application'); + assert.notOk(didInstantiateAdapter, 'We did not instantiate the adapter again'); + didInstantiateAdapter = false; + + const jsonApiAdapter = store.adapterFor('-json-api'); + assert.ok(jsonApiAdapter instanceof JsonApiAdapter, 'We found the correct adapter'); + assert.notOk(didInstantiateAdapter, 'We did not instantiate the adapter again'); + assert.strictEqual(jsonApiAdapter, appAdapter, 'We fell back to the -json-api adapter instance for application'); + assert.strictEqual( + jsonApiAdapter, + adapter, + 'We fell back to the -json-api adapter instance for the per-type adapter' + ); + } + ); + test('adapters are destroyed', async function (assert) { const { owner } = this; let didInstantiate = false; diff --git a/tests/main/tests/integration/store/query-test.js b/tests/main/tests/integration/store/query-test.js new file mode 100644 index 00000000000..f705cdd472a --- /dev/null +++ b/tests/main/tests/integration/store/query-test.js @@ -0,0 +1,49 @@ +import { module } from 'qunit'; + +import { setupTest } from 'ember-qunit'; + +import Adapter from '@ember-data/adapter'; +import Model from '@ember-data/model'; +import { createDeferred } from '@ember-data/request'; +import JSONAPISerializer from '@ember-data/serializer/json-api'; +import { deprecatedTest } from '@ember-data/unpublished-test-infra/test-support/deprecated-test'; + +module('integration/store/query', function (hooks) { + setupTest(hooks); + + hooks.beforeEach(function () { + class Person extends Model {} + + this.owner.register('model:person', Person); + this.owner.register('adapter:application', Adapter); + this.owner.register('serializer:application', class extends JSONAPISerializer {}); + }); + + deprecatedTest( + 'meta is proxied correctly on the PromiseArray', + { id: 'ember-data:deprecate-promise-proxies', until: '5.0', count: 2 }, + async function (assert) { + const store = this.owner.lookup('service:store'); + + const defered = createDeferred(); + + this.owner.register( + 'adapter:person', + class extends Adapter { + query(store, type, query) { + return defered.promise; + } + } + ); + + const result = store.query('person', {}); + + assert.notOk(result.meta?.foo, 'precond: meta is not yet set'); + + defered.resolve({ data: [], meta: { foo: 'bar' } }); + await result; + + assert.strictEqual(result.meta?.foo, 'bar', 'meta is now proxied'); + } + ); +}); diff --git a/tests/main/tests/unit/adapter-errors-test.js b/tests/main/tests/unit/adapter-errors-test.js index a5db6d55b6c..356d7979bfb 100644 --- a/tests/main/tests/unit/adapter-errors-test.js +++ b/tests/main/tests/unit/adapter-errors-test.js @@ -3,6 +3,8 @@ import { module, test } from 'qunit'; import AdapterError, { AbortError, ConflictError, + errorsArrayToHash, + errorsHashToArray, ForbiddenError, InvalidError, NotFoundError, @@ -11,6 +13,7 @@ import AdapterError, { UnauthorizedError, } from '@ember-data/adapter/error'; import testInDebug from '@ember-data/unpublished-test-infra/test-support/test-in-debug'; +import { DEPRECATE_HELPERS } from '@warp-drive/build-config/deprecations'; module('unit/adapter-errors - AdapterError', function () { test('AdapterError', function (assert) { @@ -110,6 +113,83 @@ module('unit/adapter-errors - AdapterError', function () { assert.strictEqual(error.message, 'custom error!'); }); + if (DEPRECATE_HELPERS) { + const errorsHash = { + name: ['is invalid', 'must be a string'], + age: ['must be a number'], + }; + + const errorsArray = [ + { + title: 'Invalid Attribute', + detail: 'is invalid', + source: { pointer: '/data/attributes/name' }, + }, + { + title: 'Invalid Attribute', + detail: 'must be a string', + source: { pointer: '/data/attributes/name' }, + }, + { + title: 'Invalid Attribute', + detail: 'must be a number', + source: { pointer: '/data/attributes/age' }, + }, + ]; + + const errorsPrimaryHash = { + base: ['is invalid', 'error message'], + }; + + const errorsPrimaryArray = [ + { + title: 'Invalid Document', + detail: 'is invalid', + source: { pointer: '/data' }, + }, + { + title: 'Invalid Document', + detail: 'error message', + source: { pointer: '/data' }, + }, + ]; + + test('errorsHashToArray', function (assert) { + const result = errorsHashToArray(errorsHash); + assert.deepEqual(result, errorsArray); + assert.expectDeprecation({ id: 'ember-data:deprecate-errors-hash-to-array-helper', count: 1 }); + }); + + test('errorsHashToArray for primary data object', function (assert) { + const result = errorsHashToArray(errorsPrimaryHash); + assert.deepEqual(result, errorsPrimaryArray); + assert.expectDeprecation({ id: 'ember-data:deprecate-errors-hash-to-array-helper', count: 1 }); + }); + + test('errorsArrayToHash', function (assert) { + const result = errorsArrayToHash(errorsArray); + assert.deepEqual(result, errorsHash); + assert.expectDeprecation({ id: 'ember-data:deprecate-errors-array-to-hash-helper', count: 1 }); + }); + + test('errorsArrayToHash without trailing slash', function (assert) { + const result = errorsArrayToHash([ + { + detail: 'error message', + source: { pointer: 'data/attributes/name' }, + }, + ]); + assert.deepEqual(result, { name: ['error message'] }); + assert.expectDeprecation({ id: 'ember-data:deprecate-errors-array-to-hash-helper', count: 1 }); + }); + + test('errorsArrayToHash for primary data object', function (assert) { + const result = errorsArrayToHash(errorsPrimaryArray); + assert.deepEqual(result, errorsPrimaryHash); + assert.expectDeprecation({ id: 'ember-data:deprecate-errors-array-to-hash-helper', count: 1 }); + }); + } + testInDebug('InvalidError will normalize errors hash will assert', function (assert) { assert.expectAssertion(function () { new InvalidError({ name: ['is invalid'] }); diff --git a/tests/main/tests/unit/custom-class-support/custom-class-model-test.ts b/tests/main/tests/unit/custom-class-support/custom-class-model-test.ts index 01a65a2cffc..34243a005bc 100644 --- a/tests/main/tests/unit/custom-class-support/custom-class-model-test.ts +++ b/tests/main/tests/unit/custom-class-support/custom-class-model-test.ts @@ -96,8 +96,8 @@ module('unit/model - Custom Class Model', function (hooks: NestedHooks) { store._join(() => { capabilities.notifyChange(identifier, 'relationships', 'key'); capabilities.notifyChange(identifier, 'relationships', 'key'); - capabilities.notifyChange(identifier, 'state', null); - capabilities.notifyChange(identifier, 'errors', null); + capabilities.notifyChange(identifier, 'state'); + capabilities.notifyChange(identifier, 'errors'); }); assert.strictEqual(notificationCount, 3, 'called notification callback'); diff --git a/tests/main/tests/unit/model/relationships-test.js b/tests/main/tests/unit/model/relationships-test.js index 988d410df99..6cceea0bde9 100644 --- a/tests/main/tests/unit/model/relationships-test.js +++ b/tests/main/tests/unit/model/relationships-test.js @@ -5,6 +5,7 @@ import { module, test } from 'qunit'; import { setupTest } from 'ember-qunit'; import Model, { belongsTo, hasMany } from '@ember-data/model'; +import { deprecatedTest } from '@ember-data/unpublished-test-infra/test-support/deprecated-test'; class Person extends Model { @hasMany('occupation', { async: false, inverse: null }) occupations; @@ -126,4 +127,36 @@ module('[@ember-data/model] unit - relationships', function (hooks) { assert.strictEqual(relationship.name, 'streamItems', 'relationship name has not been changed'); }); + + deprecatedTest( + 'decorators works without parens', + { id: 'ember-data:deprecate-non-strict-relationships', until: '5.0', count: 6 }, + function (assert) { + const { owner } = this; + + class StreamItem extends Model { + @belongsTo user; + } + + class User extends Model { + @hasMany streamItems; + } + + owner.unregister('model:user'); + owner.register('model:stream-item', StreamItem); + owner.register('model:user', User); + + const store = owner.lookup('service:store'); + + const user = store.modelFor('user'); + + const relationships = get(user, 'relationships'); + + assert.ok(relationships.has('stream-item'), 'relationship key has been normalized'); + + const relationship = relationships.get('stream-item')[0]; + + assert.strictEqual(relationship.name, 'streamItems', 'relationship name has not been changed'); + } + ); }); diff --git a/tests/main/tests/unit/model/relationships/has-many-test.js b/tests/main/tests/unit/model/relationships/has-many-test.js index 3d8b6bd4c40..39444d0ec81 100644 --- a/tests/main/tests/unit/model/relationships/has-many-test.js +++ b/tests/main/tests/unit/model/relationships/has-many-test.js @@ -12,7 +12,7 @@ import { recordIdentifierFor } from '@ember-data/store'; import { deprecatedTest } from '@ember-data/unpublished-test-infra/test-support/deprecated-test'; import testInDebug from '@ember-data/unpublished-test-infra/test-support/test-in-debug'; import todo from '@ember-data/unpublished-test-infra/test-support/todo'; -import { DEPRECATE_RELATIONSHIP_REMOTE_UPDATE_CLEARING_LOCAL_STATE } from '@warp-drive/build-config/deprecations'; +import { DEPRECATE_ARRAY_LIKE, DEPRECATE_MANY_ARRAY_DUPLICATES } from '@warp-drive/build-config/deprecations'; module('unit/model/relationships - hasMany', function (hooks) { setupTest(hooks); @@ -1369,471 +1369,236 @@ module('unit/model/relationships - hasMany', function (hooks) { ); }); - if (DEPRECATE_RELATIONSHIP_REMOTE_UPDATE_CLEARING_LOCAL_STATE) { - todo( - '[push hasMany] new items added to a hasMany relationship are not cleared by a store.push', - async function (assert) { - assert.expect(5); + todo( + '[push hasMany] new items added to a hasMany relationship are not cleared by a store.push', + async function (assert) { + assert.expect(5); - class Person extends Model { - @attr name; - @hasMany('pet', { async: false, inverse: null }) - pets; - } + const Person = Model.extend({ + name: attr('string'), + pets: hasMany('pet', { async: false, inverse: null }), + }); - class Pet extends Model { - @attr name; - @belongsTo('person', { async: false, inverse: null }) - person; - } + const Pet = Model.extend({ + name: attr('string'), + person: belongsTo('person', { async: false, inverse: null }), + }); - this.owner.register('model:person', Person); - this.owner.register('model:pet', Pet); + this.owner.register('model:person', Person); + this.owner.register('model:pet', Pet); - const store = this.owner.lookup('service:store'); - const adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); - adapter.shouldBackgroundReloadRecord = () => false; - adapter.deleteRecord = () => { - return Promise.resolve({ data: null }); - }; + adapter.shouldBackgroundReloadRecord = () => false; + adapter.deleteRecord = () => { + return Promise.resolve({ data: null }); + }; - store.push({ - data: { - type: 'person', - id: '1', - attributes: { - name: 'Chris Thoburn', - }, - relationships: { - pets: { - data: [{ type: 'pet', id: '1' }], - }, - }, + store.push({ + data: { + type: 'person', + id: '1', + attributes: { + name: 'Chris Thoburn', }, - included: [ - { - type: 'pet', - id: '1', - attributes: { - name: 'Shenanigans', - }, - }, - { - type: 'pet', - id: '2', - attributes: { - name: 'Rambunctious', - }, - }, - { - type: 'pet', - id: '3', - attributes: { - name: 'Rebel', - }, - }, - ], - }); - - const person = store.peekRecord('person', '1'); - const pets = await person.pets; - - const shen = pets.at(0); - const rebel = store.peekRecord('pet', '3'); - - assert.strictEqual(shen.name, 'Shenanigans', 'precond - relationships work'); - assert.deepEqual( - pets.map((p) => p.id), - ['1'], - 'precond - relationship has the correct pets to start' - ); - - pets.push(rebel); - await settled(); - - assert.deepEqual( - pets.map((p) => p.id), - ['1', '3'], - 'precond2 - relationship now has the correct two pets' - ); - - store.push({ - data: { - type: 'person', - id: '1', - relationships: { - pets: { - data: [{ type: 'pet', id: '2' }], - }, + relationships: { + pets: { + data: [{ type: 'pet', id: '1' }], }, }, - }); - - const hasManyCanonical = person.hasMany('pets').hasManyRelationship.remoteState; - - assert.todo.deepEqual( - pets.map((p) => p.id), - ['2', '3'], - 'relationship now has the correct current pets' - ); - assert.deepEqual( - hasManyCanonical.map((p) => p.id), - ['2'], - 'relationship now has the correct canonical pets' - ); - } - ); - - todo( - '[push hasMany] items removed from a hasMany relationship are not cleared by a store.push', - async function (assert) { - assert.expect(5); - - class Person extends Model { - @attr name; - @hasMany('pet', { async: false, inverse: null }) - pets; - } - - class Pet extends Model { - @attr name; - @belongsTo('person', { async: false, inverse: null }) - person; - } - - this.owner.register('model:person', Person); - this.owner.register('model:pet', Pet); - - const store = this.owner.lookup('service:store'); - const adapter = store.adapterFor('application'); - - adapter.shouldBackgroundReloadRecord = () => false; - adapter.deleteRecord = () => { - return Promise.resolve({ data: null }); - }; - - store.push({ - data: { - type: 'person', + }, + included: [ + { + type: 'pet', id: '1', attributes: { - name: 'Chris Thoburn', - }, - relationships: { - pets: { - data: [ - { type: 'pet', id: '1' }, - { type: 'pet', id: '3' }, - ], - }, + name: 'Shenanigans', }, }, - included: [ - { - type: 'pet', - id: '1', - attributes: { - name: 'Shenanigans', - }, - }, - { - type: 'pet', - id: '2', - attributes: { - name: 'Rambunctious', - }, - }, - { - type: 'pet', - id: '3', - attributes: { - name: 'Rebel', - }, + { + type: 'pet', + id: '2', + attributes: { + name: 'Rambunctious', }, - ], - }); - - const person = store.peekRecord('person', '1'); - const pets = person.pets; - - const shen = pets.at(0); - const rebel = store.peekRecord('pet', '3'); - - assert.strictEqual(shen.name, 'Shenanigans', 'precond - relationships work'); - assert.deepEqual( - pets.map((p) => p.id), - ['1', '3'], - 'precond - relationship has the correct pets to start' - ); - - pets.splice(pets.indexOf(rebel), 1); - - assert.deepEqual( - pets.map((p) => p.id), - ['1'], - 'precond2 - relationship now has the correct pet' - ); - - store.push({ - data: { - type: 'person', - id: '1', - relationships: { - pets: { - data: [ - { type: 'pet', id: '2' }, - { type: 'pet', id: '3' }, - ], - }, + }, + { + type: 'pet', + id: '3', + attributes: { + name: 'Rebel', }, }, - }); - - const hasManyCanonical = person.hasMany('pets').hasManyRelationship.remoteState; - - assert.todo.deepEqual( - pets.map((p) => p.id), - ['2'], - 'relationship now has the correct current pets' - ); - assert.deepEqual( - hasManyCanonical.map((p) => p.id), - ['2', '3'], - 'relationship now has the correct canonical pets' - ); - } - ); - } - - test('[push hasMany] new items added to a hasMany relationship are not cleared by a store.push', async function (assert) { - assert.expect(5); + ], + }); - class Person extends Model { - @attr name; - @hasMany('pet', { async: false, inverse: null, resetOnRemoteUpdate: false }) - pets; - } + const person = store.peekRecord('person', '1'); + const pets = await person.pets; - class Pet extends Model { - @attr name; - @belongsTo('person', { async: false, inverse: null }) - person; - } + const shen = pets.at(0); + const rebel = store.peekRecord('pet', '3'); - this.owner.register('model:person', Person); - this.owner.register('model:pet', Pet); + assert.strictEqual(shen.name, 'Shenanigans', 'precond - relationships work'); + assert.deepEqual( + pets.map((p) => p.id), + ['1'], + 'precond - relationship has the correct pets to start' + ); - const store = this.owner.lookup('service:store'); - const adapter = store.adapterFor('application'); + pets.push(rebel); + await settled(); - adapter.shouldBackgroundReloadRecord = () => false; - adapter.deleteRecord = () => { - return Promise.resolve({ data: null }); - }; + assert.deepEqual( + pets.map((p) => p.id), + ['1', '3'], + 'precond2 - relationship now has the correct two pets' + ); - store.push({ - data: { - type: 'person', - id: '1', - attributes: { - name: 'Chris Thoburn', - }, - relationships: { - pets: { - data: [{ type: 'pet', id: '1' }], - }, - }, - }, - included: [ - { - type: 'pet', + store.push({ + data: { + type: 'person', id: '1', - attributes: { - name: 'Shenanigans', - }, - }, - { - type: 'pet', - id: '2', - attributes: { - name: 'Rambunctious', - }, - }, - { - type: 'pet', - id: '3', - attributes: { - name: 'Rebel', - }, - }, - ], - }); - - const person = store.peekRecord('person', '1'); - const pets = await person.pets; - - const shen = pets.at(0); - const rebel = store.peekRecord('pet', '3'); - - assert.strictEqual(shen.name, 'Shenanigans', 'precond - relationships work'); - assert.deepEqual( - pets.map((p) => p.id), - ['1'], - 'precond - relationship has the correct pets to start' - ); - - pets.push(rebel); - await settled(); - - assert.deepEqual( - pets.map((p) => p.id), - ['1', '3'], - 'precond2 - relationship now has the correct two pets' - ); - - store.push({ - data: { - type: 'person', - id: '1', - relationships: { - pets: { - data: [{ type: 'pet', id: '2' }], + relationships: { + pets: { + data: [{ type: 'pet', id: '2' }], + }, }, }, - }, - }); + }); - const hasManyCanonical = person.hasMany('pets').hasManyRelationship.remoteState; + const hasManyCanonical = person.hasMany('pets').hasManyRelationship.remoteState; - assert.deepEqual( - pets.map((p) => p.id), - ['2', '3'], - 'relationship now has the correct current pets' - ); - assert.deepEqual( - hasManyCanonical.map((p) => p.id), - ['2'], - 'relationship now has the correct canonical pets' - ); - }); + assert.todo.deepEqual( + pets.map((p) => p.id), + ['2', '3'], + 'relationship now has the correct current pets' + ); + assert.deepEqual( + hasManyCanonical.map((p) => p.id), + ['2'], + 'relationship now has the correct canonical pets' + ); + } + ); - test('[push hasMany] items removed from a hasMany relationship are not cleared by a store.push', async function (assert) { - assert.expect(5); + todo( + '[push hasMany] items removed from a hasMany relationship are not cleared by a store.push', + async function (assert) { + assert.expect(5); - class Person extends Model { - @attr name; - @hasMany('pet', { async: false, inverse: null, resetOnRemoteUpdate: false }) - pets; - } + const Person = Model.extend({ + name: attr('string'), + pets: hasMany('pet', { async: false, inverse: null }), + }); - class Pet extends Model { - @attr name; - @belongsTo('person', { async: false, inverse: null }) - person; - } + const Pet = Model.extend({ + name: attr('string'), + person: belongsTo('person', { async: false, inverse: null }), + }); - this.owner.register('model:person', Person); - this.owner.register('model:pet', Pet); + this.owner.register('model:person', Person); + this.owner.register('model:pet', Pet); - const store = this.owner.lookup('service:store'); - const adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); - adapter.shouldBackgroundReloadRecord = () => false; - adapter.deleteRecord = () => { - return Promise.resolve({ data: null }); - }; + adapter.shouldBackgroundReloadRecord = () => false; + adapter.deleteRecord = () => { + return Promise.resolve({ data: null }); + }; - store.push({ - data: { - type: 'person', - id: '1', - attributes: { - name: 'Chris Thoburn', - }, - relationships: { - pets: { - data: [ - { type: 'pet', id: '1' }, - { type: 'pet', id: '3' }, - ], - }, - }, - }, - included: [ - { - type: 'pet', + store.push({ + data: { + type: 'person', id: '1', attributes: { - name: 'Shenanigans', + name: 'Chris Thoburn', }, - }, - { - type: 'pet', - id: '2', - attributes: { - name: 'Rambunctious', + relationships: { + pets: { + data: [ + { type: 'pet', id: '1' }, + { type: 'pet', id: '3' }, + ], + }, }, }, - { - type: 'pet', - id: '3', - attributes: { - name: 'Rebel', + included: [ + { + type: 'pet', + id: '1', + attributes: { + name: 'Shenanigans', + }, }, - }, - ], - }); + { + type: 'pet', + id: '2', + attributes: { + name: 'Rambunctious', + }, + }, + { + type: 'pet', + id: '3', + attributes: { + name: 'Rebel', + }, + }, + ], + }); - const person = store.peekRecord('person', '1'); - const pets = person.pets; + const person = store.peekRecord('person', '1'); + const pets = person.pets; - const shen = pets.at(0); - const rebel = store.peekRecord('pet', '3'); + const shen = pets.at(0); + const rebel = store.peekRecord('pet', '3'); - assert.strictEqual(shen.name, 'Shenanigans', 'precond - relationships work'); - assert.deepEqual( - pets.map((p) => p.id), - ['1', '3'], - 'precond - relationship has the correct pets to start' - ); + assert.strictEqual(shen.name, 'Shenanigans', 'precond - relationships work'); + assert.deepEqual( + pets.map((p) => p.id), + ['1', '3'], + 'precond - relationship has the correct pets to start' + ); - pets.splice(pets.indexOf(rebel), 1); + pets.splice(pets.indexOf(rebel), 1); - assert.deepEqual( - pets.map((p) => p.id), - ['1'], - 'precond2 - relationship now has the correct pet' - ); + assert.deepEqual( + pets.map((p) => p.id), + ['1'], + 'precond2 - relationship now has the correct pet' + ); - store.push({ - data: { - type: 'person', - id: '1', - relationships: { - pets: { - data: [ - { type: 'pet', id: '2' }, - { type: 'pet', id: '3' }, - ], + store.push({ + data: { + type: 'person', + id: '1', + relationships: { + pets: { + data: [ + { type: 'pet', id: '2' }, + { type: 'pet', id: '3' }, + ], + }, }, }, - }, - }); + }); - const hasManyCanonical = person.hasMany('pets').hasManyRelationship.remoteState; + const hasManyCanonical = person.hasMany('pets').hasManyRelationship.remoteState; - assert.deepEqual( - pets.map((p) => p.id), - ['2'], - 'relationship now has the correct current pets' - ); - assert.deepEqual( - hasManyCanonical.map((p) => p.id), - ['2', '3'], - 'relationship now has the correct canonical pets' - ); - }); + assert.todo.deepEqual( + pets.map((p) => p.id), + ['2'], + 'relationship now has the correct current pets' + ); + assert.deepEqual( + hasManyCanonical.map((p) => p.id), + ['2', '3'], + 'relationship now has the correct canonical pets' + ); + } + ); test('new items added to an async hasMany relationship are not cleared by a delete', async function (assert) { assert.expect(7); @@ -2341,7 +2106,7 @@ module('unit/model/relationships - hasMany', function (hooks) { ); test('possible to replace items in a relationship using setObjects w/ Ember Enumerable Array/Object as the argument (GH-2533)', function (assert) { - assert.expect(2); + assert.expect(DEPRECATE_ARRAY_LIKE ? 3 : 2); const Tag = Model.extend({ name: attr('string'), @@ -2405,13 +2170,78 @@ module('unit/model/relationships - hasMany', function (hooks) { const sylvain = store.peekRecord('person', '2'); // Test that since sylvain.tags instanceof ManyArray, // adding records on Relationship iterates correctly. - tom.tags.length = 0; - tom.tags.push(...sylvain.tags); + if (DEPRECATE_ARRAY_LIKE) { + tom.tags.setObjects(sylvain.tags); + assert.expectDeprecation({ id: 'ember-data:deprecate-array-like' }); + } else { + tom.tags.length = 0; + tom.tags.push(...sylvain.tags); + } assert.strictEqual(tom.tags.length, 1); assert.strictEqual(tom.tags.at(0), store.peekRecord('tag', 2)); }); + deprecatedTest( + 'Replacing `has-many` with non-array will throw assertion', + { id: 'ember-data:deprecate-array-like', until: '5.0' }, + function (assert) { + assert.expect(1); + + const Tag = Model.extend({ + name: attr('string'), + person: belongsTo('person', { async: false, inverse: 'tags' }), + }); + + const Person = Model.extend({ + name: attr('string'), + tags: hasMany('tag', { async: false, inverse: 'person' }), + }); + + this.owner.register('model:tag', Tag); + this.owner.register('model:person', Person); + + const store = this.owner.lookup('service:store'); + + store.push({ + data: [ + { + type: 'person', + id: '1', + attributes: { + name: 'Tom Dale', + }, + relationships: { + tags: { + data: [{ type: 'tag', id: '1' }], + }, + }, + }, + { + type: 'tag', + id: '1', + attributes: { + name: 'ember', + }, + }, + { + type: 'tag', + id: '2', + attributes: { + name: 'ember-data', + }, + }, + ], + }); + + const tom = store.peekRecord('person', '1'); + const tag = store.peekRecord('tag', '2'); + assert.expectAssertion(() => { + tom.tags.setObjects(tag); + }, /ManyArray.setObjects expects to receive an array as its argument/); + } + ); + test('it is possible to remove an item from a relationship', async function (assert) { assert.expect(2); @@ -2962,4 +2792,43 @@ module('unit/model/relationships - hasMany', function (hooks) { await settled(); }); + + test('checks if passed array only contains instances of Model', async function (assert) { + class Person extends Model { + @attr name; + } + class Tag extends Model { + @hasMany('person', { async: true, inverse: null }) people; + } + + this.owner.register('model:tag', Tag); + this.owner.register('model:person', Person); + + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); + + adapter.findRecord = function () { + return { + data: { + type: 'person', + id: '1', + }, + }; + }; + + const tag = store.createRecord('tag'); + const person = store.findRecord('person', '1'); + await person; + + tag.people = [person]; + + assert.expectAssertion(() => { + tag.people = [person, {}]; + }, /All elements of a hasMany relationship must be instances of Model/); + assert.expectDeprecation({ + id: 'ember-data:deprecate-promise-proxies', + count: /* inline-macro-config */ DEPRECATE_MANY_ARRAY_DUPLICATES ? 5 : 4, + until: '5.0', + }); + }); }); diff --git a/tests/main/tests/unit/record-arrays/adapter-populated-record-array-test.js b/tests/main/tests/unit/record-arrays/adapter-populated-record-array-test.js index e062ef640ab..3ddab68d8de 100644 --- a/tests/main/tests/unit/record-arrays/adapter-populated-record-array-test.js +++ b/tests/main/tests/unit/record-arrays/adapter-populated-record-array-test.js @@ -50,6 +50,18 @@ module('unit/record-arrays/collection', function (hooks) { assert.strictEqual(recordArray.links, 'foo'); }); + testInDebug('#replace() throws error', function (assert) { + const recordArray = new CollectionRecordArray({ type: 'recordType', identifiers: [] }); + + assert.throws( + () => { + recordArray.replace(); + }, + Error('Mutating this array of records via splice is not allowed.'), + 'throws error' + ); + assert.expectDeprecation({ id: 'ember-data:deprecate-array-like' }); + }); testInDebug('mutation throws error', function (assert) { const recordArray = new CollectionRecordArray({ type: 'recordType', identifiers: [] }); diff --git a/tests/main/tests/unit/record-arrays/record-array-test.js b/tests/main/tests/unit/record-arrays/record-array-test.js index bd24c73088f..56dc55d4989 100644 --- a/tests/main/tests/unit/record-arrays/record-array-test.js +++ b/tests/main/tests/unit/record-arrays/record-array-test.js @@ -7,6 +7,7 @@ import Model, { attr } from '@ember-data/model'; import { createDeferred } from '@ember-data/request'; import { recordIdentifierFor } from '@ember-data/store'; import { LiveArray, SOURCE } from '@ember-data/store/-private'; +import { deprecatedTest } from '@ember-data/unpublished-test-infra/test-support/deprecated-test'; import testInDebug from '@ember-data/unpublished-test-infra/test-support/test-in-debug'; class Tag extends Model { @@ -39,6 +40,19 @@ module('unit/record-arrays/live-array - LiveArray', function (hooks) { assert.strictEqual(recordArray.store, store); }); + testInDebug('#replace() throws error', async function (assert) { + const recordArray = new LiveArray({ identifiers: [], type: 'recordType' }); + + assert.throws( + () => { + recordArray.replace(); + }, + Error('Mutating this array of records via splice is not allowed.'), + 'throws error' + ); + assert.expectDeprecation({ id: 'ember-data:deprecate-array-like' }); + }); + testInDebug('Mutation throws error', async function (assert) { const recordArray = new LiveArray({ identifiers: [], type: 'recordType' }); @@ -85,6 +99,219 @@ module('unit/record-arrays/live-array - LiveArray', function (hooks) { assert.strictEqual(recordArray[3], undefined); }); + deprecatedTest( + '#filterBy', + { id: 'ember-data:deprecate-array-like', until: '5.0', count: 3 }, + async function (assert) { + this.owner.register('model:tag', Tag); + const store = this.owner.lookup('service:store'); + + const records = store.push({ + data: [ + { + type: 'tag', + id: '1', + attributes: { + name: 'first', + }, + }, + { + type: 'tag', + id: '3', + }, + { + type: 'tag', + id: '5', + attributes: { + name: 'fifth', + }, + }, + ], + }); + + const recordArray = new LiveArray({ + type: 'recordType', + identifiers: records.map(recordIdentifierFor), + store, + }); + + assert.strictEqual(recordArray.length, 3); + assert.strictEqual(recordArray.filterBy('id', '3').length, 1); + assert.strictEqual(recordArray.filterBy('id').length, 3); + assert.strictEqual(recordArray.filterBy('name').length, 2); + } + ); + + deprecatedTest('#reject', { id: 'ember-data:deprecate-array-like', until: '5.0', count: 3 }, async function (assert) { + this.owner.register('model:tag', Tag); + const store = this.owner.lookup('service:store'); + + const records = store.push({ + data: [ + { + type: 'tag', + id: '1', + attributes: { + name: 'first', + }, + }, + { + type: 'tag', + id: '3', + }, + { + type: 'tag', + id: '5', + attributes: { + name: 'fifth', + }, + }, + ], + }); + + const recordArray = new LiveArray({ + type: 'recordType', + identifiers: records.map(recordIdentifierFor), + store, + }); + + assert.strictEqual(recordArray.length, 3); + assert.strictEqual(recordArray.reject(({ id }) => id === '3').length, 2); + assert.strictEqual(recordArray.reject(({ id }) => id).length, 0); + assert.strictEqual(recordArray.reject(({ name }) => name).length, 1); + }); + + deprecatedTest( + '#rejectBy', + { id: 'ember-data:deprecate-array-like', until: '5.0', count: 3 }, + async function (assert) { + this.owner.register('model:tag', Tag); + const store = this.owner.lookup('service:store'); + + const records = store.push({ + data: [ + { + type: 'tag', + id: '1', + attributes: { + name: 'first', + }, + }, + { + type: 'tag', + id: '3', + }, + { + type: 'tag', + id: '5', + attributes: { + name: 'fifth', + }, + }, + ], + }); + + const recordArray = new LiveArray({ + type: 'recordType', + identifiers: records.map(recordIdentifierFor), + store, + }); + + assert.strictEqual(recordArray.length, 3); + assert.strictEqual(recordArray.rejectBy('id', '3').length, 2); + assert.strictEqual(recordArray.rejectBy('id').length, 0); + assert.strictEqual(recordArray.rejectBy('name').length, 1); + } + ); + + deprecatedTest( + '#lastObject and #firstObject', + { id: 'ember-data:deprecate-array-like', until: '5.0', count: 2 }, + async function (assert) { + this.owner.register('model:tag', Tag); + const store = this.owner.lookup('service:store'); + + const records = store.push({ + data: [ + { + type: 'tag', + id: '1', + attributes: { + name: 'first', + }, + }, + { + type: 'tag', + id: '3', + }, + { + type: 'tag', + id: '5', + attributes: { + name: 'fifth', + }, + }, + ], + }); + + const recordArray = new LiveArray({ + type: 'recordType', + identifiers: records.map(recordIdentifierFor), + store, + }); + + assert.strictEqual(recordArray.length, 3); + assert.strictEqual(recordArray.firstObject.id, '1'); + assert.strictEqual(recordArray.lastObject.id, '5'); + } + ); + + deprecatedTest( + '#objectAt and #objectsAt', + { id: 'ember-data:deprecate-array-like', until: '5.0', count: 5 }, + async function (assert) { + this.owner.register('model:tag', Tag); + const store = this.owner.lookup('service:store'); + + const records = store.push({ + data: [ + { + type: 'tag', + id: '1', + attributes: { + name: 'first', + }, + }, + { + type: 'tag', + id: '3', + }, + { + type: 'tag', + id: '5', + attributes: { + name: 'fifth', + }, + }, + ], + }); + + const recordArray = new LiveArray({ + type: 'recordType', + identifiers: records.map(recordIdentifierFor), + store, + }); + + assert.strictEqual(recordArray.length, 3); + assert.strictEqual(recordArray.objectAt(0).id, '1'); + assert.strictEqual(recordArray.objectAt(-1).id, '5'); + assert.deepEqual( + recordArray.objectsAt([2, 1]).map((r) => r.id), + ['5', '3'] + ); + } + ); + test('#update', async function (assert) { let findAllCalled = 0; const deferred = createDeferred(); diff --git a/tests/main/tests/unit/store/adapter-interop-test.js b/tests/main/tests/unit/store/adapter-interop-test.js index 80c50987b47..29585f9502d 100644 --- a/tests/main/tests/unit/store/adapter-interop-test.js +++ b/tests/main/tests/unit/store/adapter-interop-test.js @@ -1225,4 +1225,18 @@ module('unit/store/adapter-interop - Store working with a Adapter', function (ho await settled(); assert.strictEqual(store.peekRecord('person', 1).name, 'Tom', 'after background reload name is loaded'); }); + + testInDebug('Calling adapterFor with a model class should assert', function (assert) { + const Person = Model.extend(); + + this.owner.register('model:person', Person); + + const store = this.owner.lookup('service:store'); + + assert.expectAssertion(() => { + store.adapterFor(Person); + }, /Passing classes to store.adapterFor has been removed/); + + assert.expectDeprecation({ id: 'ember-data:deprecate-early-static' }); + }); }); diff --git a/tests/main/tests/unit/store/serializer-for-test.js b/tests/main/tests/unit/store/serializer-for-test.js index e7a439b1889..8ce56163706 100644 --- a/tests/main/tests/unit/store/serializer-for-test.js +++ b/tests/main/tests/unit/store/serializer-for-test.js @@ -4,6 +4,7 @@ import { setupTest } from 'ember-qunit'; import Model from '@ember-data/model'; import JSONSerializer from '@ember-data/serializer/json'; +import testInDebug from '@ember-data/unpublished-test-infra/test-support/test-in-debug'; let store, Person; @@ -39,4 +40,12 @@ module('unit/store/serializer_for - Store#serializerFor', function (hooks) { 'serializer returned from serializerFor is an instance of ApplicationSerializer' ); }); + + testInDebug('Calling serializerFor with a model class should assert', function (assert) { + assert.expectAssertion(() => { + store.serializerFor(Person); + }, /Passing classes to store.serializerFor has been removed/); + + assert.expectDeprecation({ id: 'ember-data:deprecate-early-static' }); + }); }); diff --git a/tests/main/tests/unit/system/snapshot-record-array-test.js b/tests/main/tests/unit/system/snapshot-record-array-test.js index 2bd81d943f3..0ca3f94b149 100644 --- a/tests/main/tests/unit/system/snapshot-record-array-test.js +++ b/tests/main/tests/unit/system/snapshot-record-array-test.js @@ -6,6 +6,7 @@ import { setupTest } from 'ember-qunit'; import { FetchManager, SnapshotRecordArray } from '@ember-data/legacy-compat/-private'; import Model, { attr } from '@ember-data/model'; +import { deprecatedTest } from '@ember-data/unpublished-test-infra/test-support/deprecated-test'; module('Unit - snapshot-record-array', function (hooks) { setupTest(hooks); @@ -74,4 +75,43 @@ module('Unit - snapshot-record-array', function (hooks) { assert.strictEqual(snapshot.snapshots()[0], snapshotsTaken[0], 'should return the exact same snapshot'); assert.strictEqual(didTakeSnapshot, 1, 'still only one snapshot should have been taken'); }); + + deprecatedTest( + 'SnapshotRecordArray.type loads the class lazily', + { + id: 'ember-data:deprecate-snapshot-model-class-access', + count: 1, + until: '5.0', + }, + function (assert) { + const array = A([1, 2]); + let typeLoaded = false; + + Object.defineProperty(array, 'type', { + get() { + typeLoaded = true; + return 'some type'; + }, + }); + + const options = { + adapterOptions: 'some options', + include: 'include me', + }; + + const snapshot = new SnapshotRecordArray( + { + peekAll() { + return array; + }, + }, + 'user', + options + ); + + assert.false(typeLoaded, 'model class is not eager loaded'); + assert.strictEqual(snapshot.type, 'some type'); + assert.true(typeLoaded, 'model class is loaded'); + } + ); }); diff --git a/tests/main/tsconfig.json b/tests/main/tsconfig.json index 32a05641fb9..5bf99cce967 100644 --- a/tests/main/tsconfig.json +++ b/tests/main/tsconfig.json @@ -53,8 +53,6 @@ "@ember-data/unpublished-test-infra/*": ["../../packages/unpublished-test-infra/unstable-preview-types/*"], "@warp-drive/core-types": ["../../packages/core-types/unstable-preview-types"], "@warp-drive/core-types/*": ["../../packages/core-types/unstable-preview-types/*"], - "@warp-drive/schema-record": ["../../packages/schema-record/unstable-preview-types"], - "@warp-drive/schema-record/*": ["../../packages/schema-record/unstable-preview-types/*"], "@warp-drive/build-config": ["../../packages/build-config/unstable-preview-types"], "@warp-drive/build-config/*": ["../../packages/build-config/unstable-preview-types/*"], "@warp-drive/holodeck": ["../../packages/holodeck/unstable-preview-types"], @@ -111,9 +109,6 @@ }, { "path": "../../packages/-ember-data" - }, - { - "path": "../../packages/schema-record" } ] } diff --git a/tests/performance/app/routes/relationship-materialization-complex.js b/tests/performance/app/routes/relationship-materialization-complex.js index 0b0d27863e5..7892858bc43 100644 --- a/tests/performance/app/routes/relationship-materialization-complex.js +++ b/tests/performance/app/routes/relationship-materialization-complex.js @@ -19,8 +19,6 @@ export default Route.extend({ const seen = new Set(); peekedParents.forEach((parent) => iterateParent(parent, seen)); performance.mark('end-relationship-materialization'); - // performance.measure('full-test', 'start-push-payload', 'end-relationship-materialization'); - // performance.measure('materialization', 'start-relationship-materialization', 'end-relationship-materialization'); }, }); diff --git a/tests/performance/ember-cli-build.js b/tests/performance/ember-cli-build.js index 8b87987fddb..7896757c1ef 100644 --- a/tests/performance/ember-cli-build.js +++ b/tests/performance/ember-cli-build.js @@ -2,19 +2,13 @@ const EmberApp = require('ember-cli/lib/broccoli/ember-app'); -module.exports = async function (defaults) { - const { setConfig } = await import('@warp-drive/build-config'); +module.exports = function (defaults) { const app = new EmberApp(defaults, { fingerprint: { enabled: false, }, - }); - setConfig(app, __dirname, { - compatWith: '99', - debug: { - // LOG_NOTIFICATIONS: true, - // LOG_INSTANCE_CACHE: true, - // LOG_METRIC_COUNTS: true, + emberData: { + compatWith: '99', }, }); @@ -35,11 +29,6 @@ module.exports = async function (defaults) { const TerserPlugin = require('terser-webpack-plugin'); return require('@embroider/compat').compatBuild(app, Webpack, { - skipBabel: [ - { - package: 'qunit', - }, - ], // // staticAddonTestSupportTrees: true, // staticAddonTrees: true, diff --git a/tests/performance/package.json b/tests/performance/package.json index 9e70e2123a7..7c4e160dc1e 100644 --- a/tests/performance/package.json +++ b/tests/performance/package.json @@ -1,6 +1,6 @@ { "name": "performance-test-app", - "version": "5.4.0-alpha.136", + "version": "4.13.0-alpha.3", "private": true, "description": "Small description for performance-test-app goes here", "repository": { @@ -21,7 +21,7 @@ "sync-hardlinks": "bun run sync-dependencies-meta-injected" }, "dependencies": { - "ember-auto-import": "2.10.0", + "ember-auto-import": "^2.8.1", "ember-data": "workspace:*", "pnpm-sync-dependencies-meta-injected": "0.0.14", "webpack": "^5.92.0" @@ -35,15 +35,14 @@ "@babel/core": "^7.24.5", "@babel/runtime": "^7.24.5", "@ember/optional-features": "^2.1.0", - "@ember/test-helpers": "5.1.0", + "@ember/test-helpers": "4.0.4", "@ember/test-waiters": "^3.1.0", - "@embroider/compat": "^3.8.0", - "@embroider/core": "^3.5.0", - "@embroider/webpack": "^4.0.9", + "@embroider/compat": "^3.5.3", + "@embroider/core": "^3.4.12", + "@embroider/webpack": "^4.0.3", "@glimmer/component": "^1.1.2", "@glimmer/tracking": "^1.1.2", "@warp-drive/internal-config": "workspace:*", - "@warp-drive/build-config": "workspace:*", "ember-cli": "~5.12.0", "ember-cli-babel": "^8.2.0", "ember-cli-dependency-checker": "^3.3.2", @@ -53,7 +52,7 @@ "ember-resolver": "^11.0.1", "ember-source": "~5.12.0", "loader.js": "^4.7.0", - "terser-webpack-plugin": "^5.3.11", + "terser-webpack-plugin": "^5.3.10", "webpack": "^5.92.0", "zlib": "1.0.5" }, diff --git a/tests/vite-basic-compat/package.json b/tests/vite-basic-compat/package.json index d18984356f9..61c786a869a 100644 --- a/tests/vite-basic-compat/package.json +++ b/tests/vite-basic-compat/package.json @@ -1,6 +1,6 @@ { "name": "vite-basic-compat", - "version": "0.0.1-alpha.19", + "version": "4.13.0-alpha.3", "private": true, "description": "Small description for vite-basic-compat goes here", "repository": "", @@ -89,7 +89,7 @@ "@ember-data/unpublished-test-infra": "workspace:*", "@ember/optional-features": "^2.1.0", "@ember/string": "^4.0.0", - "@ember/test-helpers": "5.1.0", + "@ember/test-helpers": "^4.0.4", "@ember/test-waiters": "^3.1.0", "@embroider/compat": "3.7.1-unstable.4070ba7", "@embroider/config-meta-loader": "0.0.1-unstable.4070ba7", @@ -99,7 +99,7 @@ "@glimmer/component": "^1.1.2", "@glimmer/tracking": "^1.1.2", "@rollup/plugin-babel": "^6.0.4", - "@tsconfig/ember": "^3.0.9", + "@tsconfig/ember": "^3.0.8", "@types/eslint__js": "^8.42.3", "@types/qunit": "2.19.10", "@types/rsvp": "^4.0.9", @@ -109,9 +109,9 @@ "@warp-drive/core-types": "workspace:*", "@warp-drive/internal-config": "workspace:*", "babel-plugin-ember-template-compilation": "^2.3.0", - "concurrently": "^9.1.2", + "concurrently": "^9.1.0", "decorator-transforms": "^2.3.0", - "ember-auto-import": "2.10.0", + "ember-auto-import": "^2.8.1", "ember-cli": "~5.12.0", "ember-cli-babel": "^8.2.0", "ember-cli-htmlbars": "^6.3.0", diff --git a/tsconfig.json b/tsconfig.json index e36abab2bd7..0715526d567 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -25,7 +25,7 @@ { "path": "./packages/serializer" }, { "path": "./packages/store" }, { "path": "./packages/tracking" }, - { "path": "./packages/unpublished-test-infra" }, - { "path": "./packages/experiments" } + { "path": "./packages/unpublished-test-infra" } + // { "path": "./packages/experiments" } ] }